diff --git a/code/SoS/association_scan/TensorQTL/TensorQTL.ipynb b/code/SoS/association_scan/TensorQTL/TensorQTL.ipynb
index b7c88b61a..a62b12f67 100644
--- a/code/SoS/association_scan/TensorQTL/TensorQTL.ipynb
+++ b/code/SoS/association_scan/TensorQTL/TensorQTL.ipynb
@@ -8,7 +8,7 @@
"source": [
"# QTL Association Testing (TensorQTL)\n",
"\n",
- "This notebook performs cis- and trans-QTL association testing using [TensorQTL](https://doi.org/10.1186/s13059-019-1836-7). For each molecular phenotype it tests association against nearby (cis) or genome-wide (trans) genetic variants and reports nominal and region-level statistics."
+ "Tests each molecular phenotype for association with nearby (cis) or genome-wide (trans) genetic variants using [TensorQTL](https://doi.org/10.1186/s13059-019-1836-7), reporting both nominal pair statistics and region-level significance."
]
},
{
@@ -17,16 +17,17 @@
"kernel": "SoS"
},
"source": [
- "## Description\n",
+ "## Overview\n",
"\n",
- "Two workflows are available:\n",
+ "The model is the standard QTL regression: each molecular phenotype is regressed on each candidate variant, with the covariate matrix (known covariates plus hidden factors) entering every fit. TensorQTL evaluates those regressions as batched tensor operations, on GPU when one is available and on CPU otherwise, which is what makes a genome-wide scan tractable. Two workflows expose that model.\n",
"\n",
- "- `cis` : association between each molecular phenotype and variants within a window around it (default 1 Mb around the TSS). Produces nominal pair statistics plus region (gene) level significance.\n",
- "- `trans`: association between phenotypes and variants on a chosen genotype chromosome, optionally restricted to a region list.\n",
+ "`cis` tests each phenotype against the variants near it. The search region is a symmetric radius around the phenotype set by `--window`, 1 Mb by default, or an explicit per-region interval supplied through `--customized-cis-windows`, in which case `--window` is forced to zero. It runs as two chained steps: the first computes nominal statistics for every variant-phenotype pair on a chromosome and, unless `--no-permutation` is given, a permutation pass that fits a beta distribution to each region null; the second gathers the per-chromosome results into a single region-level table carrying permutation and beta-approximation p-values with q-values and Benjamini-Hochberg FDR.\n",
"\n",
- "Inputs are the by-chromosome genotype/phenotype file lists and the covariate matrix produced by the genotype, phenotype, and covariate preprocessing steps.\n",
+ "`trans` tests phenotypes against variants far from them. A full scan is every phenotype against every variant, so memory rather than arithmetic is the binding constraint. The step therefore handles one phenotype chromosome at a time and loads a single genotype chromosome, named by `--trans-geno-chromosome`, so a genome-wide analysis is assembled from chromosome pairs rather than held in memory at once. Results are thinned as they are produced: `--pval` discards pairs above a nominal cutoff and records that cutoff in the output file name, while `--pvalue-cutoff` and `--qvalue-cutoff` set the significance filter applied to what remains. Permutation defaults to off here, because a null permuted within one chromosome is not the genome-wide null.\n",
"\n",
- "**Timing:** cis ~1 min; trans ~1.5 min on the toy dataset."
+ "How rare a variant may be is controlled in both workflows by `--MAC`, a minor-allele-count floor, or by `--maf-threshold`, which overrides it with a frequency floor.\n",
+ "\n",
+ "**When to run it.** After genotype, phenotype, and covariate preprocessing, which produce the by-chromosome file lists and the covariate matrix consumed here. The nominal summary statistics are the input to fine-mapping (`mnm_regression`) and to colocalization; the region-level table is what downstream enrichment and reporting read."
]
},
{
@@ -35,14 +36,50 @@
"kernel": "SoS"
},
"source": [
- "## Input Files\n",
- "\n",
- "| Parameter | Toy file | Description |\n",
- "|---|---|---|\n",
- "| `--genotype-file` | `output/genotype_by_chrom/protocol_example.genotype.merged.plink_qc.genotype_by_chrom_files.txt` | Two-column list (chrom id, PLINK bed prefix) of by-chromosome genotypes |\n",
- "| `--phenotype-file` | `output/phenotype/phenotype_by_chrom_for_cis/bulk_rnaseq.phenotype_by_chrom_files.txt` | List of bgzipped+tabixed molecular phenotype `bed.gz` files by chromosome |\n",
- "| `--covariate-file` | `output/covariate/...prune.pca.Marchenko_PC.gz` | Covariate matrix (known covariates + hidden factors) |\n",
- "| `--region-list` | `data/combined_AD_genes.csv` | (trans, optional) subset of regions/genes to analyze; gene name in column 4 |"
+ "## Input\n",
+ "\n",
+ "Required:\n",
+ "\n",
+ "* `--phenotype-file` -- one bgzipped and tabixed molecular phenotype `bed.gz`, or a text file listing one such file per chromosome, e.g. `output/phenotype/phenotype_by_chrom_for_cis/bulk_rnaseq.phenotype_by_chrom_files.txt`.\n",
+ "* `--genotype-file` -- a PLINK bed/bim/fam prefix, or a two-column list of chromosome ID and PLINK prefix, e.g. `output/genotype_by_chrom/protocol_example.genotype.merged.plink_qc.genotype_by_chrom_files.txt`. Only chromosomes present in both lists are analyzed, which is chr22 alone in the toy data.\n",
+ "* `--covariate-file` -- the covariate matrix, known covariates plus hidden factors, e.g. `output/covariate/protocol_example.rnaseq.bed.protocol_example.covariates.protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca.Marchenko_PC.gz`.\n",
+ "* `--name` -- prefix for every output file, e.g. `protocol_example`.\n",
+ "\n",
+ "Scope of the test:\n",
+ "\n",
+ "* `--window` -- cis radius in bp around each phenotype, default `1000000`.\n",
+ "* `--customized-cis-windows` -- four-column bed (chr, start, end, region ID) giving an explicit window per region; supplying it forces `--window` to zero.\n",
+ "* `--region-list` and `--region-list-phenotype-column` -- restrict the analysis to the regions named in a file, reading names from the given column (default `4`).\n",
+ "* `--trans-geno-chromosome` -- for `trans`, the single genotype chromosome to load; without it the step uses the chromosome that came in with the phenotype.\n",
+ "* `--chromosome` -- restrict to particular phenotype chromosomes.\n",
+ "* `--phenotype-group` -- group file mapping molecular traits into trait objects, for cases such as sQTL where one region holds several events.\n",
+ "* `--keep-sample` -- list of samples to retain.\n",
+ "* `--covariate-pattern` -- keep only covariates whose names match these prefixes or exact names.\n",
+ "* `--interaction` -- a file holding an interaction term when it is not already a covariate column; the name of its first column becomes the interaction name and is written into the output file names.\n",
+ "\n",
+ "Thresholds:\n",
+ "\n",
+ "* `--MAC` -- minor-allele-count floor, default `0`. `--MAC 5` is used below because the toy cohort is small.\n",
+ "* `--maf-threshold` -- minor-allele-frequency floor; overrides `--MAC` when set.\n",
+ "* `--permutation` / `--no-permutation` -- region-level permutation testing, on by default for `cis` and off for `trans`.\n",
+ "* `--pval`, `--pval-threshold`, `--pvalue-cutoff`, `--qvalue-cutoff` -- nominal and significance filters for `trans`.\n",
+ "* `--skip-nominal-if-exist` -- reuse existing nominal `parquet` files instead of recomputing them.\n",
+ "\n",
+ "Runtime:\n",
+ "\n",
+ "* `--cwd` -- output directory, default `output`.\n",
+ "* `--numThreads`, `--batch-size`, `--job-size`, `--walltime`, `--mem` -- threads, variants per tensor batch, and cluster resources.\n",
+ "* `--container`, `--entrypoint` -- software environment.\n",
+ "* `--modular-script-dir` -- location of the Python driver, default `code/script`."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### Example phenotype file"
]
},
{
@@ -198,12 +235,12 @@
"\t
\n",
"\n",
"\n"
],
@@ -213,12 +250,12 @@
" \\#id & \\#path\\\\\n",
" & \\\\\n",
"\\hline\n",
- "\t 2 & /restricted/projectnb/xqtl/jaempawi/xqtl\\_protocol/toy\\_example/output/genotype\\_by\\_chrom/protocol\\_example.genotype.merged.plink\\_qc.2.bed \\\\\n",
- "\t 6 & /restricted/projectnb/xqtl/jaempawi/xqtl\\_protocol/toy\\_example/output/genotype\\_by\\_chrom/protocol\\_example.genotype.merged.plink\\_qc.6.bed \\\\\n",
- "\t 13 & /restricted/projectnb/xqtl/jaempawi/xqtl\\_protocol/toy\\_example/output/genotype\\_by\\_chrom/protocol\\_example.genotype.merged.plink\\_qc.13.bed\\\\\n",
- "\t 8 & /restricted/projectnb/xqtl/jaempawi/xqtl\\_protocol/toy\\_example/output/genotype\\_by\\_chrom/protocol\\_example.genotype.merged.plink\\_qc.8.bed \\\\\n",
- "\t 7 & /restricted/projectnb/xqtl/jaempawi/xqtl\\_protocol/toy\\_example/output/genotype\\_by\\_chrom/protocol\\_example.genotype.merged.plink\\_qc.7.bed \\\\\n",
- "\t 11 & /restricted/projectnb/xqtl/jaempawi/xqtl\\_protocol/toy\\_example/output/genotype\\_by\\_chrom/protocol\\_example.genotype.merged.plink\\_qc.11.bed\\\\\n",
+ "\t 2 & /xqtl\\_protocol/toy\\_example/output/genotype\\_by\\_chrom/protocol\\_example.genotype.merged.plink\\_qc.2.bed \\\\\n",
+ "\t 6 & /xqtl\\_protocol/toy\\_example/output/genotype\\_by\\_chrom/protocol\\_example.genotype.merged.plink\\_qc.6.bed \\\\\n",
+ "\t 13 & /xqtl\\_protocol/toy\\_example/output/genotype\\_by\\_chrom/protocol\\_example.genotype.merged.plink\\_qc.13.bed\\\\\n",
+ "\t 8 & /xqtl\\_protocol/toy\\_example/output/genotype\\_by\\_chrom/protocol\\_example.genotype.merged.plink\\_qc.8.bed \\\\\n",
+ "\t 7 & /xqtl\\_protocol/toy\\_example/output/genotype\\_by\\_chrom/protocol\\_example.genotype.merged.plink\\_qc.7.bed \\\\\n",
+ "\t 11 & /xqtl\\_protocol/toy\\_example/output/genotype\\_by\\_chrom/protocol\\_example.genotype.merged.plink\\_qc.11.bed\\\\\n",
"\\end{tabular}\n"
],
"text/markdown": [
@@ -227,12 +264,12 @@
"\n",
"| #id <int> | #path <chr> |\n",
"|---|---|\n",
- "| 2 | /restricted/projectnb/xqtl/jaempawi/xqtl_protocol/toy_example/output/genotype_by_chrom/protocol_example.genotype.merged.plink_qc.2.bed |\n",
- "| 6 | /restricted/projectnb/xqtl/jaempawi/xqtl_protocol/toy_example/output/genotype_by_chrom/protocol_example.genotype.merged.plink_qc.6.bed |\n",
- "| 13 | /restricted/projectnb/xqtl/jaempawi/xqtl_protocol/toy_example/output/genotype_by_chrom/protocol_example.genotype.merged.plink_qc.13.bed |\n",
- "| 8 | /restricted/projectnb/xqtl/jaempawi/xqtl_protocol/toy_example/output/genotype_by_chrom/protocol_example.genotype.merged.plink_qc.8.bed |\n",
- "| 7 | /restricted/projectnb/xqtl/jaempawi/xqtl_protocol/toy_example/output/genotype_by_chrom/protocol_example.genotype.merged.plink_qc.7.bed |\n",
- "| 11 | /restricted/projectnb/xqtl/jaempawi/xqtl_protocol/toy_example/output/genotype_by_chrom/protocol_example.genotype.merged.plink_qc.11.bed |\n",
+ "| 2 | /xqtl_protocol/toy_example/output/genotype_by_chrom/protocol_example.genotype.merged.plink_qc.2.bed |\n",
+ "| 6 | /xqtl_protocol/toy_example/output/genotype_by_chrom/protocol_example.genotype.merged.plink_qc.6.bed |\n",
+ "| 13 | /xqtl_protocol/toy_example/output/genotype_by_chrom/protocol_example.genotype.merged.plink_qc.13.bed |\n",
+ "| 8 | /xqtl_protocol/toy_example/output/genotype_by_chrom/protocol_example.genotype.merged.plink_qc.8.bed |\n",
+ "| 7 | /xqtl_protocol/toy_example/output/genotype_by_chrom/protocol_example.genotype.merged.plink_qc.7.bed |\n",
+ "| 11 | /xqtl_protocol/toy_example/output/genotype_by_chrom/protocol_example.genotype.merged.plink_qc.11.bed |\n",
"\n"
],
"text/plain": [
@@ -244,12 +281,12 @@
"5 7 \n",
"6 11 \n",
" #path \n",
- "1 /restricted/projectnb/xqtl/jaempawi/xqtl_protocol/toy_example/output/genotype_by_chrom/protocol_example.genotype.merged.plink_qc.2.bed \n",
- "2 /restricted/projectnb/xqtl/jaempawi/xqtl_protocol/toy_example/output/genotype_by_chrom/protocol_example.genotype.merged.plink_qc.6.bed \n",
- "3 /restricted/projectnb/xqtl/jaempawi/xqtl_protocol/toy_example/output/genotype_by_chrom/protocol_example.genotype.merged.plink_qc.13.bed\n",
- "4 /restricted/projectnb/xqtl/jaempawi/xqtl_protocol/toy_example/output/genotype_by_chrom/protocol_example.genotype.merged.plink_qc.8.bed \n",
- "5 /restricted/projectnb/xqtl/jaempawi/xqtl_protocol/toy_example/output/genotype_by_chrom/protocol_example.genotype.merged.plink_qc.7.bed \n",
- "6 /restricted/projectnb/xqtl/jaempawi/xqtl_protocol/toy_example/output/genotype_by_chrom/protocol_example.genotype.merged.plink_qc.11.bed"
+ "1 /xqtl_protocol/toy_example/output/genotype_by_chrom/protocol_example.genotype.merged.plink_qc.2.bed \n",
+ "2 /xqtl_protocol/toy_example/output/genotype_by_chrom/protocol_example.genotype.merged.plink_qc.6.bed \n",
+ "3 /xqtl_protocol/toy_example/output/genotype_by_chrom/protocol_example.genotype.merged.plink_qc.13.bed\n",
+ "4 /xqtl_protocol/toy_example/output/genotype_by_chrom/protocol_example.genotype.merged.plink_qc.8.bed \n",
+ "5 /xqtl_protocol/toy_example/output/genotype_by_chrom/protocol_example.genotype.merged.plink_qc.7.bed \n",
+ "6 /xqtl_protocol/toy_example/output/genotype_by_chrom/protocol_example.genotype.merged.plink_qc.11.bed"
]
},
"metadata": {},
@@ -670,111 +707,60 @@
"kernel": "SoS"
},
"source": [
- "## Output Files\n",
+ "## Output\n",
"\n",
- "**cis** (`output/tensorqtl_cis/`):\n",
+ "* `._.cis_qtl.pairs.tsv.gz` (with `.tbi`) -- nominal statistics for every variant-phenotype pair tested in cis:\n",
"\n",
- "| File | Description |\n",
- "|---|---|\n",
- "| `*.cis_qtl.pairs.tsv.gz` (+`.tbi`) | Nominal variant-phenotype association statistics |\n",
- "| `*.cis_qtl_regional_significance.tsv.gz` | Region (gene) level significance |\n",
- "| `*.cis_qtl_regional_significance.summary.txt` | Summary of regional results |\n",
- "| `*.cis_qtl_pairs..parquet` | Per-chromosome nominal results (parquet) |\n",
+ " ```\n",
+ " chrom\tpos\tmolecular_trait_id\tvariant_id\ttss_distance\ttes_distance\taf\tma_samples\tma_count\tpvalue\t\n",
+ " 22\t10685239\tENSG00000283047\tchr22:10685239_C_T\t-254148\t-276098\t0.059322033\t7\t7\t0.572662489372593\n",
+ " 22\t10761227\tENSG00000283047\tchr22:10761227_G_A\t-178160\t-200110\t0.15254237\t18\t18\t0.75215622793613\n",
+ " ```\n",
"\n",
- "**trans** (`output/tensorqtl_trans/`):\n",
+ "* `..cis_qtl_pairs..parquet` -- the same nominal results in the columnar form TensorQTL writes natively, kept so that a rerun with `--skip-nominal-if-exist` need not recompute them.\n",
+ "* `._.cis_qtl_regional_significance.tsv.gz` (with `.tbi`) -- one row per region carrying the top variant, the fitted beta parameters, permutation and beta-approximation p-values, q-values, FDR, and the per-region genomic inflation factor:\n",
"\n",
- "| File | Description |\n",
- "|---|---|\n",
- "| `*_geno_.trans_qtl.pairs.tsv.gz` (+`.tbi`) | Trans variant-phenotype association statistics |"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "## Steps\n",
+ " ```\n",
+ " chrom\tpos\tn_variants\tbeta_shape1\tbeta_shape2\ttrue_df\tp_true_df\tvariant_id\ttss_distance\ttes_dista\n",
+ " 22\t11797697\t6\t0.9833031\t5.6122484\t30.858135\t0.0676042590649028\tchr22:11797697_C_T\t858310\t836360\t\n",
+ " ```\n",
"\n",
- "The commands below run on the included toy data and write results under `--cwd`. The genotype, phenotype, and covariate inputs are produced by the preprocessing notebooks."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "### cis-QTL association\n",
+ "* `._.cis_qtl_regional_significance.summary.txt` -- how many traits were tested and how many pass each threshold, written by the collecting step:\n",
"\n",
- "Test each molecular phenotype against variants within the cis window. Matching chromosomes between the genotype and phenotype lists are analyzed (chr22 in the toy data). `--MAC 5` sets the minor-allele-count cutoff for the small toy sample."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "kernel": "Bash"
- },
- "outputs": [],
- "source": [
- "sos run pipeline/TensorQTL.ipynb cis \\\n",
- " --genotype-file output/genotype_by_chrom/protocol_example.genotype.merged.plink_qc.genotype_by_chrom_files.txt \\\n",
- " --phenotype-file output/phenotype/phenotype_by_chrom_for_cis/bulk_rnaseq.phenotype_by_chrom_files.txt \\\n",
- " --covariate-file output/covariate/protocol_example.rnaseq.bed.protocol_example.covariates.protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca.Marchenko_PC.gz \\\n",
- " --cwd output/tensorqtl_cis --name protocol_example --MAC 5 --numThreads 2"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "### trans-QTL association\n",
+ " ```\n",
+ " TensorQTL cis-QTL regional significance summary\n",
+ " Input files: 1\n",
+ " Chromosomes: 1\n",
+ " Molecular traits tested: 200\n",
+ " Molecular trait objects: 200\n",
+ " \n",
+ " ```\n",
"\n",
- "For trans-analysis:\n",
+ "* `.<...>.cis_qtl.pairs.tsv.gz.cis_regional..fdr.gz` -- the region-level table repeated under each multiple-testing correction: `permutation_adjusted`, and Bonferroni with Benjamini-Hochberg over the original and the filtered variant sets.\n",
+ "* `.<...>.maf__window__cis_n_variants_stats.tsv.gz` -- variants tested per region at the MAF and window actually applied:\n",
"\n",
- " **Computational strategy designed for trans analysis:**\n",
- " \n",
- " Trans analysis faces significant memory challenges as we calculate all associations between all molecular traits × all genetic variants across the genome, creating a massive computational burden. To address this challenge, we implement a two-stage chromosome-based parallelization approach:\n",
+ " ```\n",
+ " chrom\tmolecular_trait_object_id\tn_variants\tn_variants_filtered\n",
+ " chr22\tENSG00000063515\t44\t44\n",
+ " chr22\tENSG00000075234\t111\t111\n",
+ " ```\n",
"\n",
- " **Stage 1 (trans_1): Chromosome-based parallelization**\n",
- " - Phenotype data is processed per chromosome (e.g., 22 separate jobs for autosomes)\n",
- " - For each phenotype chromosome, we test associations against variants from all 22 chromosomes\n",
- " - This creates phenotype_chr × genotype_chr combinations (e.g., phenotype chr1 vs genotype chr1-22); Garbage was collected between each chromosome combination caculation to release memory\n",
- " - Results are combined across all chromosome combinations and saved as compressed files\n",
+ "* `._geno_chr.trans_qtl.pairs.tsv.gz` (with `.tbi`) -- trans statistics for the loaded genotype chromosome, with the phenotype and genotype chromosome recorded on each row; the file name gains `_p_` when `--pval` is set:\n",
"\n",
- " **Stage 2 (trans_2): Significance filtering**\n",
- " - Supports p-value cutoffs (`--pvalue-cutoff`) or q-value cutoffs (`--qvalue-cutoff`)\n",
+ " ```\n",
+ " chrom\tpos\tvariant_id\tmolecular_trait_id\tpvalue\tbhat\tsebhat\tr2\taf\tn\tpheno_chrom\tgeno_chrom\tqvalue\n",
+ " 22\t10685239\tchr22:10685239_C_T\tENSG00000063515\t0.202382946055137\t-3.4470642\t2.6448517\t0.05358654\n",
+ " ```\n",
"\n",
+ "* `._geno_chr.trans_qtl.genomic_inflation.tsv.gz` -- the genomic inflation factor of the trans scan for each molecular trait:\n",
"\n",
- "\n"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "Test phenotypes against variants on a chosen genotype chromosome (`--trans-geno-chromosome 22`), restricted to the genes in `--region-list`. Trans analysis is memory-heavy genome-wide; here it is scoped to the toy chromosome."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "kernel": "Bash"
- },
- "outputs": [],
- "source": [
- "sos run pipeline/TensorQTL.ipynb trans \\\n",
- " --genotype-file output/genotype_by_chrom/protocol_example.genotype.merged.plink_qc.genotype_by_chrom_files.txt \\\n",
- " --phenotype-file output/phenotype/phenotype_by_chrom_for_cis/bulk_rnaseq.phenotype_by_chrom_files.txt \\\n",
- " --covariate-file output/covariate/protocol_example.rnaseq.bed.protocol_example.covariates.protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca.Marchenko_PC.gz \\\n",
- " --cwd output/tensorqtl_trans --name protocol_example --MAC 5 --numThreads 2 \\\n",
- " --trans-geno-chromosome 22 --region-list data/combined_AD_genes.csv --region-list-phenotype-column 4"
+ " ```\n",
+ " molecular_trait_id\tgenomic_inflation_lambda\n",
+ " ENSG00000063515\t0.9919021540170271\n",
+ " ENSG00000075234\t1.1570695153778894\n",
+ " ```\n",
+ "\n",
+ "Every step writes `.stdout` and `.stderr` beside its output. Example results for the toy data are under `output/tensorqtl_cis/` and `output/tensorqtl_trans/`."
]
},
{
@@ -783,7 +769,7 @@
"kernel": "SoS"
},
"source": [
- "## Output \n",
+ "### Result tables \n",
"\n",
"For each chromosome, several summary statistics files are generated, including both nominal test statistics for each test and region (gene) level association evidence. \n",
"\n",
@@ -811,7 +797,7 @@
"\n",
"### Interaction Association Results \n",
"\n",
- "The columns of interaction association results are as follows (FIXME): \n",
+ "Interaction results are produced when `--interaction` is set. The column names carry the name of the interaction variable, shown here for `msex`: \n",
"\n",
"**Model:** \n",
"$$\n",
@@ -828,17 +814,17 @@
"- **a1**: Variant alternate allele. \n",
"- **molecular_trait_id**: Molecular trait identifier, varies from phenotypes to phenotypes. \n",
"- **variant_id**: ID of the top variant (rsid or chr:position:ref:alt). \n",
- "- **af**: Alternative allele frequency in the MiGA cohort. \n",
+ "- **af**: Alternative allele frequency in the cohort analyzed. \n",
"- **ma_samples**: Number of samples carrying the minor allele. \n",
"- **ma_count**: Total number of minor alleles across individuals. \n",
- "- **pvalue**: P-value of the main effect from the nonlinear regression. \n",
- "- **bhat**: Slope of the main effect from the nonlinear regression. \n",
+ "- **pvalue**: P-value of the main effect from the interaction model. \n",
+ "- **bhat**: Slope of the main effect from the interaction model. \n",
"- **se**: Standard error of beta. \n",
- "- **pvalue_msex**: P-value of the msex term from the nonlinear regression. \n",
- "- **bhat_msex**: Slope of the msex term from the nonlinear regression. \n",
+ "- **pvalue_msex**: P-value of the msex term from the interaction model. \n",
+ "- **bhat_msex**: Slope of the msex term from the interaction model. \n",
"- **se_msex**: Standard error of bhat_msex. \n",
- "- **pvalue_msex_interaction**: P-value of the interaction term from the nonlinear regression. \n",
- "- **bhat_msex_interaction**: Slope of the interaction term from the nonlinear regression. \n",
+ "- **pvalue_msex_interaction**: P-value of the interaction term from the interaction model. \n",
+ "- **bhat_msex_interaction**: Slope of the interaction term from the interaction model. \n",
"- **se_msex_interaction**: Standard error of beta_msex_interaction. \n",
"- **molecular_trait_object_id**: An intermediate ID (can be ignored). \n",
"- **n**: Number of samples.\n",
@@ -885,9 +871,89 @@
"kernel": "SoS"
},
"source": [
- "### Command interface\n",
+ "## Minimal Working Example\n",
"\n",
- "List all workflows and options:"
+ "The commands below run on the included toy data and write results under `--cwd`. The genotype, phenotype, and covariate inputs are produced by the preprocessing notebooks."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### cis-QTL association\n",
+ "\n",
+ "Test each molecular phenotype against variants within the cis window. Matching chromosomes between the genotype and phenotype lists are analyzed (chr22 in the toy data). `--MAC 5` sets the minor-allele-count cutoff for the small toy sample."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "kernel": "Bash"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/TensorQTL.ipynb cis \\\n",
+ " --genotype-file output/genotype_by_chrom/protocol_example.genotype.merged.plink_qc.genotype_by_chrom_files.txt \\\n",
+ " --phenotype-file output/phenotype/phenotype_by_chrom_for_cis/bulk_rnaseq.phenotype_by_chrom_files.txt \\\n",
+ " --covariate-file output/covariate/protocol_example.rnaseq.bed.protocol_example.covariates.protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca.Marchenko_PC.gz \\\n",
+ " --cwd output/tensorqtl_cis --name protocol_example --MAC 5 --numThreads 2"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### trans-QTL association\n",
+ "\n",
+ "Test phenotypes against variants on a chosen genotype chromosome (`--trans-geno-chromosome 22`), restricted to the genes in `--region-list`. Trans analysis is memory-heavy genome-wide; here it is scoped to the toy chromosome."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "kernel": "Bash"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/TensorQTL.ipynb trans \\\n",
+ " --genotype-file output/genotype_by_chrom/protocol_example.genotype.merged.plink_qc.genotype_by_chrom_files.txt \\\n",
+ " --phenotype-file output/phenotype/phenotype_by_chrom_for_cis/bulk_rnaseq.phenotype_by_chrom_files.txt \\\n",
+ " --covariate-file output/covariate/protocol_example.rnaseq.bed.protocol_example.covariates.protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca.Marchenko_PC.gz \\\n",
+ " --cwd output/tensorqtl_trans --name protocol_example --MAC 5 --numThreads 2 \\\n",
+ " --trans-geno-chromosome 22 --region-list data/combined_AD_genes.csv --region-list-phenotype-column 4"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "## Command Interface"
]
},
{
@@ -907,7 +973,117 @@
"kernel": "SoS"
},
"source": [
- "## Pipeline Code\n",
+ "```\n",
+ "usage: sos run pipeline/TensorQTL.ipynb\n",
+ " [workflow_name | -t targets] [options] [workflow_options]\n",
+ " workflow_name: Single or combined workflows defined in this script\n",
+ " targets: One or more targets to generate\n",
+ " options: Single-hyphen sos parameters (see \"sos run -h\" for details)\n",
+ " workflow_options: Double-hyphen workflow-specific parameters\n",
+ "\n",
+ "Workflows:\n",
+ " cis\n",
+ " trans\n",
+ "\n",
+ "Global Workflow Options:\n",
+ " --modular-script-dir code/script (as path)\n",
+ " --cwd output (as path)\n",
+ " Path to the work directory of the analysis.\n",
+ " --phenotype-file VAL (as path, required)\n",
+ " Phenotype file, or a list of phenotype per region.\n",
+ " --genotype-file VAL (as path, required)\n",
+ " A genotype file in PLINK binary format (bed/bam/fam)\n",
+ " format, or a list of genotype per chrom\n",
+ " --covariate-file VAL (as path, required)\n",
+ " Covariate file\n",
+ " --covariate-pattern (as list)\n",
+ " Optional pattern to filter covariates (list of covariate\n",
+ " prefixes or exact names)\n",
+ " --name VAL (as str, required)\n",
+ " Prefix for the analysis output\n",
+ " --region-list . (as path)\n",
+ " An optional subset of regions of molecular features to\n",
+ " analyze. The last column is the gene names\n",
+ " --region-list-phenotype-column 4 (as int)\n",
+ " --keep-sample . (as path)\n",
+ " Set list of sample to be keep\n",
+ " --interaction ''\n",
+ " FIXME: please document\n",
+ " --customized-cis-windows . (as path)\n",
+ " An optional list documenting the custom cis window for\n",
+ " each region to analyze, with four column, chr, start,\n",
+ " end, region ID (eg gene ID). If this list is not\n",
+ " provided, the default `window` parameter (see below)\n",
+ " will be used.\n",
+ " --phenotype-group . (as path)\n",
+ " The phenotype group file to group molecule_trait into\n",
+ " molecule_trait_object This applies to multiple molecular\n",
+ " events in the same region, such as sQTL analysis.\n",
+ " --chromosome (as list)\n",
+ " The name of phenotype corresponding to gene_id or\n",
+ " gene_name in the region\n",
+ " --MAC 0 (as int)\n",
+ " Minor allele count cutoff\n",
+ " --trans-geno-chromosome ''\n",
+ " Specify genotype chromosome for trans analysis (e.g.\n",
+ " --trans-geno-chromosome 5) When set, trans step loads\n",
+ " this genotype chrom instead of the one from input_files\n",
+ " --window 1000000 (as int)\n",
+ " Specify the cis window for the up and downstream radius\n",
+ " to analyze around the region of interest in units of bp\n",
+ " This parameter will be set to zero if\n",
+ " `customized_cis_windows` is provided.\n",
+ " --numThreads 8 (as int)\n",
+ " Number of threads\n",
+ " --job-size 1 (as int)\n",
+ " For cluster jobs, number commands to run per job\n",
+ " --walltime 12h\n",
+ " --mem 16G\n",
+ " --container ''\n",
+ " Container option for software to run the analysis:\n",
+ " docker or singularity\n",
+ " --entrypoint ''\n",
+ " --maf-threshold MAC/(2.0*N)\n",
+ "\n",
+ " Minor allele frequency cutoff. It will overwrite minor\n",
+ " allele cutoff. You may consider setting it to higher for\n",
+ " interaction analysis if you have statistical power\n",
+ " concerns\n",
+ " --pvalue-cutoff '5e-8'\n",
+ " Filtering significant trans associations (for trans_2\n",
+ " workflow)\n",
+ " --qvalue-cutoff ''\n",
+ "\n",
+ "Sections\n",
+ " cis_1:\n",
+ " Workflow Options:\n",
+ " --[no-]skip-nominal-if-exist (default to False)\n",
+ " parse input file lists skip nominal association results\n",
+ " if the files exists already This is false by default\n",
+ " which means to recompute everything This is only\n",
+ " relevant when the `parquet` files for nominal results\n",
+ " exist but not the other files and you want to avoid\n",
+ " computing the nominal results again\n",
+ " --[no-]permutation (default to True)\n",
+ " cis_2:\n",
+ " trans:\n",
+ " Workflow Options:\n",
+ " --batch-size 10000 (as int)\n",
+ " --pval-threshold 1.0 (as float)\n",
+ " --[no-]permutation (default to False)\n",
+ " Permutation testing is incorrect when the analysis is\n",
+ " done by chrom\n",
+ " --pval 0.0 (as float)\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "## Workflow implementation\n",
"\n",
"The SoS workflow definitions below are unchanged from the original protocol."
]
@@ -1193,17 +1369,6 @@
" Rscript ${modular_script_dir}/association_scan/TensorQTL/TensorQTL.R \\\n",
" trans_qvalues \"${_output['nominal']}\"\n"
]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "## Anticipated Results\n",
- "\n",
- "The pipeline produces output files in the `output/` subdirectory named after the workflow step. Verify success by checking that output files exist and are non-empty. See the **Output** section above for the expected file names and formats."
- ]
}
],
"metadata": {
diff --git a/code/SoS/association_scan/qtl_association_postprocessing.ipynb b/code/SoS/association_scan/qtl_association_postprocessing.ipynb
index 9766c05bc..9d7e402cf 100644
--- a/code/SoS/association_scan/qtl_association_postprocessing.ipynb
+++ b/code/SoS/association_scan/qtl_association_postprocessing.ipynb
@@ -8,7 +8,7 @@
"source": [
"# Hierarchical Multiple Testing\n",
"\n",
- "This is a runnable, toy-data example of the `qtl_association_testing` post-processing step. It performs hierarchical multiple-testing correction on the per-gene cis-QTL association results produced by TensorQTL, then archives the bulky intermediate files."
+ "Applies hierarchical multiple-testing correction to the per-gene cis-QTL results from TensorQTL and assembles the survivors into one consolidated object."
]
},
{
@@ -17,11 +17,25 @@
"kernel": "SoS"
},
"source": [
- "## Description\n",
+ "## Overview\n",
"\n",
- "Given the per-gene association output from a cis-QTL scan, this pipeline implements a three-step hierarchical procedure: (1) local adjustment of the p-values of all cis-variants within each gene, (2) global adjustment of the minimum adjusted p-value across genes, and (3) identification of significant xQTL whose locally adjusted p-value falls below the threshold. It also reorganises the intermediate TensorQTL files into an archive folder for book-keeping or deletion.\n",
+ "A cis scan reports a p-value for every variant against every molecular phenotype, which is\n",
+ "not yet a result: the variants within a gene's window are correlated and the genes are\n",
+ "many, so raw p-values overstate significance twice over.\n",
"\n",
- "The same `default` workflow handles three flavours of upstream results: the **genetic (cis) effect** (shown below), the **interaction effect** (run on `*.cis_qtl_top_assoc.txt.gz` with `--maf-cutoff 0 --cis-window 0` and interaction p/q-value patterns), and **quantile QTL**. This example demonstrates the genetic-effect flavour on toy data. (cf. Huang et al. 2018)"
+ "The correction is hierarchical, in three steps: local adjustment of the p-values of all cis\n",
+ "variants within each gene, global adjustment of the minimum adjusted p-value across genes,\n",
+ "then selection of the xQTLs whose locally adjusted p-value falls below the threshold\n",
+ "(the eigenMT-BH procedure of [Huang et al. 2018, *NAR* 46(22):e133](https://doi.org/10.1093/nar/gky780)). The survivors are packaged as a `QtlSumStats` object with a\n",
+ "regional FDR table, and the intermediate TensorQTL files are reorganised into an archive\n",
+ "folder for book-keeping or deletion.\n",
+ "\n",
+ "The same `default` workflow handles three flavours of upstream result: the genetic (cis)\n",
+ "effect shown below, the interaction effect - run on `*.cis_qtl_top_assoc.txt.gz` with\n",
+ "`--maf-cutoff 0 --cis-window 0` and the interaction p/q-value patterns - and quantile QTL.\n",
+ "\n",
+ "**When to run it.** Immediately after `TensorQTL`, before fine-mapping. Nothing downstream\n",
+ "reads the raw scan directory directly."
]
},
{
@@ -30,7 +44,44 @@
"kernel": "SoS"
},
"source": [
- "**Timing:** ~1-2 min on typical compute infrastructure."
+ "## Input\n",
+ "\n",
+ "The workflow is pointed at a directory rather than at files: it globs `--cwd` for the three TensorQTL products below using `--regional-pattern`, `--qtl-pattern` and `--n-variants-suffix`. `output/tensorqtl_cis` is one directory in that shape.\n",
+ "\n",
+ "- `--cwd`: the TensorQTL working directory holding the scan output. Three kinds of file are picked up from it.\n",
+ "\n",
+ " The per-gene regional result, matched by `--regional-pattern` (default `cis_qtl[.]regional[.]tsv[.]gz$`). Example `output/tensorqtl_cis/protocol_example.chr22_chr22.cis_qtl.regional.tsv.gz`, 21 columns and 201 rows:\n",
+ "\n",
+ " ```\n",
+ " chrom pos n_variants beta_shape1 beta_shape2 true_df p_true_df variant_id\n",
+ " 22 11797697 6 0.9833031 5.6122484 30.858135 0.0676042590649028 chr22:11797697_C_T\n",
+ " 22 15838003 46 0.98497593 26.038029 30.771364 0.007232871450238482 chr22:15838003_CTTTC...\n",
+ " ```\n",
+ "\n",
+ " The full variant-level pairs file, matched by `--qtl-pattern` (default `cis_qtl[.]pairs[.]tsv[.]gz$`). Example `output/tensorqtl_cis/protocol_example.chr22_chr22.cis_qtl.pairs.tsv.gz`, 15 columns and 18319 rows:\n",
+ "\n",
+ " ```\n",
+ " chrom pos molecular_trait_id variant_id tss_distance tes_distance af pvalue\n",
+ " 22 10685239 ENSG00000283047 chr22:10685239_C_T -254148 -276098 0.059322033 0.572662489372593\n",
+ " 22 10761227 ENSG00000283047 chr22:10761227_G_A -178160 -200110 0.15254237 0.752156227936135\n",
+ " ```\n",
+ "\n",
+ " The recounted variant tallies, matched by `--n-variants-suffix` (default `cis_n_variants_stats[.]tsv[.]gz$`). Example `output/tensorqtl_cis/protocol_example.chr22_chr22.maf_0.01_window_1000000_cis_n_variants_stats.tsv.gz`, 4 columns and 201 rows:\n",
+ "\n",
+ " ```\n",
+ " chrom molecular_trait_object_id n_variants n_variants_filtered\n",
+ " chr22 ENSG00000063515 44 44\n",
+ " chr22 ENSG00000075234 111 111\n",
+ " ```\n",
+ "\n",
+ "- `--output-dir`: where the consolidated object and the regional FDR table are written. Required.\n",
+ "- `--modular-script-dir`: the `code/script` directory holding the pecotmr_integration wrappers. Required.\n",
+ "- `--maf-cutoff` and `--cis-window`: `0.01` and `1000000` by default. When either is non-zero the variant counts are recomputed from the full pairs file before Bonferroni correction, which matters because a correction taken over too wide a window, or over many low-frequency variants, overstates the number of independent tests. Set both to `0` to skip the recount and use the counts as given.\n",
+ "- `--pvalue-cutoff`: `0.05` by default. Pairs above it are dropped before assembly to keep the object small; the per-gene minimum variant is always retained so the Bonferroni minimum stays exact.\n",
+ "- `--fdr-threshold`: `0.05` by default, the threshold for the global significance call.\n",
+ "- `--study`, `--context`, `--genome`: labels stamped onto the assembled `QtlSumStats` object, `study` / `context` / `hg38` by default.\n",
+ "- `--af-col`, `--pvalue-col`, `--molecular-id-col`: the column names to read, `af` / `pvalue` / `molecular_trait_object_id` by default.\n",
+ "- `--[no-]enable-archive`: off by default. When on, the bulky TensorQTL intermediates are moved out of the working directory for book-keeping or deletion."
]
},
{
@@ -39,13 +90,14 @@
"kernel": "SoS"
},
"source": [
- "## Input Files\n",
+ "## Output\n",
+ "\n",
+ "- `{output-dir}/{cwd basename}.qtl_association_postprocessing.rds` - the consolidated `QtlSumStats` object holding the surviving xQTLs together with the `--study`, `--context` and `--genome` labels. This is the only file named in the workflow's `output:` statement.\n",
+ "- `{output-dir}/{cwd basename}.qtl_association_postprocessing.cis_regional.fdr.tsv.gz` - the per-gene regional FDR table.\n",
+ "\n",
+ "Both are named from the basename of `--cwd`, so the example below writes `tensorqtl_cis.qtl_association_postprocessing.rds` and `tensorqtl_cis.qtl_association_postprocessing.cis_regional.fdr.tsv.gz` into `output/qtlpp`.\n",
"\n",
- "| Role | Toy file | Produced by |\n",
- "|------|----------|-------------|\n",
- "| Per-gene cis association | `output/tensorqtl_cis/*.cis_qtl.regional.tsv.gz` | TensorQTL cis scan |\n",
- "| Full variant-level pairs | `output/tensorqtl_cis/*.cis_qtl.pairs.tsv.gz` | TensorQTL cis scan |\n",
- "| Gene coordinates | `input/reference_data/look_up_gene_id.tsv` (`chr`, `start`, `end`, `gene_id`) | reference |"
+ "No example output ships for this step. The `*.cis_regional.*.fdr.gz` and `*_regional_significance.*` files sitting alongside the inputs in `output/tensorqtl_cis` were written by an earlier version of this interface and carry names the workflow no longer produces."
]
},
{
@@ -54,19 +106,9 @@
"kernel": "SoS"
},
"source": [
- "## Output Files\n",
+ "## Minimal Working Example\n",
"\n",
- "Written under `--output-dir` (significance results) and `--archive-dir` (moved intermediates):\n",
- "\n",
- "| File | Meaning |\n",
- "|------|---------|\n",
- "| `*_multiple_testing_consolidated.rds` | Consolidated multiple-testing results object |\n",
- "| `*.cis_regional.summary.txt` | Per-gene regional summary after correction |\n",
- "| `*significant_qtl*.tsv.gz` | Significant variant-level QTL (Bonferroni / BH / permutation) |\n",
- "| `*significant_events*.tsv.gz` | Significant gene-level events |\n",
- "| `archive/.../*.parquet` | Bulky TensorQTL intermediates moved out of the working dir |\n",
- "\n",
- "On the 6-sample toy data the correction runs cleanly and produces the consolidated object and summary; no events pass the genome-wide significance threshold, which is expected at this tiny scale."
+ "The workflow globs `--cwd` for its inputs rather than taking file arguments, so it is pointed straight at the TensorQTL scan directory."
]
},
{
@@ -75,9 +117,7 @@
"kernel": "SoS"
},
"source": [
- "## Steps\n",
- "\n",
- "Run the hierarchical correction on the toy cis results. The MAF / cis-window options are non-zero, so the program recomputes the variant counts from the full pairs file before applying the Bonferroni correction. On this toy data the distance columns in the TensorQTL output are named `tss_distance` and `tes_distance`, so we point `--tss-dist-col` / `--tes-dist-col` at them."
+ "**Timing**: TBD (on toy dataset)"
]
},
{
@@ -90,15 +130,10 @@
"source": [
"sos run pipeline/qtl_association_postprocessing.ipynb default \\\n",
" --cwd output/tensorqtl_cis \\\n",
- " --gene-coordinates input/reference_data/look_up_gene_id.tsv \\\n",
- " --sub-dir . \\\n",
- " --tss-dist-col tss_distance --tes-dist-col tes_distance \\\n",
- " --maf-cutoff 0.01 --cis-window 1000000 \\\n",
- " --regional-pattern \"*.cis_qtl.regional.tsv.gz$\" \\\n",
- " --output-dir output/hierarchical_multi_test/output \\\n",
- " --archive-dir output/hierarchical_multi_test/archive --enable-archive True \\\n",
- " --pecotmr-path ../pecotmr \\\n",
- " -s force"
+ " --output-dir output/qtlpp \\\n",
+ " --modular-script-dir code/script \\\n",
+ " --maf-cutoff 0.01 --cis-window 1000000 --pvalue-cutoff 0.05 \\\n",
+ " --study protocol_example --context bulk_rnaseq --genome hg38"
]
},
{
@@ -107,16 +142,14 @@
"kernel": "SoS"
},
"source": [
- "## Command interface\n",
- "\n",
- "Show all workflow options and their defaults:"
+ "## Command Interface"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
- "kernel": "Bash"
+ "kernel": "SoS"
},
"outputs": [],
"source": [
@@ -129,9 +162,48 @@
"kernel": "SoS"
},
"source": [
- "## Workflow implementation\n",
+ "```\n",
+ "usage: sos run pipeline/qtl_association_postprocessing.ipynb\n",
+ " [workflow_name | -t targets] [options] [workflow_options]\n",
+ " workflow_name: Single or combined workflows defined in this script\n",
+ " targets: One or more targets to generate\n",
+ " options: Single-hyphen sos parameters (see \"sos run -h\" for details)\n",
+ " workflow_options: Double-hyphen workflow-specific parameters\n",
"\n",
- "The cells below are the unmodified SoS workflow definition that the example above calls."
+ "Workflows:\n",
+ " default\n",
+ "\n",
+ "Global Workflow Options:\n",
+ " --cwd . (as path)\n",
+ " --modular-script-dir VAL (as path, required)\n",
+ " code/script dir that holds the pecotmr_integration\n",
+ " wrappers\n",
+ " --output-dir VAL (as path, required)\n",
+ " --sub-dir . (as path)\n",
+ " --study study\n",
+ " labels for the assembled QtlSumStats\n",
+ " --context context\n",
+ " --genome hg38\n",
+ " --maf-cutoff 0.01 (as float)\n",
+ " --cis-window 1000000 (as int)\n",
+ " --af-col af\n",
+ " --pvalue-col pvalue\n",
+ " --molecular-id-col 'molecular_trait_object_id'\n",
+ " --pvalue-cutoff 0.05 (as float)\n",
+ " drop pairs with p above this before assembling (keeps\n",
+ " the object small; the per-gene min variant is always\n",
+ " retained so the Bonferroni min is exact)\n",
+ " --fdr-threshold 0.05 (as float)\n",
+ " --regional-pattern 'cis_qtl[.]regional[.]tsv[.]gz$'\n",
+ " list.files() regex patterns to locate the inputs in the\n",
+ " work dir\n",
+ " --qtl-pattern 'cis_qtl[.]pairs[.]tsv[.]gz$'\n",
+ " --n-variants-suffix 'cis_n_variants_stats[.]tsv[.]gz$'\n",
+ " --[no-]enable-archive (default to False)\n",
+ "\n",
+ "Sections\n",
+ " default:\n",
+ "```"
]
},
{
@@ -140,9 +212,9 @@
"kernel": "SoS"
},
"source": [
- "## Anticipated Results\n",
+ "## Workflow implementation\n",
"\n",
- "The pipeline produces output files in the `output/` subdirectory named after the workflow step. Verify success by checking that output files exist and are non-empty. See the **Output** section above for the expected file names and formats."
+ "The cells below are the unmodified SoS workflow definition that the example above calls."
]
},
{
diff --git a/code/SoS/association_scan/qtl_association_testing.ipynb b/code/SoS/association_scan/qtl_association_testing.ipynb
index 6f5cbed56..79f26c004 100644
--- a/code/SoS/association_scan/qtl_association_testing.ipynb
+++ b/code/SoS/association_scan/qtl_association_testing.ipynb
@@ -2,122 +2,63 @@
"cells": [
{
"cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "# QTL Association Testing \n",
- "\n",
- "This is an **introductory, run-through notebook** for the QTL association testing section of the xQTL protocol. If you are new to the protocol, start here: it explains what this section does, what inputs it needs, and walks you through each analysis on the included toy dataset so you can reproduce results end to end before applying the pipeline to your own data.\n",
- "\n",
- "The heavy lifting is done by the worker notebook [`TensorQTL.ipynb`](TensorQTL/TensorQTL.ipynb). This section notebook simply orchestrates it for the three common analyses."
- ]
- },
- {
- "cell_type": "markdown",
+ "id": "4f5d029f",
"metadata": {},
"source": [
- "## Prerequisites\n",
+ "# QTL association testing\n",
"\n",
- "Requires phenotype matrices from molecular phenotype steps and covariates from `covariate_preprocessing`."
+ "This mini-protocol selects and runs cis-, trans-, or interaction-QTL association testing with the TensorQTL module."
]
},
{
"cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "## Description\n",
- "\n",
- "QTL association testing measures the statistical association between genetic variants and molecular phenotypes (e.g. gene expression). This section runs three flavours of that test, all through TensorQTL:\n",
- "\n",
- "1. **cis** \u2014 each phenotype against nearby variants (within a window around the gene). This is the most common QTL scan.\n",
- "2. **trans** \u2014 phenotypes against variants elsewhere in the genome (here, a chosen chromosome), optionally limited to a set of genes of interest.\n",
- "3. **interaction** \u2014 a cis scan that additionally tests whether the genotype effect is modified by a covariate (e.g. sex).\n",
- "\n",
- "Each analysis is an independent call to the same worker; you can run only the ones you need."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
+ "id": "6a1baa78",
+ "metadata": {},
"source": [
- "## How it fits in the pipeline\n",
- "\n",
- "This section sits **after** the data-preprocessing sections. Its inputs are produced by:\n",
+ "#### Miniprotocol Timing\n",
"\n",
- "- **genotype_preprocessing** \u2192 by-chromosome PLINK genotypes and the file list,\n",
- "- **phenotype_preprocessing** \u2192 by-chromosome molecular phenotype `bed.gz` files and the file list,\n",
- "- **covariate_preprocessing** \u2192 the covariate matrix (known covariates + hidden factors).\n",
- "\n",
- "The outputs of this section (cis/trans/interaction summary statistics) feed the downstream **association postprocessing** and fine-mapping / integration sections."
+ "Timing: TBD"
]
},
{
"cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
+ "id": "fc058d62",
+ "metadata": {},
"source": [
- "## Input\n",
+ "## Overview\n",
"\n",
- "Make sure the preprocessing sections have been run so the following toy inputs exist. The commands below assume you launch them from the toy-example root directory.\n",
+ "QTL association testing identifies genetic variants associated with molecular traits such as gene expression. This mini-protocol calls [`TensorQTL.ipynb`](TensorQTL/TensorQTL.ipynb) for three independent analyses: a cis scan of variants near each molecular trait, a trans scan of variants outside the local region, and an interaction scan testing whether a covariate modifies the genotype effect.\n",
"\n",
- "| Role | File | Produced by |\n",
- "|---|---|---|\n",
- "| Genotypes (by chrom) | `output/genotype_by_chrom/protocol_example.genotype.merged.plink_qc.genotype_by_chrom_files.txt` | genotype_preprocessing |\n",
- "| Phenotypes (by chrom) | `output/phenotype/phenotype_by_chrom_for_cis/bulk_rnaseq.phenotype_by_chrom_files.txt` | phenotype_preprocessing |\n",
- "| Covariates | `output/covariate/...prune.pca.Marchenko_PC.gz` | covariate_preprocessing |\n",
- "| Genes of interest (trans) | `data/combined_AD_genes.csv` | provided with the toy data |\n",
- "| Cis-window definitions (optional) | `reference_data/TAD/TADB_enhanced_cis.bed` | provided reference data |\n",
+ "Run the genotype-, phenotype-, and covariate-preprocessing mini-protocols first. Their outputs provide chromosome-indexed PLINK genotypes, chromosome-indexed molecular phenotypes, and a sample-aligned covariate matrix. The association results can then be passed to association postprocessing, fine-mapping, or multi-omics integration.\n",
"\n",
- "You can confirm they are present by listing them:"
+ "Choose one route below; the three commands are alternatives rather than one mandatory chain."
]
},
{
"cell_type": "markdown",
+ "id": "8c638946",
"metadata": {},
- "source": [
- "**Timing:** Runtime varies by dataset size and compute resources. For the toy chr22 MWE dataset, most steps complete in under 10 minutes on a standard HPC node."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "kernel": "Bash"
- },
- "outputs": [],
- "source": [
- "ls -la output/genotype_by_chrom/protocol_example.genotype.merged.plink_qc.genotype_by_chrom_files.txt \\\n",
- " output/phenotype/phenotype_by_chrom_for_cis/bulk_rnaseq.phenotype_by_chrom_files.txt \\\n",
- " output/covariate/*Marchenko_PC.gz \\\n",
- " data/combined_AD_genes.csv reference_data/TAD/TADB_enhanced_cis.bed"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
"source": [
"## Steps\n",
+ "Choose one route; cis-, trans-, and interaction-QTL scans answer different questions and are not a mandatory chain.\n",
"\n",
- "Each analysis below is a single command. They are independent \u2014 read the short explanation, then run the cell. On the toy data each takes roughly 1\u20132 minutes."
+ "| Analysis goal | Command to run | Inputs |\n",
+ "|---|---:|---|\n",
+ "| Test local genetic effects within each phenotype's cis window | 1 | `output/genotype_by_chrom/protocol_example.genotype.merged.plink_qc.genotype_by_chrom_files.txt`; `output/phenotype/phenotype_by_chrom_for_cis/bulk_rnaseq.phenotype_by_chrom_files.txt`; `output/covariate/protocol_example.rnaseq.bed.protocol_example.covariates.protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca.Marchenko_PC.gz` |\n",
+ "| Test distal effects across chromosomes | 2 | The same genotype, phenotype, and covariate files, plus `data/combined_AD_genes.csv` as the example phenotype-region list |\n",
+ "| Test whether a covariate modifies a cis-QTL effect | 3 | The same genotype, phenotype, and covariate files, plus the interaction variable `msex` in the covariate data |\n",
+ "\n",
+ "Run only the command matching the scientific question."
]
},
{
"cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
+ "id": "cd4853d9",
+ "metadata": {},
"source": [
- "### 1. cis-QTL scan\n",
+ "### 1. [cis-QTL scan](TensorQTL/TensorQTL.ipynb)\n",
"\n",
- "This is the standard scan: every molecular phenotype is tested against the variants within its cis window (1 Mb around the TSS by default). Genotype and phenotype lists are matched by chromosome (the toy data covers chr22). `--MAC 5` is a relaxed minor-allele-count cutoff appropriate for the small toy sample. Results land in `output/tensorqtl_cis/`."
+ "**What it does:** Tests each molecular trait against variants within its cis window, using `--MAC 5` for the small chromosome 22 example."
]
},
{
@@ -137,13 +78,12 @@
},
{
"cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
+ "id": "21598aa2",
+ "metadata": {},
"source": [
- "### 2. trans-QTL scan\n",
+ "### 2. [trans-QTL scan](TensorQTL/TensorQTL.ipynb)\n",
"\n",
- "A trans scan tests phenotypes against variants on a chosen genotype chromosome (`--trans-geno-chromosome 22`). Because genome-wide trans testing is expensive, here we restrict the phenotypes to the genes listed in `--region-list` (gene name in column 4). Results land in `output/tensorqtl_trans/`."
+ "**What it does:** Tests the selected traits against variants on chromosome 22, restricting traits to the identifiers listed in `data/combined_AD_genes.csv`."
]
},
{
@@ -164,13 +104,12 @@
},
{
"cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
+ "id": "b6f552ec",
+ "metadata": {},
"source": [
- "### 3. interaction (iQTL) scan\n",
+ "### 3. [interaction-QTL scan](TensorQTL/TensorQTL.ipynb)\n",
"\n",
- "An interaction scan is a cis scan that also tests whether the variant effect depends on a covariate. Here we use `--interaction msex` (the sex column in the toy covariate file; in the full protocol this column is named `sex`). `--no-permutation` skips the permutation step (permutation is not meaningful for a single interaction test on toy data), and `--maf-threshold 0.05` sets the MAF cutoff. Results land in `output/tensorqtl_int/`."
+ "**What it does:** Runs the cis model with a genotype-by-`msex` interaction term and reports evidence that the genotype effect changes with this covariate."
]
},
{
@@ -191,41 +130,42 @@
},
{
"cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
+ "id": "a3d087be",
+ "metadata": {},
"source": [
- "## Output\n",
- "\n",
- "After running the cells above you will have three result folders:\n",
+ "## Output Files\n",
"\n",
- "| Folder | Key files | Meaning |\n",
+ "| Route | Relative path | Contents |\n",
"|---|---|---|\n",
- "| `output/tensorqtl_cis/` | `*.cis_qtl.pairs.tsv.gz`, `*.cis_qtl_regional_significance.tsv.gz` (+summary), `*.parquet` | cis nominal + region-level statistics |\n",
- "| `output/tensorqtl_trans/` | `*_geno_.trans_qtl.pairs.tsv.gz` | trans nominal statistics |\n",
- "| `output/tensorqtl_int/` | `*_.cis_qtl.pairs.tsv.gz`, `*.parquet` | interaction (iQTL) nominal statistics |\n",
+ "| cis-QTL | `output/tensorqtl_cis/protocol_example._.cis_qtl.pairs.tsv.gz` | Nominal statistics for tested cis variant-trait pairs |\n",
+ "| cis-QTL | `output/tensorqtl_cis/protocol_example._.cis_qtl_regional_significance.tsv.gz` | Region-level permutation and multiple-testing results |\n",
+ "| cis-QTL | `output/tensorqtl_cis/protocol_example..cis_qtl_pairs..parquet` | Native TensorQTL nominal results retained for reuse |\n",
+ "| trans-QTL | `output/tensorqtl_trans/protocol_example._geno_chr22.trans_qtl.pairs.tsv.gz` | Trans-association statistics for chromosome 22 |\n",
+ "| interaction-QTL | `output/tensorqtl_int/protocol_example.__msex.cis_qtl.pairs.tsv.gz` | Nominal genotype, interaction, and genotype-by-interaction effects |\n",
"\n",
- "The nominal `pairs` files contain one row per variant\u2013phenotype pair with effect size, standard error and p-value; the regional-significance files summarise the strongest signal per gene."
+ "Tabix index files (`.tbi`) accompany the bgzipped result tables. Exact chromosome tokens follow the input file lists."
]
},
{
"cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
+ "id": "38c65345",
+ "metadata": {},
"source": [
- "## Next steps\n",
+ "## Anticipated Results\n",
"\n",
- "Proceed to **association postprocessing** to filter, calibrate and summarise these statistics, and then to the fine-mapping / multi-omics integration sections. For full parameter documentation of any analysis, open the worker notebook directly:"
+ "The selected route produces association statistics for the chromosome 22 example. Cis analysis returns nominal variant-trait results plus region-level significance summaries; trans analysis returns the restricted cross-region tests; interaction analysis adds main and genotype-by-covariate effect estimates.\n",
+ "\n",
+ "Proceed to association postprocessing when calibrated regional summaries are needed, or use the nominal and region-level tables as input to the appropriate fine-mapping or integration workflow."
]
},
{
"cell_type": "markdown",
+ "id": "2073dd44",
"metadata": {},
"source": [
- "## Anticipated Results\n",
+ "## Command interface\n",
"\n",
- "Produces `.cis_qtl_pairs.chr*.parquet` files with nominal p-values and `.cis_qtl.txt.gz` with significant results. For the toy chr22 dataset expect hundreds of nominal associations."
+ "Inspect all TensorQTL workflows, options, and defaults:"
]
},
{
@@ -266,5 +206,5 @@
}
},
"nbformat": 4,
- "nbformat_minor": 4
+ "nbformat_minor": 5
}
\ No newline at end of file
diff --git a/code/SoS/association_scan/quantile_models/qr_and_twas.ipynb b/code/SoS/association_scan/quantile_models/qr_and_twas.ipynb
index 089ff788f..c9cccd6d8 100644
--- a/code/SoS/association_scan/quantile_models/qr_and_twas.ipynb
+++ b/code/SoS/association_scan/quantile_models/qr_and_twas.ipynb
@@ -8,13 +8,7 @@
"source": [
"# Quantile regression for QTL association testing\n",
"\n",
- "## Description\n",
- "\n",
- "This module performs quantile QTL (QR) association testing and computes quantile TWAS weights. For each genomic region it fits quantile regression across quantile levels (0.05, 0.1, ..., 0.95) and integrates the quantile-level p-values into a single QR p-value using the Cauchy combination method, then derives TWAS weights for use in downstream gene-trait association.\n",
- "\n",
- "The analysis is region-based: regions can be selected by name (`--region-name`), by a region list (`--region-list`), or, if neither is given, the union of all regions present in the phenotype region lists is analyzed.\n",
- "\n",
- "**Timing:** ~2-5 min on typical compute infrastructure.\n"
+ "Fits quantile regression of a molecular phenotype on genotype across a grid of quantiles, and turns the fit into TWAS weights."
]
},
{
@@ -23,43 +17,121 @@
"kernel": "SoS"
},
"source": [
- "## Step 1: Quantile QTL association and TWAS weight\n",
- "\n",
- "For each region the workflow fits quantile regression of the molecular phenotype on genotype across the quantile grid, combines the per-quantile p-values into a single QR p-value via the Cauchy combination method, and computes quantile TWAS weights. Covariates (genotype PCs, hidden factors, fixed covariates) are regressed out, and cis/trans windows are taken from a customized association-window file when provided, otherwise a fixed cis-window around each region is used.\n",
+ "## Overview\n",
"\n",
- "**Inputs**\n",
+ "Standard QTL mapping models the mean: it asks whether genotype shifts average expression. That misses variants whose effect is confined to part of the distribution - acting only in highly expressing samples, or changing spread rather than centre. Quantile regression tests across the distribution instead, so those effects become visible, and the same fit yields weights that can be carried into a TWAS.\n",
"\n",
- "| Option | Description |\n",
- "|--------|-------------|\n",
- "| `--genoFile` | Either a list of per-chromosome genotype files, or one PLINK `.bed` file for the whole genome. |\n",
- "| `--phenoFile` | One or more lists of phenotype files per region (`bed.gz` with `.bed.gz.tbi` index), output of the `phenotype_per_region`/`annotate_coord` preprocessing steps. |\n",
- "| `--covFile` | One or more covariate files matched to the phenotype lists (genotype PCs, hidden factors, fixed covariates). |\n",
- "| `--customized-association-windows` | Optional 4-column file (chr, start, end, region ID) defining cis/trans windows. If omitted, a fixed cis-window around each region is used. |\n",
- "| `--region-name` / `--region-list` | Optional ways to restrict the analysis to selected regions. Region IDs must match the 4th column of the phenotype list. |\n",
- "| `--phenotype-names` | Names for the phenotypic conditions (`cond1 cond2 ...`). |\n",
+ "For each region the workflow fits quantile regression of the molecular phenotype on genotype across the quantile grid, combines the per-quantile p-values into a single QR p-value by the Cauchy combination method, and computes quantile TWAS weights. Covariates - genotype PCs, hidden factors and fixed covariates - are regressed out, and cis or trans windows are taken from a customized association-window file when one is given, otherwise a fixed cis-window around each region is used.\n",
"\n",
- "*Example genotype list* (`--genoFile`):\n",
- "\n",
- "```\n",
- "#chr\tpath\n",
- "chr21\tprotocol_example.genotype.chr21.bed\n",
- "chr22\tprotocol_example.genotype.chr22.bed\n",
- "```\n",
- "\n",
- "Alternatively use `protocol_example.genotype.chr21_22.bed` if all chromosomes are in one file.\n",
- "\n",
- "*Example association-window file* (strictly 4 columns, header commented out):\n",
- "\n",
- "```\n",
- "#chr\tstart\tend\tgene_id\n",
- "chr10\t0\t6480000\tENSG00000008128\n",
- "chr1\t0\t6480000\tENSG00000008130\n",
- "chr1\t0\t6480000\tENSG00000067606\n",
- "```\n",
+ "**When to run it.** As an alternative to mean-based association and TWAS weight estimation, when you have reason to expect distribution-dependent effects."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS",
+ "tags": []
+ },
+ "source": [
+ "## Input\n",
+ "\n",
+ "- `--genoFile`: either one PLINK `.bed` for the whole genome, or a two-column list of per-chromosome genotype files:\n",
+ "\n",
+ " ```\n",
+ " #chr path\n",
+ " chr21 protocol_example.genotype.chr21.bed\n",
+ " chr22 protocol_example.genotype.chr22.bed\n",
+ " ```\n",
+ "\n",
+ "- `--phenoFile`: one or more per-region phenotype lists, as written by the `phenotype_per_region` and `annotate_coord` preprocessing steps. Each row names a region and points at its `bed.gz`, which must carry a `bed.gz.tbi` index. Example `output/phenotype_protein/protocol_example_protein.phenotype_by_chrom_files.region_list.txt`:\n",
+ "\n",
+ " ```\n",
+ " #chr start end ID path\n",
+ " chr22 17592135 17628748 ENSG00000131100_P36543 output/phenotype_protein/protocol_example_protein.chr22.bed.gz\n",
+ " chr22 17787648 18024560 ENSG00000243156_Q7RTP6 output/phenotype_protein/protocol_example_protein.chr22.bed.gz\n",
+ " ```\n",
+ "\n",
+ "- `--covFile`: one or more covariate files matched to the phenotype lists, carrying genotype PCs, hidden factors and fixed covariates, samples in columns. Example `output/covariate_protein/protocol_example_protein.chr22.<...>.Marchenko_PC.gz`:\n",
+ "\n",
+ " ```\n",
+ " #id SAMPLE_001 SAMPLE_002 SAMPLE_003 SAMPLE_004\n",
+ " msex 1 1 1 1\n",
+ " age_death 90.97 80.24 83.9 74.1\n",
+ " pmi 10.57 7.87 9.93 2.91\n",
+ " ```\n",
+ "\n",
+ "- `--customized-association-windows`: an optional 4-column file giving the cis or trans window for each region. Its 4th column must match the region ID in the 4th column of the phenotype list, otherwise the window for that region is not found. Without it a fixed cis-window is used around each region. Example `input/reference_data/TAD/protocol_example_protein.enhanced_cis_chr22.bed`:\n",
+ "\n",
+ " ```\n",
+ " #chr start end gene_id\n",
+ " chr22 0 18960000 ENSG00000131100_P36543\n",
+ " chr22 0 19024561 ENSG00000243156_Q7RTP6\n",
+ " ```\n",
+ "\n",
+ "- `--region-name` and `--region-list`: optional ways to restrict the run to selected regions, by naming them or by supplying a window file. Region IDs must match the 4th column of the phenotype list. With neither, every region in the phenotype lists is analysed.\n",
+ "- `--phenotype-names`: names for the phenotypic conditions, defaulting to the phenotype file basenames.\n",
+ "- `--name`: the stem of the output files.\n",
+ "- `--cwd`: the directory outputs are written to.\n",
+ "- `--maf`, `--mac` and `--imiss`: variant filters, `0.0025`, `5` and `1.0` by default. `--min-twas-maf` (`0.01`) applies to the TWAS weight step instead.\n",
+ "- `--indel`: `True` by default. Set `--no-indel` to drop indels from the analysis.\n",
+ "- `--screen-threshold` and `--screen-method`: `0.01` and `qvalue`, controlling which variants survive the QR screen before weights are computed."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "jp-MarkdownHeadingCollapsed": true,
+ "kernel": "SoS",
+ "tags": []
+ },
+ "source": [
+ "## Output\n",
+ "\n",
+ "- `{cwd}/quantile_qtl_twas_weight/{name}.{region}.univariate_qr_twas_weights.rds` - one file per analysed region, holding the quantile-QTL association results and the quantile TWAS weights. Example `output/quantile_twas/quantile_qtl_twas_weight/protocol_example_protein.chr22_ENSG00000241973_P42356.univariate_qr_twas_weights.rds`:\n",
+ "\n",
+ " ```\n",
+ " List of 1\n",
+ " $ ENSG00000241973_P42356:List of 1\n",
+ " ..$ protein_ENSG00000241973_P42356:List of 5\n",
+ " .. ..$ vqtl_results :'data.frame': 275 obs. of 12 variables:\n",
+ " .. ..$ qr_screen_pvalue_df:'data.frame': 275 obs. of 65 variables:\n",
+ " .. ..$ message : chr \"No significant SNPs detected in region ENSG00000241973_P42356_protein\"\n",
+ " .. ..$ region_info :List of 3\n",
+ " .. ..$ maf : Named num [1:275] 0.2583 0.3583 0.2417 0.0536 0.0536 ...\n",
+ " ```\n",
+ "\n",
+ "The object is nested by region, then by phenotype condition. `qr_screen_pvalue_df` carries the per-quantile QR p-values and q-values alongside the integrated Cauchy p-value, and `vqtl_results` carries the variance-QTL fit. "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "## Minimal Working Example\n",
"\n",
- "**Output**\n",
+ "The analysis is region-based. Regions can be picked one at a time by name, or taken from a region list; if neither is given, every region present in the phenotype region lists is analysed."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### One region by name\n",
"\n",
- "The pipeline writes results to the `--cwd` directory, named after the workflow step. Each region yields a quantile-QTL association result table (per-quantile and integrated Cauchy QR p-values) and the corresponding quantile TWAS weight object used downstream.\n"
+ "`--region-name` restricts the run to a single region, which is the quickest way to check the setup end to end."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
]
},
{
@@ -82,6 +154,26 @@
" "
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### Every region in a list\n",
+ "\n",
+ "`--region-list` runs the same workflow over each region in a 4-column window file. Dropping both options analyses every region in the phenotype lists."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ]
+ },
{
"cell_type": "code",
"execution_count": null,
@@ -101,11 +193,20 @@
" --phenotype-names protein \n"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "## Command Interface"
+ ]
+ },
{
"cell_type": "code",
"execution_count": null,
"metadata": {
- "kernel": "Bash"
+ "kernel": "SoS"
},
"outputs": [],
"source": [
@@ -118,9 +219,112 @@
"kernel": "SoS"
},
"source": [
- "## Workflow implementation\n",
- "\n",
- "The cells below define the SoS workflow steps used by the commands above. They are not meant to be edited for routine analysis.\n"
+ "```\n",
+ "usage: sos run pipeline/qr_and_twas.ipynb\n",
+ " [workflow_name | -t targets] [options] [workflow_options]\n",
+ " workflow_name: Single or combined workflows defined in this script\n",
+ " targets: One or more targets to generate\n",
+ " options: Single-hyphen sos parameters (see \"sos run -h\" for details)\n",
+ " workflow_options: Double-hyphen workflow-specific parameters\n",
+ "\n",
+ "Workflows:\n",
+ " get_analysis_regions\n",
+ " quantile_qtl_twas_weight\n",
+ "\n",
+ "Global Workflow Options:\n",
+ " --name VAL (as str, required)\n",
+ " It is required to input the name of the analysis\n",
+ " --cwd output (as path)\n",
+ " --genoFile VAL (as path, required)\n",
+ " A list of file paths for genotype data, or the genotype\n",
+ " data itself.\n",
+ " --phenoFile paths\n",
+ "\n",
+ " One or multiple lists of file paths for phenotype data.\n",
+ " --phenoIDFile paths()\n",
+ "\n",
+ " One or multiple lists of file paths for phenotype ID\n",
+ " mapping file. The first column should be the original\n",
+ " ID, the 2nd column should be the ID to be mapped to.\n",
+ " --covFile paths\n",
+ "\n",
+ " Covariate file path\n",
+ " --region-list . (as path)\n",
+ " Optional: if a region list is provide the analysis will\n",
+ " be focused on provided region. The LAST column of this\n",
+ " list will contain the ID of regions to focus on\n",
+ " Otherwise, all regions with both genotype and phenotype\n",
+ " files will be analyzed\n",
+ " --region-name (as list)\n",
+ " Optional: if a region name is provided the analysis\n",
+ " would be focused on the union of provides region list\n",
+ " and region names\n",
+ " --keep-samples . (as path)\n",
+ " Only focus on a subset of samples\n",
+ " --customized-association-windows . (as path)\n",
+ " An optional list documenting the custom association\n",
+ " window for each region to analyze, with four column,\n",
+ " chr, start, end, region ID (eg gene ID). If this list is\n",
+ " not provided, the default `window` parameter (see below)\n",
+ " will be used.\n",
+ " --cis-window -1 (as int)\n",
+ " Specify the cis window for the up and downstream radius\n",
+ " to analyze around the region of interest in units of bp\n",
+ " When this is set to negative, we will rely on using\n",
+ " customized_association_windows\n",
+ " --[no-]save-data (default to False)\n",
+ " save data object or not\n",
+ " --phenotype-names [f'{x:bn}' for x in phenoFile]\n",
+ "\n",
+ " Name of phenotypes\n",
+ " --seed 999 (as int)\n",
+ " --imiss 1.0 (as float)\n",
+ " remove a variant if it has more than imiss missing\n",
+ " individual level data\n",
+ " --maf 0.0025 (as float)\n",
+ " MAF cutoff\n",
+ " --mac 5 (as int)\n",
+ " MAC cutoff, on top of MAF cutoff\n",
+ " --[no-]indel (default to True)\n",
+ " Remove indels if indel = False\n",
+ " --min-twas-maf 0.01 (as float)\n",
+ " --screen-threshold 0.01 (as float)\n",
+ " --screen-method qvalue\n",
+ " --[no-]screen-significant (default to True)\n",
+ " --[no-]pre-filter-by-pqr (default to False)\n",
+ " --initial-corr-filter-cutoff 0.8 (as float)\n",
+ " --full-rank-corr-filter-cutoff 'seq(0.75, 0.5, by = -0.05)'\n",
+ " --ld-reference-meta-file . (as path)\n",
+ " --keep-variants . (as path)\n",
+ " Only focus on a subset of variants\n",
+ " --[no-]marginal-beta-calculate (default to True)\n",
+ " --[no-]twas-weight-calculate (default to True)\n",
+ " --[no-]qrank-screen-calculate (default to True)\n",
+ " --[no-]vqtl-calculate (default to True)\n",
+ " --container ''\n",
+ " Analysis environment settings\n",
+ " --job-size 200 (as int)\n",
+ " For cluster jobs, number commands to run per job\n",
+ " --walltime 1h\n",
+ " Wall clock time expected\n",
+ " --mem 20G\n",
+ " Memory expected\n",
+ " --numThreads 1 (as int)\n",
+ " Number of threads\n",
+ "\n",
+ "Sections\n",
+ " get_analysis_regions:\n",
+ " quantile_qtl_twas_weight:\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "## Workflow implementation"
]
},
{
@@ -264,7 +468,192 @@
"kernel": "SoS"
},
"outputs": [],
- "source": "[get_analysis_regions: shared = \"regional_data\"]\n# input is genoFile, phenoFile, covFile and optionally region_list. If region_list presents then we only analyze what's contained in the list.\n# regional_data should be a dictionary like:\n#{'data': [(\"genotype_1.bed\", \"phenotype_1.bed.gz\", \"covariate_1.gz\"), (\"genotype_2.bed\", \"phenotype_1.bed.gz\", \"phenotype_2.bed.gz\", \"covariate_1.gz\", \"covariate_2.gz\") ... ],\n# 'meta_info': [(\"chr12:752578-752579\",\"chr12:752577-752580\", \"gene_1\", \"trait_1\"), (\"chr13:852580-852581\",\"chr13:852579-852580\", \"gene_2\", \"trait_1\", \"trait_2\") ... ]}\nimport numpy as np\n\ndef preload_id_map(id_map_files):\n id_maps = {}\n for id_map_file in id_map_files:\n if id_map_file is not None and os.path.isfile(id_map_file):\n df = pd.read_csv(id_map_file, sep=r'\\s+', header=None, comment='#', names=['old_ID', 'new_ID'])\n id_maps[id_map_file] = df.set_index('old_ID')['new_ID'].to_dict()\n return id_maps\n\ndef load_and_apply_id_map(pheno_path, id_map_path, preloaded_id_maps):\n pheno_df = pd.read_csv(pheno_path, sep=r\"\\s+\", header=0)\n pheno_df['Original_ID'] = pheno_df['ID']\n if id_map_path in preloaded_id_maps:\n id_map = preloaded_id_maps[id_map_path]\n pheno_df['ID'] = pheno_df['ID'].map(id_map).fillna(pheno_df['ID'])\n return pheno_df\n\ndef filter_by_region_ids(data, region_ids):\n if region_ids is not None and len(region_ids) > 0:\n # Check both ID (mapped) and Original_ID (unmapped) to handle phenoIDFile cases\n # where region_name uses the original ID but ID column has been mapped\n if 'Original_ID' in data.columns:\n data = data[data['ID'].isin(region_ids) | data['Original_ID'].isin(region_ids)]\n else:\n data = data[data['ID'].isin(region_ids)]\n return data\n\ndef custom_join(series):\n # Initialize an empty list to hold the processed items\n result = []\n for item in series:\n if ',' in item:\n # If the item contains commas, split by comma and convert to tuple\n result.append(tuple(item.split(',')))\n else:\n # If the item does not contain commas, add it directly\n result.append(item)\n # Convert the list of items to a tuple and return\n return tuple(result)\n\ndef aggregate_phenotype_data(accumulated_pheno_df):\n if not accumulated_pheno_df.empty:\n accumulated_pheno_df = accumulated_pheno_df.groupby(['#chr','ID','cond','path','cov_path'], as_index=False).agg({\n '#chr': lambda x: np.unique(x).astype(str)[0],\n 'ID': lambda x: np.unique(x).astype(str)[0],\n 'Original_ID': ','.join,\n 'start': 'min',\n 'end': 'max'\n }).groupby(['#chr','ID'], as_index=False).agg({\n 'cond': ','.join,\n 'path': ','.join,\n 'Original_ID': custom_join,\n 'cov_path': ','.join,\n 'start': 'min',\n 'end': 'max'\n })\n return accumulated_pheno_df\n\ndef process_cis_files(pheno_files, cov_files, phenotype_names, pheno_id_files, region_ids, preloaded_id_maps):\n '''\n Example output:\n #chr start end ID Original_ID path cov_path cond\n chr12 752578 752579 ENSG00000060237 Q9H4A3,P62873 protocol_example.protein_1.bed.gz,protocol_example.protein_2.bed.gz covar_1.gz,covar_2.gz trait_A,trait_B\n '''\n accumulated_pheno_df = pd.DataFrame()\n pheno_id_files = [None] * len(pheno_files) if len(pheno_id_files) == 0 else pheno_id_files\n for pheno_path, cov_path, phenotype_name, id_map_path in zip(pheno_files, cov_files, phenotype_names, pheno_id_files):\n if not os.path.isfile(cov_path):\n raise FileNotFoundError(f\"No valid path found for file: {cov_path}\")\n pheno_df = load_and_apply_id_map(pheno_path, id_map_path, preloaded_id_maps)\n pheno_df = filter_by_region_ids(pheno_df, region_ids)\n if not pheno_df.empty:\n # Use 'path' column by name to handle both 5-col (#chr,start,end,ID,path)\n # and 6-col (#chr,start,end,ID,strand,path) region_list formats\n pheno_df['path'] = adapt_file_path_all(pheno_df, 'path', f\"{pheno_path:a}\")\n pheno_df = pheno_df.assign(cov_path=str(cov_path), cond=phenotype_name) \n accumulated_pheno_df = pd.concat([accumulated_pheno_df, pheno_df], ignore_index=True)\n\n accumulated_pheno_df = aggregate_phenotype_data(accumulated_pheno_df)\n return accumulated_pheno_df\n\ndef process_trans_files(pheno_files, cov_files, phenotype_names, pheno_id_files, region_ids, customized_association_windows):\n '''\n Example output:\n #chr start end ID Original_ID path cov_path cond\n chr21 0 0 chr21_18133254_19330300 carnitine,benzoate,hippurate metabolon_1.bed.gz,metabolon_2.bed.gz covar_1.gz,covar_2.gz trait_A,trait_B\n '''\n \n if not os.path.isfile(customized_association_windows):\n raise ValueError(\"Customized association analysis window must be specified for trans analysis.\")\n accumulated_pheno_df = pd.DataFrame()\n pheno_id_files = [None] * len(pheno_files) if len(pheno_id_files) == 0 else pheno_id_files\n genotype_windows = pd.read_csv(customized_association_windows, comment=\"#\", header=None, names=[\"#chr\",\"start\",\"end\",\"ID\"], sep=\"\\t\")\n genotype_windows = filter_by_region_ids(genotype_windows, region_ids)\n if genotype_windows.empty:\n return accumulated_pheno_df\n \n for pheno_path, cov_path, phenotype_name, id_map_path in zip(pheno_files, cov_files, phenotype_names, pheno_id_files):\n if not os.path.isfile(cov_path):\n raise FileNotFoundError(f\"No valid path found for file: {cov_path}\")\n pheno_df = pd.read_csv(pheno_path, sep=r\"\\s+\", header=0, names=['Original_ID', 'path'])\n if not pheno_df.empty:\n pheno_df.iloc[:, -1] = adapt_file_path_all(pheno_df, pheno_df.columns[-1], f\"{pheno_path:a}\")\n pheno_df = pheno_df.assign(cov_path=str(cov_path), cond=phenotype_name)\n # Here we combine genotype_windows which contains \"#chr\" and \"ID\" to pheno_df by creating a cartesian product\n pheno_df = pd.merge(genotype_windows.assign(key=1), pheno_df.assign(key=1), on='key').drop('key', axis=1)\n # then set start and end columns to zero\n pheno_df['start'] = 0\n pheno_df['end'] = 0\n if id_map_path is not None:\n # Filter pheno_df by specific association-window and phenotype pairs\n association_analysis_pair = pd.read_csv(id_map_path, sep=r'\\s+', header=None, comment='#', names=['ID', 'Original_ID'])\n pheno_df = pd.merge(pheno_df, association_analysis_pair, on=['ID', 'Original_ID'])\n accumulated_pheno_df = pd.concat([accumulated_pheno_df, pheno_df], ignore_index=True)\n\n accumulated_pheno_df = aggregate_phenotype_data(accumulated_pheno_df)\n return accumulated_pheno_df\n\n# Load genotype meta data\nif f\"{genoFile:x}\" == \".bed\":\n geno_meta_data = pd.DataFrame([(\"chr\"+str(x), f\"{genoFile:a}\") for x in range(1,23)] + [(\"chrX\", f\"{genoFile:a}\")], columns=['#chr', 'geno_path'])\nelse:\n geno_meta_data = pd.read_csv(f\"{genoFile:a}\", sep = r\"\\s+\", header=0)\n geno_meta_data.iloc[:, 1] = adapt_file_path_all(geno_meta_data, geno_meta_data.columns[1], f\"{genoFile:a}\")\n geno_meta_data.columns = ['#chr', 'geno_path']\n geno_meta_data['#chr'] = geno_meta_data['#chr'].apply(lambda x: str(x) if str(x).startswith('chr') else f'chr{x}')\n\n# Checking the DataFrame\nvalid_chr_values = [f'chr{x}' for x in range(1, 23)] + ['chrX']\nif not all(value in valid_chr_values for value in geno_meta_data['#chr']):\n raise ValueError(\"Invalid chromosome values found. Allowed values are chr1 to chr22 and chrX.\")\n\nregion_ids = []\n# If region_list is provided, read the file and extract IDs\nif region_list.is_file():\n region_list_df = pd.read_csv(region_list, sep=r'\\s+', header=None, comment = \"#\")\n region_ids = region_list_df.iloc[:, -1].unique() # Extracting the last column for IDs\n# If region_name is provided, include those IDs as well\n# --region-name A B C will result in a list of [\"A\", \"B\", \"C\"] here\nif len(region_name) > 0:\n region_ids = list(set(region_ids).union(set(region_name)))\n\ntrans_analysis = False\nif trans_analysis:\n meta_data = process_trans_files(phenoFile, covFile, phenotype_names, phenoIDFile, region_ids, customized_association_windows)\nelse:\n meta_data = process_cis_files(phenoFile, covFile, phenotype_names, phenoIDFile, region_ids, preload_id_map(phenoIDFile))\n\nif not meta_data.empty:\n meta_data = meta_data.merge(geno_meta_data, on='#chr', how='inner')\n # Adjust association-window\n if os.path.isfile(customized_association_windows):\n print(f\"Loading customized association analysis window from {customized_association_windows}\")\n association_windows_list = pd.read_csv(customized_association_windows, comment=\"#\", header=None, names=[\"#chr\",\"start\",\"end\",\"ID\"], sep=\"\\t\")\n meta_data = pd.merge(meta_data, association_windows_list, on=['#chr', 'ID'], how='left', suffixes=('', '_association'))\n mismatches = meta_data[meta_data['start_association'].isna()]\n if not mismatches.empty:\n raise ValueError(f\"{len(mismatches)} regions to analyze cannot be found in ``{customized_association_windows}``. Please check your ``{customized_association_windows}`` database to make sure it contains all association-window definitions. \")\n else:\n if cis_window < 0 :\n raise ValueError(\"Please either input valid path to association-window file via ``--customized-association-windows``, or set ``--cis-window`` to a non-negative integer.\")\n if cis_window == 0:\n print(\"Warning: only variants within the range of start and end of molecular phenotype will be considered since cis_window is set to zero and no customized association window file was found. Please make sure this is by design.\")\n meta_data['start_association'] = meta_data['start'].apply(lambda x: max(x - cis_window, 0))\n meta_data['end_association'] = meta_data['end'] + cis_window\n\n # Example meta_data:\n # #chr start end start_association end_association ID Original_ID path cov_path cond coordinate geno_path\n # 0 chr12 752578 752579 652578 852579 ENSG00000060237 Q9H4A3,P62873 protocol_example.protein_1.bed.gz,protocol_example.protein_2.bed.gz covar_1.gz,covar_2.gz trait_A,trait_B chr12:752578-752579 protocol_example.genotype.chr21_22.bed \n # Create the final dictionary\n regional_data = {\n 'data': [(row['geno_path'], *row['path'].split(','), *row['cov_path'].split(',')) for _, row in meta_data.iterrows()],\n 'meta_info': [(f\"{row['#chr']}:{row['start']}-{row['end']}\", # this is the phenotypic region to extract data from\n f\"{row['#chr']}:{row['start_association']}-{row['end_association']}\", # this is the association window region\n row['ID'], row['Original_ID'], *row['cond'].split(',')) for _, row in meta_data.iterrows()]\n }\nelse:\n regional_data = {'data':[], 'meta_info':[]}"
+ "source": [
+ "[get_analysis_regions: shared = \"regional_data\"]\n",
+ "# input is genoFile, phenoFile, covFile and optionally region_list. If region_list presents then we only analyze what's contained in the list.\n",
+ "# regional_data should be a dictionary like:\n",
+ "#{'data': [(\"genotype_1.bed\", \"phenotype_1.bed.gz\", \"covariate_1.gz\"), (\"genotype_2.bed\", \"phenotype_1.bed.gz\", \"phenotype_2.bed.gz\", \"covariate_1.gz\", \"covariate_2.gz\") ... ],\n",
+ "# 'meta_info': [(\"chr12:752578-752579\",\"chr12:752577-752580\", \"gene_1\", \"trait_1\"), (\"chr13:852580-852581\",\"chr13:852579-852580\", \"gene_2\", \"trait_1\", \"trait_2\") ... ]}\n",
+ "import numpy as np\n",
+ "\n",
+ "def preload_id_map(id_map_files):\n",
+ " id_maps = {}\n",
+ " for id_map_file in id_map_files:\n",
+ " if id_map_file is not None and os.path.isfile(id_map_file):\n",
+ " df = pd.read_csv(id_map_file, sep=r'\\s+', header=None, comment='#', names=['old_ID', 'new_ID'])\n",
+ " id_maps[id_map_file] = df.set_index('old_ID')['new_ID'].to_dict()\n",
+ " return id_maps\n",
+ "\n",
+ "def load_and_apply_id_map(pheno_path, id_map_path, preloaded_id_maps):\n",
+ " pheno_df = pd.read_csv(pheno_path, sep=r\"\\s+\", header=0)\n",
+ " pheno_df['Original_ID'] = pheno_df['ID']\n",
+ " if id_map_path in preloaded_id_maps:\n",
+ " id_map = preloaded_id_maps[id_map_path]\n",
+ " pheno_df['ID'] = pheno_df['ID'].map(id_map).fillna(pheno_df['ID'])\n",
+ " return pheno_df\n",
+ "\n",
+ "def filter_by_region_ids(data, region_ids):\n",
+ " if region_ids is not None and len(region_ids) > 0:\n",
+ " # Check both ID (mapped) and Original_ID (unmapped) to handle phenoIDFile cases\n",
+ " # where region_name uses the original ID but ID column has been mapped\n",
+ " if 'Original_ID' in data.columns:\n",
+ " data = data[data['ID'].isin(region_ids) | data['Original_ID'].isin(region_ids)]\n",
+ " else:\n",
+ " data = data[data['ID'].isin(region_ids)]\n",
+ " return data\n",
+ "\n",
+ "def custom_join(series):\n",
+ " # Initialize an empty list to hold the processed items\n",
+ " result = []\n",
+ " for item in series:\n",
+ " if ',' in item:\n",
+ " # If the item contains commas, split by comma and convert to tuple\n",
+ " result.append(tuple(item.split(',')))\n",
+ " else:\n",
+ " # If the item does not contain commas, add it directly\n",
+ " result.append(item)\n",
+ " # Convert the list of items to a tuple and return\n",
+ " return tuple(result)\n",
+ "\n",
+ "def aggregate_phenotype_data(accumulated_pheno_df):\n",
+ " if not accumulated_pheno_df.empty:\n",
+ " accumulated_pheno_df = accumulated_pheno_df.groupby(['#chr','ID','cond','path','cov_path'], as_index=False).agg({\n",
+ " '#chr': lambda x: np.unique(x).astype(str)[0],\n",
+ " 'ID': lambda x: np.unique(x).astype(str)[0],\n",
+ " 'Original_ID': ','.join,\n",
+ " 'start': 'min',\n",
+ " 'end': 'max'\n",
+ " }).groupby(['#chr','ID'], as_index=False).agg({\n",
+ " 'cond': ','.join,\n",
+ " 'path': ','.join,\n",
+ " 'Original_ID': custom_join,\n",
+ " 'cov_path': ','.join,\n",
+ " 'start': 'min',\n",
+ " 'end': 'max'\n",
+ " })\n",
+ " return accumulated_pheno_df\n",
+ "\n",
+ "def process_cis_files(pheno_files, cov_files, phenotype_names, pheno_id_files, region_ids, preloaded_id_maps):\n",
+ " '''\n",
+ " Example output:\n",
+ " #chr start end ID Original_ID path cov_path cond\n",
+ " chr12 752578 752579 ENSG00000060237 Q9H4A3,P62873 protocol_example.protein_1.bed.gz,protocol_example.protein_2.bed.gz covar_1.gz,covar_2.gz trait_A,trait_B\n",
+ " '''\n",
+ " accumulated_pheno_df = pd.DataFrame()\n",
+ " pheno_id_files = [None] * len(pheno_files) if len(pheno_id_files) == 0 else pheno_id_files\n",
+ " for pheno_path, cov_path, phenotype_name, id_map_path in zip(pheno_files, cov_files, phenotype_names, pheno_id_files):\n",
+ " if not os.path.isfile(cov_path):\n",
+ " raise FileNotFoundError(f\"No valid path found for file: {cov_path}\")\n",
+ " pheno_df = load_and_apply_id_map(pheno_path, id_map_path, preloaded_id_maps)\n",
+ " pheno_df = filter_by_region_ids(pheno_df, region_ids)\n",
+ " if not pheno_df.empty:\n",
+ " # Use 'path' column by name to handle both 5-col (#chr,start,end,ID,path)\n",
+ " # and 6-col (#chr,start,end,ID,strand,path) region_list formats\n",
+ " pheno_df['path'] = adapt_file_path_all(pheno_df, 'path', f\"{pheno_path:a}\")\n",
+ " pheno_df = pheno_df.assign(cov_path=str(cov_path), cond=phenotype_name) \n",
+ " accumulated_pheno_df = pd.concat([accumulated_pheno_df, pheno_df], ignore_index=True)\n",
+ "\n",
+ " accumulated_pheno_df = aggregate_phenotype_data(accumulated_pheno_df)\n",
+ " return accumulated_pheno_df\n",
+ "\n",
+ "def process_trans_files(pheno_files, cov_files, phenotype_names, pheno_id_files, region_ids, customized_association_windows):\n",
+ " '''\n",
+ " Example output:\n",
+ " #chr start end ID Original_ID path cov_path cond\n",
+ " chr21 0 0 chr21_18133254_19330300 carnitine,benzoate,hippurate metabolon_1.bed.gz,metabolon_2.bed.gz covar_1.gz,covar_2.gz trait_A,trait_B\n",
+ " '''\n",
+ " \n",
+ " if not os.path.isfile(customized_association_windows):\n",
+ " raise ValueError(\"Customized association analysis window must be specified for trans analysis.\")\n",
+ " accumulated_pheno_df = pd.DataFrame()\n",
+ " pheno_id_files = [None] * len(pheno_files) if len(pheno_id_files) == 0 else pheno_id_files\n",
+ " genotype_windows = pd.read_csv(customized_association_windows, comment=\"#\", header=None, names=[\"#chr\",\"start\",\"end\",\"ID\"], sep=\"\\t\")\n",
+ " genotype_windows = filter_by_region_ids(genotype_windows, region_ids)\n",
+ " if genotype_windows.empty:\n",
+ " return accumulated_pheno_df\n",
+ " \n",
+ " for pheno_path, cov_path, phenotype_name, id_map_path in zip(pheno_files, cov_files, phenotype_names, pheno_id_files):\n",
+ " if not os.path.isfile(cov_path):\n",
+ " raise FileNotFoundError(f\"No valid path found for file: {cov_path}\")\n",
+ " pheno_df = pd.read_csv(pheno_path, sep=r\"\\s+\", header=0, names=['Original_ID', 'path'])\n",
+ " if not pheno_df.empty:\n",
+ " pheno_df.iloc[:, -1] = adapt_file_path_all(pheno_df, pheno_df.columns[-1], f\"{pheno_path:a}\")\n",
+ " pheno_df = pheno_df.assign(cov_path=str(cov_path), cond=phenotype_name)\n",
+ " # Here we combine genotype_windows which contains \"#chr\" and \"ID\" to pheno_df by creating a cartesian product\n",
+ " pheno_df = pd.merge(genotype_windows.assign(key=1), pheno_df.assign(key=1), on='key').drop('key', axis=1)\n",
+ " # then set start and end columns to zero\n",
+ " pheno_df['start'] = 0\n",
+ " pheno_df['end'] = 0\n",
+ " if id_map_path is not None:\n",
+ " # Filter pheno_df by specific association-window and phenotype pairs\n",
+ " association_analysis_pair = pd.read_csv(id_map_path, sep=r'\\s+', header=None, comment='#', names=['ID', 'Original_ID'])\n",
+ " pheno_df = pd.merge(pheno_df, association_analysis_pair, on=['ID', 'Original_ID'])\n",
+ " accumulated_pheno_df = pd.concat([accumulated_pheno_df, pheno_df], ignore_index=True)\n",
+ "\n",
+ " accumulated_pheno_df = aggregate_phenotype_data(accumulated_pheno_df)\n",
+ " return accumulated_pheno_df\n",
+ "\n",
+ "# Load genotype meta data\n",
+ "if f\"{genoFile:x}\" == \".bed\":\n",
+ " geno_meta_data = pd.DataFrame([(\"chr\"+str(x), f\"{genoFile:a}\") for x in range(1,23)] + [(\"chrX\", f\"{genoFile:a}\")], columns=['#chr', 'geno_path'])\n",
+ "else:\n",
+ " geno_meta_data = pd.read_csv(f\"{genoFile:a}\", sep = r\"\\s+\", header=0)\n",
+ " geno_meta_data.iloc[:, 1] = adapt_file_path_all(geno_meta_data, geno_meta_data.columns[1], f\"{genoFile:a}\")\n",
+ " geno_meta_data.columns = ['#chr', 'geno_path']\n",
+ " geno_meta_data['#chr'] = geno_meta_data['#chr'].apply(lambda x: str(x) if str(x).startswith('chr') else f'chr{x}')\n",
+ "\n",
+ "# Checking the DataFrame\n",
+ "valid_chr_values = [f'chr{x}' for x in range(1, 23)] + ['chrX']\n",
+ "if not all(value in valid_chr_values for value in geno_meta_data['#chr']):\n",
+ " raise ValueError(\"Invalid chromosome values found. Allowed values are chr1 to chr22 and chrX.\")\n",
+ "\n",
+ "region_ids = []\n",
+ "# If region_list is provided, read the file and extract IDs\n",
+ "if region_list.is_file():\n",
+ " region_list_df = pd.read_csv(region_list, sep=r'\\s+', header=None, comment = \"#\")\n",
+ " region_ids = region_list_df.iloc[:, -1].unique() # Extracting the last column for IDs\n",
+ "# If region_name is provided, include those IDs as well\n",
+ "# --region-name A B C will result in a list of [\"A\", \"B\", \"C\"] here\n",
+ "if len(region_name) > 0:\n",
+ " region_ids = list(set(region_ids).union(set(region_name)))\n",
+ "\n",
+ "trans_analysis = False\n",
+ "if trans_analysis:\n",
+ " meta_data = process_trans_files(phenoFile, covFile, phenotype_names, phenoIDFile, region_ids, customized_association_windows)\n",
+ "else:\n",
+ " meta_data = process_cis_files(phenoFile, covFile, phenotype_names, phenoIDFile, region_ids, preload_id_map(phenoIDFile))\n",
+ "\n",
+ "if not meta_data.empty:\n",
+ " meta_data = meta_data.merge(geno_meta_data, on='#chr', how='inner')\n",
+ " # Adjust association-window\n",
+ " if os.path.isfile(customized_association_windows):\n",
+ " print(f\"Loading customized association analysis window from {customized_association_windows}\")\n",
+ " association_windows_list = pd.read_csv(customized_association_windows, comment=\"#\", header=None, names=[\"#chr\",\"start\",\"end\",\"ID\"], sep=\"\\t\")\n",
+ " meta_data = pd.merge(meta_data, association_windows_list, on=['#chr', 'ID'], how='left', suffixes=('', '_association'))\n",
+ " mismatches = meta_data[meta_data['start_association'].isna()]\n",
+ " if not mismatches.empty:\n",
+ " raise ValueError(f\"{len(mismatches)} regions to analyze cannot be found in ``{customized_association_windows}``. Please check your ``{customized_association_windows}`` database to make sure it contains all association-window definitions. \")\n",
+ " else:\n",
+ " if cis_window < 0 :\n",
+ " raise ValueError(\"Please either input valid path to association-window file via ``--customized-association-windows``, or set ``--cis-window`` to a non-negative integer.\")\n",
+ " if cis_window == 0:\n",
+ " print(\"Warning: only variants within the range of start and end of molecular phenotype will be considered since cis_window is set to zero and no customized association window file was found. Please make sure this is by design.\")\n",
+ " meta_data['start_association'] = meta_data['start'].apply(lambda x: max(x - cis_window, 0))\n",
+ " meta_data['end_association'] = meta_data['end'] + cis_window\n",
+ "\n",
+ " # Example meta_data:\n",
+ " # #chr start end start_association end_association ID Original_ID path cov_path cond coordinate geno_path\n",
+ " # 0 chr12 752578 752579 652578 852579 ENSG00000060237 Q9H4A3,P62873 protocol_example.protein_1.bed.gz,protocol_example.protein_2.bed.gz covar_1.gz,covar_2.gz trait_A,trait_B chr12:752578-752579 protocol_example.genotype.chr21_22.bed \n",
+ " # Create the final dictionary\n",
+ " regional_data = {\n",
+ " 'data': [(row['geno_path'], *row['path'].split(','), *row['cov_path'].split(',')) for _, row in meta_data.iterrows()],\n",
+ " 'meta_info': [(f\"{row['#chr']}:{row['start']}-{row['end']}\", # this is the phenotypic region to extract data from\n",
+ " f\"{row['#chr']}:{row['start_association']}-{row['end_association']}\", # this is the association window region\n",
+ " row['ID'], row['Original_ID'], *row['cond'].split(',')) for _, row in meta_data.iterrows()]\n",
+ " }\n",
+ "else:\n",
+ " regional_data = {'data':[], 'meta_info':[]}"
+ ]
},
{
"cell_type": "code",
@@ -546,4 +935,4 @@
},
"nbformat": 4,
"nbformat_minor": 4
-}
\ No newline at end of file
+}
diff --git a/code/SoS/data_preprocessing/covariate/covariate_formatting.ipynb b/code/SoS/data_preprocessing/covariate/covariate_formatting.ipynb
index 6dcb64cb7..50d575e8b 100644
--- a/code/SoS/data_preprocessing/covariate/covariate_formatting.ipynb
+++ b/code/SoS/data_preprocessing/covariate/covariate_formatting.ipynb
@@ -8,25 +8,8 @@
},
"source": [
"# Covariate Data Formatting\n",
- "\n"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "## Description"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "Our covariate preprocessing steps merge genotypic principal components and fixed covariate files into one file for downstream QTL analysis. "
+ "\n",
+ "Merges known covariates, genotype principal components and hidden factors into the single file the association scan expects."
]
},
{
@@ -35,12 +18,16 @@
"kernel": "SoS"
},
"source": [
- "## Input Files\n",
+ "## Overview\n",
+ "\n",
+ "The association scan takes a single covariate file, but covariates arrive from several\n",
+ "places: batch and demographic variables you supply, genotype principal components from\n",
+ "`PCA`, and hidden factors estimated by `covariate_hidden_factor`. This step merges them,\n",
+ "reconciles sample identifiers across the sources, and writes the combined matrix in the\n",
+ "orientation the scan expects.\n",
"\n",
- "| File | Description |\n",
- "|------|-------------|\n",
- "| `protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca.rds` | Genotype PCA result (RDS) produced by the genotype PCA module; contains the `pc_scores` matrix of per-sample principal components. |\n",
- "| `protocol_example.covariates.tsv` | Fixed/known covariates (e.g. sex, age, PMI, study), samples in rows and covariates in columns with an `#id` header. |"
+ "**When to run it.** After PCA and hidden-factor estimation, immediately before QTL\n",
+ "association testing."
]
},
{
@@ -49,11 +36,40 @@
"kernel": "SoS"
},
"source": [
- "## Output Files\n",
+ "## Input\n",
"\n",
- "| File | Description |\n",
- "|------|-------------|\n",
- "| `protocol_example.covariates.protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca.gz` | Combined covariate matrix: the fixed covariates stacked with the selected genotype PCs, samples as columns and an `#id` header, ready for downstream QTL analysis. |"
+ "- `--covFile`: the known covariate table, samples in columns and covariates in rows, with an `#id` header line. Example `input/covariate/protocol_example.covariates.base.tsv`, showing the first 4 of 59 samples:\n",
+ "\n",
+ " ```\n",
+ " #id SAMPLE_001 SAMPLE_002 SAMPLE_003 SAMPLE_004\n",
+ " sex 1 1 1 1\n",
+ " age 90.97 80.24 83.9 74.1\n",
+ " ```\n",
+ "\n",
+ "- `--pcaFile`: the genotype PCA result written by the `PCA` module, an RDS holding the per-sample scores in `pc_scores`. Example `output/genotype/genotype_pca/protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca.rds`:\n",
+ "\n",
+ " ```\n",
+ " List of 6\n",
+ " $ pca_model :List of 7\n",
+ " ..- attr(*, \"class\")= chr \"flashpca\"\n",
+ " $ pc_scores :'data.frame': 60 obs. of 28 variables:\n",
+ " $ meta : chr \"protocol_example.genotype.merged.plink_qc.plink_qc.prune \"\n",
+ " $ pc_cov : num [1:5, 1:5] 1.91e-02 -4.83e-18 6.02e-18 -1.35e-18 2.33e-18 ...\n",
+ " $ pc_mean : num [1:5] 5.17e-17 -1.22e-17 -4.58e-17 7.87e-17 -3.73e-17\n",
+ " $ pc_median : num [1:5] 0.00271 0.00131 0.00781 -0.00328 0.00532\n",
+ " ```\n",
+ "\n",
+ "- `--k`: how many genotype PCs to keep, 20 by default. The example instead reads the count off the PCA scree file, taking the last PC whose cumulative variance explained stays under 80%. Scree file `output/genotype/genotype_pca/protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca.scree.txt`:\n",
+ "\n",
+ " ```\n",
+ " PCs PVE PVE_cum\n",
+ " 1 0.05 0.05\n",
+ " 2 0.05 0.1\n",
+ " ```\n",
+ "\n",
+ "- `--tol-cov`: the largest fraction of missing values a covariate row may carry and still be kept, its missing entries mean-imputed rather than the row dropped. The default `-1.0` drops any row that has a missing value; `0.4` admits rows up to 40% missing.\n",
+ "- `--name`: the stem of the output file.\n",
+ "- `--cwd`: the directory the output is written to."
]
},
{
@@ -62,9 +78,18 @@
"kernel": "SoS"
},
"source": [
- "## Minimal Working Example Steps\n",
+ "## Output\n",
+ "\n",
+ "- `{cwd}/{name}.gz`: the combined covariate matrix, samples in columns and covariates in rows under an `#id` header, in the orientation the association scan expects. Example `output/covariate/protocol_example.covariates.protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca.gz` has 60 columns and 18 rows, the header plus the 2 supplied covariates plus the 15 PCs `--k` selected. First 4 columns and first 6 rows:\n",
"\n",
- "The data and singularity used in this minimal working example can be found on [Synapse](https://www.synapse.org/#!Synapse:syn69670658)."
+ " ```\n",
+ " #id SAMPLE_001 SAMPLE_002 SAMPLE_003\n",
+ " sex 1 1 1\n",
+ " age 90.97 80.24 83.9\n",
+ " PC1 -0.45257953377688636 -0.04501122972985456 0.07044283190889761\n",
+ " PC2 0.049068179683990002 -0.1674973221595418 -0.004163580814102901\n",
+ " PC3 -0.021287959073347167 0.011427192975949114 0.2219354934004062\n",
+ " ```"
]
},
{
@@ -73,9 +98,7 @@
"kernel": "SoS"
},
"source": [
- "### Merge Covariates and Genotype PCs\n",
- "\n",
- "The data and singularity used in this minimal working example can be found on [Synapse](https://www.synapse.org/#!Synapse:syn69670658)."
+ "## Minimal Working Example"
]
},
{
@@ -84,16 +107,19 @@
"kernel": "SoS"
},
"source": [
- "Timing: <1 min"
+ "### Merge covariates and genotype PCs\n",
+ "\n",
+ "This step keeps the samples present in both the PCA result and the covariate table, then stacks the chosen genotype PCs on top of the known covariates to give the association scan a single matrix."
]
},
{
"cell_type": "markdown",
"metadata": {
- "kernel": "SoS"
+ "kernel": "SoS",
+ "tags": []
},
"source": [
- "This step reads the genotype PCA result (`--pcaFile`) and the fixed covariate table (`--covFile`), keeps the samples present in both, and stacks the chosen genotype PCs on top of the covariates to produce a single matrix for QTL analysis. The number of PCs to keep is set with `--k`; here we pick the count whose cumulative variance explained stays under 80% by reading the PCA scree file. `--tol-cov 0.4` allows samples with up to 40% missing covariate values to be mean-imputed rather than dropped."
+ "**Timing**: TBD (on toy dataset)"
]
},
{
@@ -129,11 +155,11 @@
"cell_type": "code",
"execution_count": null,
"metadata": {
- "kernel": "Bash"
+ "kernel": "SoS"
},
"outputs": [],
"source": [
- "sos run covariate_formatting.ipynb -h"
+ "sos run pipeline/covariate_formatting.ipynb -h"
]
},
{
@@ -142,7 +168,65 @@
"kernel": "SoS"
},
"source": [
- "## Setup and global parameters"
+ "```\n",
+ "usage: sos run pipeline/covariate_formatting.ipynb\n",
+ " [workflow_name | -t targets] [options] [workflow_options]\n",
+ " workflow_name: Single or combined workflows defined in this script\n",
+ " targets: One or more targets to generate\n",
+ " options: Single-hyphen sos parameters (see \"sos run -h\" for details)\n",
+ " workflow_options: Double-hyphen workflow-specific parameters\n",
+ "\n",
+ "Workflows:\n",
+ " merge_genotype_pc\n",
+ "\n",
+ "Global Workflow Options:\n",
+ " --modular-script-dir code/script (as path)\n",
+ " --cwd output (as path)\n",
+ " The output directory for generated files.\n",
+ " --covFile VAL (as path, required)\n",
+ " The covariate file\n",
+ " --job-size 1 (as int)\n",
+ " For cluster jobs, number commands to run per job\n",
+ " --walltime 5h\n",
+ " Wall clock time expected\n",
+ " --mem 2G\n",
+ " Memory expected\n",
+ " --numThreads 8 (as int)\n",
+ " Number of threads\n",
+ " --container ''\n",
+ " Software container option\n",
+ " --entrypoint ''\n",
+ "\n",
+ "Sections\n",
+ " merge_genotype_pc:\n",
+ " Workflow Options:\n",
+ " --pcaFile VAL (as path, required)\n",
+ " An RDS file as the output of the genotype PCA module\n",
+ " --k 20 (as int)\n",
+ " The number of PCs to retain, by default is 20, in\n",
+ " practice can be the number of PC that captured more than\n",
+ " 70% PVE\n",
+ " --name f'{covFile:bn}.{pcaFile:bn}'\n",
+ "\n",
+ " --outliersFile . (as path)\n",
+ " Outliers\n",
+ " --[no-]remove-outliers (default to False)\n",
+ " --tol-cov -1.0 (as float)\n",
+ " Tolerance of missingness in covariates, -1 means do\n",
+ " nothing, otherwise for samples with covariates missing\n",
+ " rate larger than tol_cov will be removed, with missing\n",
+ " rate smaller than tol_cov will be kept.\n",
+ " --[no-]mean-impute (default to True)\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "## Workflow implementation"
]
},
{
@@ -173,29 +257,9 @@
"cwd = path(f\"{cwd:a}\")"
]
},
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "### **Step 0.** Merge Covariates and Genotype PCs"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "## Anticipated Results\n",
- "\n",
- "The pipeline produces output files in the `output/` subdirectory named after the workflow step. Verify success by checking that output files exist and are non-empty. See the **Output** section above for the expected file names and formats."
- ]
- },
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 1,
"metadata": {
"kernel": "SoS"
},
@@ -232,6 +296,15 @@
" ${\"--mean-impute\" if mean_impute else \"\"} \\\n",
" --numThreads ${numThreads}\n"
]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "outputs": [],
+ "source": []
}
],
"metadata": {
diff --git a/code/SoS/data_preprocessing/covariate/covariate_hidden_factor.ipynb b/code/SoS/data_preprocessing/covariate/covariate_hidden_factor.ipynb
index d22dedd5a..f65e78dcd 100644
--- a/code/SoS/data_preprocessing/covariate/covariate_hidden_factor.ipynb
+++ b/code/SoS/data_preprocessing/covariate/covariate_hidden_factor.ipynb
@@ -7,7 +7,9 @@
"tags": []
},
"source": [
- "# Hidden Factor Analysis"
+ "# Hidden Factor Analysis\n",
+ "\n",
+ "Infers hidden factors - latent technical and biological covariates - from a molecular phenotype matrix."
]
},
{
@@ -16,18 +18,13 @@
"kernel": "SoS"
},
"source": [
- "## Description\n",
- "\n",
- "This step infers **hidden factors** (latent technical/biological covariates) from a molecular phenotype matrix. Including these factors as covariates in xQTL mapping improves power and controls for unwanted variation.\n",
+ "## Overview\n",
"\n",
- "The pipeline provides several procedures:\n",
+ "Molecular phenotype matrices carry structure nobody recorded: batch, cell-type composition, unmeasured technical variation. Left in, that structure inflates false positives in the association scan, because a variant that happens to track a batch looks like a QTL. This step estimates those hidden factors from the data and adds them to the known covariates, so the scan can regress them out.\n",
"\n",
- "- **PEER** – Probabilistic Estimation of Expression Residuals (Stegle et al. 2010), the approach used by GTEx.\n",
- "- **PCA (Marchenko-Pastur)** – the approach used for our main analyses: residualize the phenotype on known covariates, run PCA, and keep the number of PCs above the Marchenko-Pastur noise threshold.\n",
- "- **PCA (Buja & Eyuboglu permutation)** – an alternative that chooses the number of PCs by permutation (`jackstraw`).\n",
- "- **BiCV factor analysis with APEX** – Bi-cross-validation using the APEX tool.\n",
+ "Every method here follows the same shape: residualise the phenotype on the covariates you already have, estimate factors from what is left, then stack the chosen factors back on top of the known covariates. They differ only in how factors are estimated and how many are kept.\n",
"\n",
- "The minimal working example below runs the **PCA (Marchenko-Pastur)** method, since it runs fully on the toy data with no external container or network access."
+ "**When to run it.** After known covariates are merged and before the association scan, on the phenotype matrix the scan will use."
]
},
{
@@ -36,14 +33,24 @@
"kernel": "SoS"
},
"source": [
- "## Input Files\n",
+ "## Input\n",
+ "\n",
+ "- `--phenoFile`: the molecular phenotype matrix in UCSC `bed.gz` form, one row per feature and one column per sample. Example `output/rnaseq/protocol_example.rnaseq.bed.bed.gz`.\n",
+ "- `--covFile`: the merged known-covariate matrix, an `#id` header plus one column per sample, as written by `covariate_formatting`. Example `output/covariate/protocol_example.covariates.protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca.gz`:\n",
"\n",
- "| File | Description |\n",
- "|------|-------------|\n",
- "| `--phenoFile` | Molecular phenotype matrix in indexed `bed.gz` format (`#chr start end ID` + one column per sample). Toy file: `output/rnaseq/protocol_example.rnaseq.bed.bed.gz` (200 genes × 60 samples). |\n",
- "| `--covFile` | Merged known-covariate matrix (`#id` + one column per sample), e.g. the output of `covariate_formatting`. Toy file: `output/covariate/protocol_example.covariates.protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca.gz`. |\n",
+ " ```\n",
+ " #id SAMPLE_001 SAMPLE_002 SAMPLE_003\n",
+ " sex 1 1 1\n",
+ " age 90.97 80.24 83.9\n",
+ " PC1 -0.4525795 -0.0450112 0.0704428\n",
+ " ```\n",
"\n",
- "Notes: the covariate file is optional for PEER but recommended so a proper xQTL model is built. The `BiCV`/`APEX` method additionally requires an indexed VCF."
+ " The covariate file is optional for `PEER` but recommended, so that a proper xQTL model is built. `BiCV` additionally requires an indexed VCF, which the workflow generates for itself.\n",
+ "\n",
+ "- `--N`: how many hidden factors to keep. `0`, the default, lets the method choose: `Marchenko_PC` keeps components above the Marchenko-Pastur bound and `PCA` uses the Buja and Eyuboglu permutation. Give a positive number to fix the count instead, which `PEER` needs.\n",
+ "- `--choose_k_method`: `Marchenko` or the permutation alternative, selecting which rule the shared PCA step applies. It also names the output file.\n",
+ "- `--mean-impute-missing`: off by default. When set, missing phenotype values are mean-imputed rather than the feature being dropped.\n",
+ "- `--cwd`: the directory outputs are written to. Must be a full path."
]
},
{
@@ -52,14 +59,25 @@
"kernel": "SoS"
},
"source": [
- "## Output Files\n",
+ "## Output\n",
+ "\n",
+ "- `{cwd}/{name}.{choose_k_method}_PC.gz` - the deliverable: the known covariates with the inferred `Hidden_Factor_PC*` rows stacked underneath, ready for the association scan. `{name}` is the phenotype file stem joined to the covariate file stem. Example `...prune.pca.Marchenko_PC.gz` has 60 columns and 26 rows, being the header plus 2 known covariates plus the 15 genotype PCs plus the 8 hidden factors this method kept:\n",
"\n",
- "| File | Description |\n",
- "|------|-------------|\n",
- "| `{phenoFile:bn}.{covFile:bn}.Marchenko_PC.gz` | Combined covariate matrix: known covariates + inferred `Hidden_Factor_PC*` rows, ready for xQTL mapping. |\n",
- "| `{phenoFile:bn}.{covFile:bn}.residual.bed.gz` | Intermediate residualized phenotype matrix used as input to PCA. |\n",
+ " ```\n",
+ " #id SAMPLE_001 SAMPLE_002 SAMPLE_003\n",
+ " sex 1 1 1\n",
+ " age 90.97 80.24 83.9\n",
+ " PC1 -0.4525795337766864 -0.04501122972985456 0.07044283190889761\n",
+ " PC2 0.04906817968399002 -0.1674973221595418 -0.0041635808141029\n",
+ " ...\n",
+ " Hidden_Factor_PC8 ... ... ...\n",
+ " ```\n",
"\n",
- "For PEER, additional diagnostic outputs (precision, residuals, weights, convergence plots) are produced."
+ "- `{cwd}/{name}.residual.bed.gz` - the residualised phenotype matrix produced by the shared first step and consumed by whichever method follows, written with a `.tbi` index.\n",
+ "- `{cwd}/{name}.residual.PEER_MODEL.hd5` - the fitted PEER model, from `PEER` only.\n",
+ "- `{cwd}/{name}.residual.PEER.gz` and `{cwd}/{name}.residual.PEER.diag.pdf` - the PEER factors and the convergence diagnostic plot.\n",
+ "- `{cwd}/{name}.residual.bed.fake.vcf.gz` - the indexed VCF `BiCV` builds for APEX, an intermediate rather than a result.\n",
+ "- `{cwd}/{name}.residual.BiCV.gz` - the BiCV factors."
]
},
{
@@ -68,11 +86,9 @@
"kernel": "SoS"
},
"source": [
- "### PCA with Marchenko-Pastur (recommended)\n",
+ "## Minimal Working Example\n",
"\n",
- "This is the method used for our main analyses. The `Marchenko_PC` workflow first regresses the molecular phenotype on the known covariates to get residuals, then runs PCA on the residual matrix and keeps the number of principal components whose eigenvalues exceed the Marchenko-Pastur random-matrix noise threshold. The retained components are written out as `Hidden_Factor_PC*` rows, stacked together with the known covariates so the result is ready to use directly in xQTL mapping. Use `--mean-impute-missing` to mean-impute any missing phenotype values.\n",
- "\n",
- "Timing: <1 min."
+ "Four alternative methods, each a chain that starts from the same residualisation step and then estimates hidden factors its own way. Pick one; they are not run in sequence."
]
},
{
@@ -81,7 +97,19 @@
"kernel": "SoS"
},
"source": [
- "## Steps\n"
+ "### PCA with Marchenko-Pastur (recommended)\n",
+ "\n",
+ "The default choice. `Marchenko_PC` first residualises the phenotype on the known covariates, runs PCA on the residual matrix, and keeps the components whose eigenvalues exceed the Marchenko-Pastur upper bound, which is the threshold random noise alone would produce. The kept components are stacked back onto the known covariates."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS",
+ "tags": []
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
]
},
{
@@ -107,9 +135,17 @@
"source": [
"### PEER (GTEx-style)\n",
"\n",
- "PEER infers hidden factors using a MOFA-based probabilistic model (Stegle et al.). The number of factors follows GTEx recommendations based on sample size, or can be fixed with `--N`. This method runs natively here (it uses the `mofapy2` Python package), so the example below is fully runnable on the toy data and produces the factor matrix plus a convergence-diagnostic PDF.\n",
- "\n",
- "Timing: ~1 min."
+ "PEER infers hidden factors using a MOFA-based probabilistic model (Stegle et al.). The number of factors follows GTEx recommendations based on sample size, or can be fixed with `--N`. This method runs natively here (it uses the `mofapy2` Python package), so the example below is fully runnable on the toy data and produces the factor matrix plus a convergence-diagnostic PDF.\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS",
+ "tags": []
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
]
},
{
@@ -135,9 +171,17 @@
"source": [
"### PCA with Buja & Eyuboglu permutation\n",
"\n",
- "This is the same PCA workflow as above (`PCA`), but the number of components to keep is chosen by the Buja & Eyuboglu permutation procedure instead of the Marchenko-Pastur threshold. Set `--choose_k_method Buja_Eyuboglu`. This route uses `jackstraw::permutationPA` (B=100 permutations), so it runs a little longer than the Marchenko variant but is fully runnable on the toy data.\n",
- "\n",
- "Timing: ~1 min."
+ "This is the same PCA workflow as above (`PCA`), but the number of components to keep is chosen by the Buja & Eyuboglu permutation procedure instead of the Marchenko-Pastur threshold. Set `--choose_k_method Buja_Eyuboglu`. This route uses `jackstraw::permutationPA` (B=100 permutations), so it runs a little longer than the Marchenko variant but is fully runnable on the toy data.\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS",
+ "tags": []
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
]
},
{
@@ -164,10 +208,17 @@
"source": [
"### BiCV factor analysis with APEX\n",
"\n",
- "BiCV chooses the number of hidden factors by bi-cross-validation using the APEX software (Owen et al. 2016). The pipeline computes residuals on the merged covariates, builds the inputs APEX expects (an indexed VCF placeholder and a phenotype matrix), then calls `apex factor`. The number of factors follows GTEx recommendations or can be fixed with `--N`.\n",
- "\n",
- "\n",
- "Timing: ~30 s."
+ "BiCV chooses the number of hidden factors by bi-cross-validation using the APEX software (Owen et al. 2016). The pipeline computes residuals on the merged covariates, builds the inputs APEX expects (an indexed VCF placeholder and a phenotype matrix), then calls `apex factor`. The number of factors follows GTEx recommendations or can be fixed with `--N`.\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS",
+ "tags": []
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
]
},
{
@@ -177,7 +228,13 @@
"kernel": "Bash"
},
"outputs": [],
- "source": "sos run pipeline/covariate_hidden_factor.ipynb BiCV \\\n --cwd output/covariate \\\n --phenoFile output/rnaseq/protocol_example.rnaseq.bed.bed.gz \\\n --covFile output/covariate/protocol_example.covariates.protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca.gz \\\n --N 3"
+ "source": [
+ "sos run pipeline/covariate_hidden_factor.ipynb BiCV \\\n",
+ " --cwd output/covariate \\\n",
+ " --phenoFile output/rnaseq/protocol_example.rnaseq.bed.bed.gz \\\n",
+ " --covFile output/covariate/protocol_example.covariates.protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca.gz \\\n",
+ " --N 3"
+ ]
},
{
"cell_type": "markdown",
@@ -195,88 +252,97 @@
},
{
"cell_type": "code",
- "execution_count": 2,
+ "execution_count": null,
"metadata": {
- "kernel": "Bash"
+ "kernel": "SoS"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/covariate_hidden_factor.ipynb -h"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "usage: sos run covariate_hidden_factor.ipynb\n",
- " [workflow_name | -t targets] [options] [workflow_options]\n",
- " workflow_name: Single or combined workflows defined in this script\n",
- " targets: One or more targets to generate\n",
- " options: Single-hyphen sos parameters (see \"sos run -h\" for details)\n",
- " workflow_options: Double-hyphen workflow-specific parameters\n",
- "\n",
- "Workflows:\n",
- " Marchenko_PC\n",
- " PEER\n",
- " BiCV\n",
- "\n",
- "Global Workflow Options:\n",
- " --cwd output (as path)\n",
- " The output directory for generated files. MUST BE FULL\n",
- " PATH\n",
- " --covFile VAL (as path, required)\n",
- " Merged Covariates File\n",
- " --phenoFile VAL (as path, required)\n",
- " Path to the input molecular phenotype data.\n",
- " --name f'{phenoFile:bnn}.{covFile:bn}'\n",
- "\n",
- " --job-size 1 (as int)\n",
- " For cluster jobs, number commands to run per job\n",
- " --walltime 5h\n",
- " Wall clock time expected\n",
- " --mem 16G\n",
- " Memory expected\n",
- " --numThreads 8 (as int)\n",
- " Number of threads\n",
- " --container ''\n",
- " Software container option\n",
- "\n",
- "\n",
- "Sections\n",
- " *_1:\n",
- " Workflow Options:\n",
- " --[no-]mean-impute-missing (default to False)\n",
- " Marchenko_PC_2:\n",
- " PEER_2:\n",
- " Workflow Options:\n",
- " --N 0 (as int)\n",
- " N PEER factors, If do not specify or specified as 0,\n",
- " default values suggested by GTEx (based on different\n",
- " sample size) will be used\n",
- " --iteration 1000 (as int)\n",
- " Default values from PEER software: The number of max\n",
- " iteration\n",
- " --tol 0.001 (as float)\n",
- " Prior parameters parameter: Alpha_a = 0.001 parameter:\n",
- " Alpha_b = 0.1 parameter: Eps_a = 0.1 parameter: Eps_b =\n",
- " 10.0 Tolarance parameters\n",
- " --[no-]r2-tol (default to False)\n",
- " parameter: var_tol = 0.00001 minimum variance explained\n",
- " criteria to drop factors while training\n",
- " --convergence-mode fast\n",
- " Convergence mode: Convergence mode for MOFAr \"slow\",\n",
- " \"medium\" or \"fast\", corresponding to 1e-5%, 1e-4% or\n",
- " 1e-3% deltaELBO change.\n",
- " PEER_3:\n",
- " BiCV_2:\n",
- " BiCV_3:\n",
- " Workflow Options:\n",
- " --N 0 (as int)\n",
- " N factors, if not specify, calculated based on sample\n",
- " size according to GTeX\n",
- " --iteration 10 (as int)\n",
- " The number of iteration\n"
- ]
- }
- ],
"source": [
- "sos run covariate_hidden_factor.ipynb -h"
+ "```\n",
+ "usage: sos run pipeline/covariate_hidden_factor.ipynb\n",
+ " [workflow_name | -t targets] [options] [workflow_options]\n",
+ " workflow_name: Single or combined workflows defined in this script\n",
+ " targets: One or more targets to generate\n",
+ " options: Single-hyphen sos parameters (see \"sos run -h\" for details)\n",
+ " workflow_options: Double-hyphen workflow-specific parameters\n",
+ "\n",
+ "Workflows:\n",
+ " Marchenko_PC\n",
+ " PCA\n",
+ " PEER\n",
+ " BiCV\n",
+ "\n",
+ "Global Workflow Options:\n",
+ " --modular-script-dir code/script (as path)\n",
+ " --cwd output (as path)\n",
+ " The output directory for generated files. MUST BE FULL\n",
+ " PATH\n",
+ " --covFile VAL (as path, required)\n",
+ " Merged Covariates File\n",
+ " --phenoFile VAL (as path, required)\n",
+ " Path to the input molecular phenotype data.\n",
+ " --name f'{phenoFile:bnn}.{covFile:bn}'\n",
+ "\n",
+ " --job-size 1 (as int)\n",
+ " For cluster jobs, number commands to run per job\n",
+ " --walltime 5h\n",
+ " Wall clock time expected\n",
+ " --mem 16G\n",
+ " Memory expected\n",
+ " --numThreads 8 (as int)\n",
+ " Number of threads\n",
+ " --container ''\n",
+ " Software container option\n",
+ " --entrypoint ''\n",
+ "\n",
+ "Sections\n",
+ " *_1:\n",
+ " Workflow Options:\n",
+ " --[no-]mean-impute-missing (default to False)\n",
+ " Marchenko_PC_2, PCA_2:\n",
+ " Workflow Options:\n",
+ " --choose-k-method Marchenko\n",
+ " Marchenko or Marchenko\n",
+ " --N 0 (as int)\n",
+ " PEER_2:\n",
+ " Workflow Options:\n",
+ " --N 0 (as int)\n",
+ " N PEER factors, If do not specify or specified as 0,\n",
+ " default values suggested by GTEx (based on different\n",
+ " sample size) will be used\n",
+ " --iteration 1000 (as int)\n",
+ " Default values from PEER software: The number of max\n",
+ " iteration\n",
+ " --tol 0.001 (as float)\n",
+ " Prior parameters parameter: Alpha_a = 0.001 parameter:\n",
+ " Alpha_b = 0.1 parameter: Eps_a = 0.1 parameter: Eps_b =\n",
+ " 10.0 Tolarance parameters\n",
+ " --[no-]r2-tol (default to False)\n",
+ " parameter: var_tol = 0.00001 minimum variance explained\n",
+ " criteria to drop factors while training\n",
+ " --convergence-mode fast\n",
+ " Convergence mode: Convergence mode for MOFAr \"slow\",\n",
+ " \"medium\" or \"fast\", corresponding to 1e-5%, 1e-4% or\n",
+ " 1e-3% deltaELBO change.\n",
+ " PEER_3:\n",
+ " BiCV_2:\n",
+ " BiCV_3:\n",
+ " Workflow Options:\n",
+ " --N 0 (as int)\n",
+ " N factors, if not specify, calculated based on sample\n",
+ " size according to GTeX\n",
+ " --iteration 10 (as int)\n",
+ " The number of iteration\n",
+ "```"
]
},
{
@@ -285,7 +351,7 @@
"kernel": "R"
},
"source": [
- "## Setup and global parameters"
+ "## Workflow implementation"
]
},
{
@@ -394,7 +460,43 @@
"kernel": "SoS"
},
"outputs": [],
- "source": "[PEER_2]\n# N PEER factors, If do not specify or specified as 0, default values suggested by \n# GTEx (based on different sample size) will be used\nparameter: N = 0\n# Default values from PEER software:\n## The number of max iteration\nparameter: iteration = 1000\n### Prior parameters\n#parameter: Alpha_a = 0.001\n#parameter: Alpha_b = 0.1\n#parameter: Eps_a = 0.1\n#parameter: Eps_b = 10.0\n# Tolarance parameters\nparameter: tol = 0.001\n#parameter: var_tol = 0.00001\n# minimum variance explained criteria to drop factors while training\nparameter: r2_tol = False\n# Convergence mode: Convergence mode for MOFAr \"slow\", \"medium\" or \"fast\", corresponding to 1e-5%, 1e-4% or 1e-3% deltaELBO change.\nparameter: convergence_mode = \"fast\"\n# input is the residual file from the first step\noutput: f'{cwd:a}/{_input:bnn}.PEER_MODEL.hd5'\ntask: trunk_workers = 1, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output[0]:bn}'\nbash: expand= \"${ }\", stderr = f'{_output[0]}.stderr', stdout = f'{_output[0]}.stdout', container = container, entrypoint = entrypoint\n python ${modular_script_dir}/data_preprocessing/covariate/covariate_hidden_factor_peer.py \\\n --resid-file \"${_input}\" \\\n --model-file \"${_output}\" \\\n --factors-out \"${cwd:a}/${_input:bnn}.PEER.factors.tsv\" \\\n --weights-out \"${cwd:a}/${_input:bnn}.PEER.weights.tsv\" \\\n --variance-out \"${cwd:a}/${_input:bnn}.PEER.variance.tsv\" \\\n --num-factor ${N} \\\n --iteration ${iteration} \\\n --convergence-mode \"${convergence_mode}\" \\\n --num-threads ${numThreads} \\\n --tol ${tol} \\\n --r2-tol ${r2_tol}\n"
+ "source": [
+ "[PEER_2]\n",
+ "# N PEER factors, If do not specify or specified as 0, default values suggested by \n",
+ "# GTEx (based on different sample size) will be used\n",
+ "parameter: N = 0\n",
+ "# Default values from PEER software:\n",
+ "## The number of max iteration\n",
+ "parameter: iteration = 1000\n",
+ "### Prior parameters\n",
+ "#parameter: Alpha_a = 0.001\n",
+ "#parameter: Alpha_b = 0.1\n",
+ "#parameter: Eps_a = 0.1\n",
+ "#parameter: Eps_b = 10.0\n",
+ "# Tolarance parameters\n",
+ "parameter: tol = 0.001\n",
+ "#parameter: var_tol = 0.00001\n",
+ "# minimum variance explained criteria to drop factors while training\n",
+ "parameter: r2_tol = False\n",
+ "# Convergence mode: Convergence mode for MOFAr \"slow\", \"medium\" or \"fast\", corresponding to 1e-5%, 1e-4% or 1e-3% deltaELBO change.\n",
+ "parameter: convergence_mode = \"fast\"\n",
+ "# input is the residual file from the first step\n",
+ "output: f'{cwd:a}/{_input:bnn}.PEER_MODEL.hd5'\n",
+ "task: trunk_workers = 1, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output[0]:bn}'\n",
+ "bash: expand= \"${ }\", stderr = f'{_output[0]}.stderr', stdout = f'{_output[0]}.stdout', container = container, entrypoint = entrypoint\n",
+ " python ${modular_script_dir}/data_preprocessing/covariate/covariate_hidden_factor_peer.py \\\n",
+ " --resid-file \"${_input}\" \\\n",
+ " --model-file \"${_output}\" \\\n",
+ " --factors-out \"${cwd:a}/${_input:bnn}.PEER.factors.tsv\" \\\n",
+ " --weights-out \"${cwd:a}/${_input:bnn}.PEER.weights.tsv\" \\\n",
+ " --variance-out \"${cwd:a}/${_input:bnn}.PEER.variance.tsv\" \\\n",
+ " --num-factor ${N} \\\n",
+ " --iteration ${iteration} \\\n",
+ " --convergence-mode \"${convergence_mode}\" \\\n",
+ " --num-threads ${numThreads} \\\n",
+ " --tol ${tol} \\\n",
+ " --r2-tol ${r2_tol}\n"
+ ]
},
{
"cell_type": "code",
@@ -436,9 +538,7 @@
"source": [
"### BiCV Factor Analysis with APEX\n",
"\n",
- "Factor anallysis using Bi-Cross validation with the APEX software package [[cf. Owen et al., Statistical Science, 2016](https://doi.org/10.1214/15-STS539)] [[cf. Quick et al., bioRxiv, 2020](https://doi.org/10.1101/2020.12.18.423490)]\n",
- "\n",
- "Notice that the command options are different from those on the APEX website documentation. The commands on the documentation page does not work (last updated September 2021). The commands below were constructed and tested by our team based on our understanding of the program, without input from APEX authors.\n"
+ "The commands below were constructed and tested against the maintainers' own reading of the program, without input from the APEX authors, so treat the parameter choices as a starting point rather than as endorsed defaults."
]
},
{
@@ -473,17 +573,33 @@
}
},
"outputs": [],
- "source": "[BiCV_3]\n# N factors, if not specify, calculated based on sample size according to GTeX\nparameter: N = 0\n# The number of iteration\nparameter: iteration = 10\ninput: output_from(1), output_from(2)\noutput: f'{cwd:a}/{_input[0]:bnn}.BiCV.gz'\ntask: trunk_workers = 1, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output[0]:bn}'\nbash: expand= \"${ }\", stderr = f'{_output[0]}.stderr', stdout = f'{_output[0]}.stdout', container = container, entrypoint = entrypoint\n n_factors_file=\"${_output[0]:nn}.n_factors.txt\"\n Rscript ${modular_script_dir}/data_preprocessing/covariate/covariate_hidden_factor.R \\\n --step BiCV_nfactors \\\n --residFile \"${_input[0]}\" \\\n --N ${N} \\\n --output \"$n_factors_file\"\n n_factors=$(cat \"$n_factors_file\")\n out_prefix=\"${_output[0]:nn}\"\n apex factor \\\n --out \"$out_prefix\" \\\n --iter ${iteration} \\\n --factors \"$n_factors\" \\\n --bed \"${_input[0]}\" \\\n --vcf \"${_input[1]}\" \\\n --threads ${numThreads} \\\n --cov \"${covFile}\"\n mv -f \"$out_prefix.cov.gz\" \"${_output[0]}\""
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
"source": [
- "## Anticipated Results\n",
- "\n",
- "The pipeline produces output files in the `output/` subdirectory named after the workflow step. Verify success by checking that output files exist and are non-empty. See the **Output** section above for the expected file names and formats."
+ "[BiCV_3]\n",
+ "# N factors, if not specify, calculated based on sample size according to GTeX\n",
+ "parameter: N = 0\n",
+ "# The number of iteration\n",
+ "parameter: iteration = 10\n",
+ "input: output_from(1), output_from(2)\n",
+ "output: f'{cwd:a}/{_input[0]:bnn}.BiCV.gz'\n",
+ "task: trunk_workers = 1, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output[0]:bn}'\n",
+ "bash: expand= \"${ }\", stderr = f'{_output[0]}.stderr', stdout = f'{_output[0]}.stdout', container = container, entrypoint = entrypoint\n",
+ " n_factors_file=\"${_output[0]:nn}.n_factors.txt\"\n",
+ " Rscript ${modular_script_dir}/data_preprocessing/covariate/covariate_hidden_factor.R \\\n",
+ " --step BiCV_nfactors \\\n",
+ " --residFile \"${_input[0]}\" \\\n",
+ " --N ${N} \\\n",
+ " --output \"$n_factors_file\"\n",
+ " n_factors=$(cat \"$n_factors_file\")\n",
+ " out_prefix=\"${_output[0]:nn}\"\n",
+ " apex factor \\\n",
+ " --out \"$out_prefix\" \\\n",
+ " --iter ${iteration} \\\n",
+ " --factors \"$n_factors\" \\\n",
+ " --bed \"${_input[0]}\" \\\n",
+ " --vcf \"${_input[1]}\" \\\n",
+ " --threads ${numThreads} \\\n",
+ " --cov \"${covFile}\"\n",
+ " mv -f \"$out_prefix.cov.gz\" \"${_output[0]}\""
]
}
],
diff --git a/code/SoS/data_preprocessing/covariate_preprocessing.ipynb b/code/SoS/data_preprocessing/covariate_preprocessing.ipynb
index ec1f72902..49835e9c9 100644
--- a/code/SoS/data_preprocessing/covariate_preprocessing.ipynb
+++ b/code/SoS/data_preprocessing/covariate_preprocessing.ipynb
@@ -6,8 +6,11 @@
"kernel": "SoS"
},
"source": [
- "# Covariate Data Preprocessing"
- ]
+ "# Covariate data preprocessing\n",
+ "\n",
+ "Combine known covariates with genotype principal components and optionally infer hidden factors for xQTL association testing.\n"
+ ],
+ "id": "25e9e86a"
},
{
"cell_type": "markdown",
@@ -15,12 +18,13 @@
"kernel": "SoS"
},
"source": [
- "## Description\n",
+ "#### Miniprotocol Timing\n",
"\n",
- "This notebook runs the full covariate preprocessing pipeline end-to-end on the toy dataset. It chains the two covariate worker notebooks: it first merges the user-supplied covariates with the genotype principal components, then computes residuals on the merged covariates and infers hidden factors. The result is a single covariate matrix (known covariates + genotype PCs + hidden factors) ready for QTL association testing.\n",
+ "This is the total duration for one complete toy-data route; module-specific timings appear on their respective pages.\n",
"\n",
- "Timing: < 3 minutes on the toy data."
- ]
+ "Timing: <3 min (on the toy dataset)\n"
+ ],
+ "id": "c9f10ef3"
},
{
"cell_type": "markdown",
@@ -28,15 +32,13 @@
"kernel": "SoS"
},
"source": [
- "## Input Files\n",
+ "## Overview\n",
+ "\n",
+ "This mini-protocol walks through construction of an association-ready covariate matrix. [`covariate_formatting.ipynb`](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/covariate/covariate_formatting.html) first merges sample covariates with genotype principal components. [`covariate_hidden_factor.ipynb`](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/covariate/covariate_hidden_factor.html) then residualizes the molecular phenotype against those observed covariates and optionally estimates latent factors using Marchenko\u2013Pastur selection, a user-specified PCA dimension, PEER, or bi-cross-validation (BiCV).\n",
"\n",
- "| File | Description |\n",
- "|------|-------------|\n",
- "| `input/covariate/protocol_example.covariates.base.tsv` | Known sample covariates (e.g. sex, age, study) |\n",
- "| `output/genotype/genotype_pca/protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca.rds` | Genotype PCA result (from genotype preprocessing) |\n",
- "| `output/genotype/genotype_pca/protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca.scree.txt` | Scree values used to choose the number of PCs to keep |\n",
- "| `output/rnaseq/protocol_example.rnaseq.bed.bed.gz` | Molecular phenotype matrix (from phenotype preprocessing) |"
- ]
+ "Run step 1 once, then choose one hidden-factor method from steps 2\u20135. These alternatives are not a four-step chain. Use the resulting matrix as the covariate input to association testing.\n"
+ ],
+ "id": "5d626526"
},
{
"cell_type": "markdown",
@@ -44,13 +46,21 @@
"kernel": "SoS"
},
"source": [
- "## Output Files\n",
+ "## Steps\n",
+ "\n",
+ "Choose a route before running commands; steps 2\u20135 are alternative hidden-factor methods.\n",
+ "\n",
+ "| **Analysis goal** | **Commands to run, in order** | **Inputs** |\n",
+ "| --- | --- | --- |\n",
+ "| Observed covariates and genotype PCs only | 1 | `input/covariate/protocol_example.covariates.base.tsv`; `output/genotype/genotype_pca/protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca.rds`; `output/genotype/genotype_pca/protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca.scree.txt` |\n",
+ "| Automatically select PCA hidden factors | 1 \u2192 2 | Step 1 inputs; `output/rnaseq/protocol_example.rnaseq.bed.bed.gz` |\n",
+ "| Use a fixed number of PEER factors | 1 \u2192 3 | Step 1 inputs; `output/rnaseq/protocol_example.rnaseq.bed.bed.gz` |\n",
+ "| Use PCA factors with configurable dimension selection | 1 \u2192 4 | Step 1 inputs; `output/rnaseq/protocol_example.rnaseq.bed.bed.gz` |\n",
+ "| Select hidden factors by bi-cross-validation | 1 \u2192 5 | Step 1 inputs; `output/rnaseq/protocol_example.rnaseq.bed.bed.gz` |\n",
"\n",
- "| File | Description |\n",
- "|------|-------------|\n",
- "| `output/covariate/protocol_example.covariates.protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca.gz` | Merged covariates + genotype PCs (output of step i) |\n",
- "| `output/covariate/...prune.pca.Marchenko_PC.gz` | Final covariate matrix with known covariates, genotype PCs, and inferred hidden factors, ready for TensorQTL |"
- ]
+ "Run only the row matching the intended analysis goal."
+ ],
+ "id": "e88d7b48"
},
{
"cell_type": "markdown",
@@ -58,8 +68,29 @@
"kernel": "SoS"
},
"source": [
- "## Steps"
- ]
+ "### 1. [Merge observed covariates and genotype PCs](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/covariate/covariate_formatting.html)\n",
+ "\n",
+ "**What it does:** Combine the base covariate table with the selected genotype principal components.\n"
+ ],
+ "id": "0c4ac9c6"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "kernel": "Bash"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/covariate_formatting.ipynb merge_genotype_pc \\\n",
+ " --cwd output/covariate \\\n",
+ " --pcaFile output/genotype/genotype_pca/protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca.rds \\\n",
+ " --covFile input/covariate/protocol_example.covariates.base.tsv \\\n",
+ " --name protocol_example.covariates.protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca \\\n",
+ " --tol-cov 0.4 \\\n",
+ " --k `awk '$3 < 0.8' output/genotype/genotype_pca/protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca.scree.txt | tail -1 | cut -f 1`\n"
+ ],
+ "id": "f5e2ad67"
},
{
"cell_type": "markdown",
@@ -67,10 +98,11 @@
"kernel": "SoS"
},
"source": [
- "### Merge covariates and genotype PCs\n",
+ "### 2. [Infer PCA factors with Marchenko\u2013Pastur selection](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/covariate/covariate_hidden_factor.html)\n",
"\n",
- "Merge the known covariates with the genotype PCs, keeping only PCs that explain a meaningful fraction of variance. The number of PCs to keep is read from the scree file (here, components with the scree statistic below 0.8). This writes the merged covariate file used as input to the hidden-factor step."
- ]
+ "**What it does:** Residualize the phenotype and automatically retain PCA factors above the Marchenko\u2013Pastur noise threshold.\n"
+ ],
+ "id": "b0523bf6"
},
{
"cell_type": "code",
@@ -80,14 +112,13 @@
},
"outputs": [],
"source": [
- "sos run pipeline/covariate_formatting.ipynb merge_genotype_pc \\\n",
- " --cwd output/covariate/ \\\n",
- " --pcaFile output/genotype/genotype_pca/protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca.rds \\\n",
- " --covFile input/covariate/protocol_example.covariates.base.tsv \\\n",
- " --name protocol_example.covariates.protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca \\\n",
- " --tol-cov 0.4 \\\n",
- " --k `awk '$3 < 0.8' output/genotype/genotype_pca/protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca.scree.txt | tail -1 | cut -f 1`"
- ]
+ "sos run pipeline/covariate_hidden_factor.ipynb Marchenko_PC \\\n",
+ " --cwd output/covariate \\\n",
+ " --phenoFile output/rnaseq/protocol_example.rnaseq.bed.bed.gz \\\n",
+ " --covFile output/covariate/protocol_example.covariates.protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca.gz \\\n",
+ " --mean-impute-missing\n"
+ ],
+ "id": "a10fb6ad"
},
{
"cell_type": "markdown",
@@ -95,10 +126,27 @@
"kernel": "SoS"
},
"source": [
- "### Compute residuals and infer hidden factors\n",
+ "### 3. [Infer PEER factors](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/covariate/covariate_hidden_factor.html)\n",
"\n",
- "Using the merged covariate file and the molecular phenotype matrix, compute residuals on the known covariates and infer hidden factors with the Marchenko-Pastur PCA method. `--mean-impute-missing` fills any missing covariate values with the column mean. The output is the final covariate matrix (known covariates + genotype PCs + hidden factors)."
- ]
+ "**What it does:** Residualize the phenotype and estimate the requested number of probabilistic PEER factors.\n"
+ ],
+ "id": "89e6dabb"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "kernel": "Bash"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/covariate_hidden_factor.ipynb PEER \\\n",
+ " --cwd output/covariate \\\n",
+ " --phenoFile output/rnaseq/protocol_example.rnaseq.bed.bed.gz \\\n",
+ " --covFile output/covariate/protocol_example.covariates.protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca.gz \\\n",
+ " --N 3\n"
+ ],
+ "id": "afa6aaa5"
},
{
"cell_type": "markdown",
@@ -106,10 +154,11 @@
"kernel": "SoS"
},
"source": [
- "## Anticipated Results\n",
+ "### 4. [Infer configurable PCA factors](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/covariate/covariate_hidden_factor.html)\n",
"\n",
- "The pipeline produces output files in the `output/` subdirectory named after the workflow step. Verify success by checking that output files exist and are non-empty. See the **Output** section above for the expected file names and formats."
- ]
+ "**What it does:** Residualize the phenotype and estimate PCA factors using the selected dimension rule.\n"
+ ],
+ "id": "0fa9e389"
},
{
"cell_type": "code",
@@ -119,12 +168,99 @@
},
"outputs": [],
"source": [
- "sos run pipeline/covariate_hidden_factor.ipynb Marchenko_PC \\\n",
+ "sos run pipeline/covariate_hidden_factor.ipynb PCA \\\n",
+ " --cwd output/covariate \\\n",
+ " --phenoFile output/rnaseq/protocol_example.rnaseq.bed.bed.gz \\\n",
+ " --covFile output/covariate/protocol_example.covariates.protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca.gz \\\n",
+ " --choose_k_method Marchenko \\\n",
+ " --mean-impute-missing\n"
+ ],
+ "id": "60317089"
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### 5. [Infer factors with bi-cross-validation](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/covariate/covariate_hidden_factor.html)\n",
+ "\n",
+ "**What it does:** Residualize the phenotype and select latent structure using the BiCV workflow.\n"
+ ],
+ "id": "c5d0f178"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "kernel": "Bash"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/covariate_hidden_factor.ipynb BiCV \\\n",
" --cwd output/covariate \\\n",
" --phenoFile output/rnaseq/protocol_example.rnaseq.bed.bed.gz \\\n",
" --covFile output/covariate/protocol_example.covariates.protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca.gz \\\n",
- " --mean-impute-missing"
- ]
+ " --N 3\n"
+ ],
+ "id": "a104f40d"
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "## Output Files\n",
+ "\n",
+ "| Route or step | Products and relative paths |\n",
+ "| --- | --- |\n",
+ "| Step 1 | `output/covariate/protocol_example.covariates.protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca.gz` |\n",
+ "| Step 2 | `output/covariate/protocol_example.rnaseq.Marchenko_PC.gz` |\n",
+ "| Step 3 | `output/covariate/protocol_example.rnaseq.bed.PEER.gz`; `output/covariate/protocol_example.rnaseq.bed.PEER.diag.pdf`; `output/covariate/protocol_example.rnaseq.bed.PEER_MODEL.hd5` |\n",
+ "| Step 4 | `output/covariate/protocol_example.rnaseq.Marchenko_PC.gz` when `--choose_k_method Marchenko` is used |\n",
+ "| Step 5 | `output/covariate/protocol_example.rnaseq.bed.BiCV.gz` |"
+ ],
+ "id": "5bd240a3"
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "## Anticipated Results\n",
+ "\n",
+ "Step 1 produces a sample-by-covariate matrix containing the observed covariates and selected genotype principal components. A selected hidden-factor route adds latent factors estimated after residualizing the molecular phenotype against those observed covariates.\n",
+ "\n",
+ "Use exactly one final matrix in association testing; do not concatenate results from alternative hidden-factor methods.\n"
+ ],
+ "id": "eefe4f67"
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "## Command interface\n",
+ "\n",
+ "List the workflows and parameters available in each module used by this mini-protocol.\n"
+ ],
+ "id": "af8ffbf0"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "kernel": "Bash"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/covariate_formatting.ipynb -h\n",
+ "sos run pipeline/covariate_hidden_factor.ipynb -h\n"
+ ],
+ "id": "cc7345e3"
}
],
"metadata": {
@@ -162,5 +298,5 @@
}
},
"nbformat": 4,
- "nbformat_minor": 4
-}
+ "nbformat_minor": 5
+}
\ No newline at end of file
diff --git a/code/SoS/data_preprocessing/genotype/GRM.ipynb b/code/SoS/data_preprocessing/genotype/GRM.ipynb
index d55068b62..25eb5f56e 100644
--- a/code/SoS/data_preprocessing/genotype/GRM.ipynb
+++ b/code/SoS/data_preprocessing/genotype/GRM.ipynb
@@ -20,30 +20,38 @@
"kernel": "SoS"
},
"source": [
- "## Description"
+ "## Overview\n",
+ "\n",
+ "A genomic relationship matrix summarises how genetically similar every pair of samples is, and association models use it to absorb that relatedness rather than mistake it for signal. Computing it from all chromosomes at once creates a problem: the variant being tested contributes to the matrix that is supposed to control for background relatedness, which deflates the very association being measured.\n",
+ "\n",
+ "The leave-one-chromosome-out construction avoids that. For each chromosome the GRM is built from every *other* chromosome, so the variants under test never appear in the matrix correcting them. `GCTA` does the computation, and a final step reformats the result for APEX.\n",
+ "\n",
+ "Calling the `grm` workflow runs four numbered steps in sequence: write the leave-one-out file list per chromosome, compute the GRM with GCTA, format it for APEX, then collect the per-chromosome results into one list.\n",
+ "\n",
+ "**When to run it.** Once per genotype set, before any association scan that takes a GRM."
]
},
{
+ "attachments": {},
"cell_type": "markdown",
"metadata": {
- "kernel": "SoS"
+ "kernel": "SoS",
+ "tags": []
},
"source": [
- "For each chromosome, we compute a GRM using data excluding this chromosome. Computation is implemented using `GCTA` software package.\n",
+ "## Input\n",
"\n",
- "Running the `grm` pipeline executes **four steps** in sequence:\n",
+ "- `--genoFile`: a two-column, tab-separated list with one row per chromosome, giving the chromosome number and the path to that chromosome's PLINK 1.0 `.bed`. Its `.bim` and `.fam` must sit beside each `.bed`. Common variants, MAF above 1%, are recommended. Example `input/genotype/protocol_example.genotype.list`, 3 rows:\n",
"\n",
- "1. **Generate LOCO file list** \u2014 for each chromosome, write a list of the per-chromosome genotype files of all *other* chromosomes (leave-one-chromosome-out).\n",
- "2. **Compute LOCO-GRM** \u2014 run `GCTA` on that leave-one-out set to produce the GRM for the held-out chromosome.\n",
- "3. **Format output for APEX** \u2014 reshape each GRM into the APEX-compatible kinship table.\n",
- "4. **Generate APEX input list** \u2014 write a master list mapping each chromosome to its APEX-formatted LOCO GRM."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "**Timing:** ~5-10 min on typical compute infrastructure."
+ " ```\n",
+ " 20 output/plink/protocol_example.genotype.chr20.bed\n",
+ " 21 output/plink/protocol_example.genotype.chr21.bed\n",
+ " 22 output/plink/protocol_example.genotype.chr22.bed\n",
+ " ```\n",
+ "\n",
+ "- `--cwd`: the directory outputs are written to.\n",
+ "- `--numThreads`: threads handed to GCTA, `20` by default.\n",
+ "- `--job-size`, `--walltime` and `--mem`: cluster job resources."
]
},
{
@@ -54,40 +62,45 @@
"tags": []
},
"source": [
- "## Input Files\n",
+ "## Output\n",
+ "\n",
+ "- `{cwd}/{name}.chr.loco.txt` - for chromosome N, the list of every *other* chromosome's genotype file. This is what makes it leave-one-out. Example `output/grm_uf/protocol_example.genotype.chr20.loco.txt`, 2 lines:\n",
+ "\n",
+ " ```\n",
+ " output/plink/protocol_example.genotype.chr21\n",
+ " output/plink/protocol_example.genotype.chr22\n",
+ " ```\n",
+ "\n",
+ "- `{cwd}/{name}.chr.loco.grm.gz` - the GRM for chromosome N computed by GCTA from all other chromosomes, with `.grm.id` naming the samples and `.log` recording the run.\n",
+ "- `{cwd}/{name}.chr.loco.apex.grm` - the same matrix reformatted for APEX.\n",
+ "- `{cwd}/{name}.loco_grm_list.txt` - the per-chromosome results collected into one table, `#chr` and the GRM directory. Example:\n",
"\n",
- "| File | Description |\n",
- "|------|-------------|\n",
- "| `input/genotype/protocol_example.genotype.list` | Two-column, tab-separated list: chromosome number and the path to that chromosome's PLINK 1.0 `.bed` file. One row per chromosome. |\n",
- "| `output/plink/protocol_example.genotype.chr.{bed,bim,fam}` | Per-chromosome PLINK 1.0 genotype files referenced by the list above. Common variants (MAF > 1%) are recommended for GRM. |"
+ " ```\n",
+ " #chr\tdir\n",
+ " chr20chr21chr22\toutput/grm_uf/protocol_example.genotype.chr20.apex.grm\n",
+ " chr20chr21chr22\toutput/grm_uf/protocol_example.genotype.chr21.apex.grm\n",
+ " ```"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {
- "kernel": "SoS",
- "tags": []
+ "kernel": "SoS"
},
"source": [
- "## Output Files\n",
- "\n",
- "| File | Description |\n",
- "|------|-------------|\n",
- "| `output/grm_uf/protocol_example.genotype.chr.loco.txt` | Per-chromosome leave-one-chromosome-out file list (all chromosomes except chr N). |\n",
- "| `output/grm_uf/protocol_example.genotype.chr.loco.grm.gz` | LOCO GRM for chr N, computed by GCTA from all other chromosomes. |\n",
- "| `output/grm_uf/protocol_example.genotype.chr.loco.apex.grm` | LOCO GRM reformatted for APEX (LMM) input. |\n",
- "| `output/grm_uf/protocol_example.genotype.loco_grm_list.txt` | Master list mapping each chromosome to its APEX-formatted LOCO GRM. |"
+ "## Minimal Working Example"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {
- "kernel": "SoS"
+ "kernel": "SoS",
+ "tags": []
},
"source": [
- "## Minimal Working Example"
+ "**Timing**: TBD (on toy dataset)"
]
},
{
@@ -98,7 +111,9 @@
},
"outputs": [],
"source": [
- "sos run pipeline/GRM.ipynb grm \\\n --cwd output/grm_uf \\\n --genoFile input/genotype/protocol_example.genotype.list"
+ "sos run pipeline/GRM.ipynb grm \\\n",
+ " --cwd output/grm_uf \\\n",
+ " --genoFile input/genotype/protocol_example.genotype.list"
]
},
{
@@ -108,7 +123,7 @@
"kernel": "SoS"
},
"source": [
- "## Command interface"
+ "## Command Interface"
]
},
{
@@ -123,24 +138,54 @@
]
},
{
- "cell_type": "code",
- "execution_count": null,
+ "cell_type": "markdown",
"metadata": {
"kernel": "SoS"
},
- "outputs": [],
"source": [
- "[global]\n# Work directory & output directory\nparameter: cwd = path(\"output\")\n# The filename name for output data\nparameter: modular_script_dir = path('code/script')\n# For cluster jobs, number commands to run per job\nparameter: job_size = 1\n# Wall clock time expected\nparameter: walltime = \"5h\"\n# Memory expected\nparameter: mem = \"16G\"\n# Number of threads\nparameter: numThreads = 20\nparameter: genoFile = paths\ncwd = f\"{cwd:a}\"\n\n## Code below are copied from genotype_formatting.ipynb. Change should be made to codes in that document and sync with this chunk.\nfrom sos.utils import expand_size\nimport os\n\ndef get_genotype_file(geno_file_paths):\n #\n def valid_geno_file(x):\n suffixes = path(x).suffixes\n if suffixes[-1] == '.bed':\n return True\n if len(suffixes)>1 and ''.join(suffixes[-2:]) == \".vcf.gz\":\n return True\n return False\n #\n def complete_geno_path(x, geno_file):\n if not valid_geno_file(x):\n raise ValueError(f\"Genotype file {x} should be VCF (end with .vcf.gz) or PLINK bed file (end with .bed)\")\n if not os.path.isfile(x):\n # relative path\n if not os.path.isfile(f'{geno_file:ad}/' + x):\n raise ValueError(f\"Cannot find genotype file {x}\")\n else:\n x = f'{geno_file:ad}/' + x\n return x\n # \n def format_chrom(chrom):\n if chrom.startswith('chr'):\n chrom = chrom[3:]\n return chrom\n # Inputs are either VCF or bed, or a vector of them \n if len(geno_file_paths) > 1:\n if all([valid_geno_file(x) for x in geno_file_paths]):\n return paths(geno_file_paths)\n else: \n raise ValueError(f\"Invalid input {geno_file_paths}\")\n # Input is one genotype file or text list of genotype files\n geno_file = geno_file_paths[0]\n if valid_geno_file(geno_file):\n return path(geno_file)\n else: \n units = [x.strip().split() for x in open(geno_file).readlines() if x.strip() and not x.strip().startswith('#')]\n if all([len(x) == 1 for x in units]):\n return paths([complete_geno_path(x[0], geno_file) for x in units])\n elif all([len(x) == 2 for x in units]):\n genos = dict([(format_chrom(x[0]), path(complete_geno_path(x[1], geno_file))) for x in units])\n else:\n raise ValueError(f\"{geno_file} should contain one column of file names, or two columns of chrom number and corresponding file name\")\n return genos\n \ngenotypes = get_genotype_file(genoFile)"
+ "```\n",
+ "\u001b[91mERROR\u001b[0m: \u001b[91mNotebook JSON is invalid: %s\u001b[0m\n",
+ "usage: sos run code/SoS/data_preprocessing/genotype/GRM.ipynb\n",
+ " [workflow_name | -t targets] [options] [workflow_options]\n",
+ " workflow_name: Single or combined workflows defined in this script\n",
+ " targets: One or more targets to generate\n",
+ " options: Single-hyphen sos parameters (see \"sos run -h\" for details)\n",
+ " workflow_options: Double-hyphen workflow-specific parameters\n",
+ "\n",
+ "Workflows:\n",
+ " grm\n",
+ "\n",
+ "Global Workflow Options:\n",
+ " --cwd output (as path)\n",
+ " Work directory & output directory\n",
+ " --modular-script-dir code/script (as path)\n",
+ " The filename name for output data\n",
+ " --job-size 1 (as int)\n",
+ " For cluster jobs, number commands to run per job\n",
+ " --walltime 5h\n",
+ " Wall clock time expected\n",
+ " --mem 16G\n",
+ " Memory expected\n",
+ " --numThreads 20 (as int)\n",
+ " Number of threads\n",
+ " --genoFile paths\n",
+ "\n",
+ "\n",
+ "Sections\n",
+ " grm_1: Generate LOCO file list\n",
+ " grm_2: Compute LOCO-GRM\n",
+ " grm_3: Format output for APEX\n",
+ " grm_4: Generate APEX input list\n",
+ "```"
]
},
{
- "attachments": {},
"cell_type": "markdown",
"metadata": {
"kernel": "SoS"
},
"source": [
- "### **Step 1.** Generate LOCO file list"
+ "## Workflow implementation"
]
},
{
@@ -151,7 +196,72 @@
},
"outputs": [],
"source": [
- "# Generate LOCO file list\n[grm_1]\nchrom = list(genotypes.keys())\ninput: for_each = 'chrom'\noutput: f'{cwd}/{genotypes[_chrom]:bn}.loco.txt'\nwith open(_output, 'w') as f:\n f.write('\\n'.join([str(genotypes[x].with_suffix('')) for x in genotypes if x != _chrom]))"
+ "[global]\n",
+ "# Work directory & output directory\n",
+ "parameter: cwd = path(\"output\")\n",
+ "# The filename name for output data\n",
+ "parameter: modular_script_dir = path('code/script')\n",
+ "# For cluster jobs, number commands to run per job\n",
+ "parameter: job_size = 1\n",
+ "# Wall clock time expected\n",
+ "parameter: walltime = \"5h\"\n",
+ "# Memory expected\n",
+ "parameter: mem = \"16G\"\n",
+ "# Number of threads\n",
+ "parameter: numThreads = 20\n",
+ "parameter: genoFile = paths\n",
+ "cwd = f\"{cwd:a}\"\n",
+ "\n",
+ "## Code below are copied from genotype_formatting.ipynb. Change should be made to codes in that document and sync with this chunk.\n",
+ "from sos.utils import expand_size\n",
+ "import os\n",
+ "\n",
+ "def get_genotype_file(geno_file_paths):\n",
+ " #\n",
+ " def valid_geno_file(x):\n",
+ " suffixes = path(x).suffixes\n",
+ " if suffixes[-1] == '.bed':\n",
+ " return True\n",
+ " if len(suffixes)>1 and ''.join(suffixes[-2:]) == \".vcf.gz\":\n",
+ " return True\n",
+ " return False\n",
+ " #\n",
+ " def complete_geno_path(x, geno_file):\n",
+ " if not valid_geno_file(x):\n",
+ " raise ValueError(f\"Genotype file {x} should be VCF (end with .vcf.gz) or PLINK bed file (end with .bed)\")\n",
+ " if not os.path.isfile(x):\n",
+ " # relative path\n",
+ " if not os.path.isfile(f'{geno_file:ad}/' + x):\n",
+ " raise ValueError(f\"Cannot find genotype file {x}\")\n",
+ " else:\n",
+ " x = f'{geno_file:ad}/' + x\n",
+ " return x\n",
+ " # \n",
+ " def format_chrom(chrom):\n",
+ " if chrom.startswith('chr'):\n",
+ " chrom = chrom[3:]\n",
+ " return chrom\n",
+ " # Inputs are either VCF or bed, or a vector of them \n",
+ " if len(geno_file_paths) > 1:\n",
+ " if all([valid_geno_file(x) for x in geno_file_paths]):\n",
+ " return paths(geno_file_paths)\n",
+ " else: \n",
+ " raise ValueError(f\"Invalid input {geno_file_paths}\")\n",
+ " # Input is one genotype file or text list of genotype files\n",
+ " geno_file = geno_file_paths[0]\n",
+ " if valid_geno_file(geno_file):\n",
+ " return path(geno_file)\n",
+ " else: \n",
+ " units = [x.strip().split() for x in open(geno_file).readlines() if x.strip() and not x.strip().startswith('#')]\n",
+ " if all([len(x) == 1 for x in units]):\n",
+ " return paths([complete_geno_path(x[0], geno_file) for x in units])\n",
+ " elif all([len(x) == 2 for x in units]):\n",
+ " genos = dict([(format_chrom(x[0]), path(complete_geno_path(x[1], geno_file))) for x in units])\n",
+ " else:\n",
+ " raise ValueError(f\"{geno_file} should contain one column of file names, or two columns of chrom number and corresponding file name\")\n",
+ " return genos\n",
+ " \n",
+ "genotypes = get_genotype_file(genoFile)"
]
},
{
@@ -161,7 +271,7 @@
"kernel": "SoS"
},
"source": [
- "### **Step 2.** Compute LOCO-GRM"
+ "#### Generate LOCO file list"
]
},
{
@@ -172,7 +282,13 @@
},
"outputs": [],
"source": [
- "# Compute LOCO-GRM\n[grm_2]\noutput: f'{_input:nn}.grm.gz'\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'\nbash: expand= \"$[ ]\", stderr = f'{_output}.stderr', stdout = f'{_output}.stdout'\n gcta \\\n --mbfile $[_input] \\\n --make-grm-gz \\\n --out $[_output:nn]\n"
+ "# Generate LOCO file list\n",
+ "[grm_1]\n",
+ "chrom = list(genotypes.keys())\n",
+ "input: for_each = 'chrom'\n",
+ "output: f'{cwd}/{genotypes[_chrom]:bn}.loco.txt'\n",
+ "with open(_output, 'w') as f:\n",
+ " f.write('\\n'.join([str(genotypes[x].with_suffix('')) for x in genotypes if x != _chrom]))"
]
},
{
@@ -182,7 +298,7 @@
"kernel": "SoS"
},
"source": [
- "### **Step 3.** Format output for APEX"
+ "#### Compute LOCO-GRM"
]
},
{
@@ -193,7 +309,15 @@
},
"outputs": [],
"source": [
- "# Format output for APEX\n[grm_3]\noutput: f'{_input:nn}.apex.grm'\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'\nbash: expand= \"$[ ]\", stderr = f'{_output}.stderr', stdout = f'{_output}.stdout'\n Rscript $[modular_script_dir:a]/data_preprocessing/genotype/GRM.R --step format_apex \\\n --grm $[_input] \\\n --id $[_input:n].id \\\n --output $[_output]\n"
+ "# Compute LOCO-GRM\n",
+ "[grm_2]\n",
+ "output: f'{_input:nn}.grm.gz'\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'\n",
+ "bash: expand= \"$[ ]\", stderr = f'{_output}.stderr', stdout = f'{_output}.stdout'\n",
+ " gcta \\\n",
+ " --mbfile $[_input] \\\n",
+ " --make-grm-gz \\\n",
+ " --out $[_output:nn]\n"
]
},
{
@@ -203,14 +327,36 @@
"kernel": "SoS"
},
"source": [
- "### **Step 4.** Generate APEX input list"
+ "#### Format output for APEX"
]
},
{
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "outputs": [],
+ "source": [
+ "# Format output for APEX\n",
+ "[grm_3]\n",
+ "output: f'{_input:nn}.apex.grm'\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'\n",
+ "bash: expand= \"$[ ]\", stderr = f'{_output}.stderr', stdout = f'{_output}.stdout'\n",
+ " Rscript $[modular_script_dir:a]/data_preprocessing/genotype/GRM.R --step format_apex \\\n",
+ " --grm $[_input] \\\n",
+ " --id $[_input:n].id \\\n",
+ " --output $[_output]\n"
+ ]
+ },
+ {
+ "attachments": {},
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "kernel": "SoS"
+ },
"source": [
- "## Anticipated Results\n\nThe pipeline produces output files in the `output/` subdirectory named after the workflow step. Verify success by checking that output files exist and are non-empty. See the **Output** section above for the expected file names and formats."
+ "#### Generate APEX input list"
]
},
{
@@ -221,7 +367,15 @@
},
"outputs": [],
"source": [
- "# Generate APEX input list\n[grm_4]\ninput: group_by = \"all\"\noutput: f'{cwd}/{path(list(genotypes.values())[1]):bnn}.loco_grm_list.txt'\nbash: expand= \"$[ ]\", stderr = f'{_output}.stderr', stdout = f'{_output}.stdout'\n Rscript $[modular_script_dir:a]/data_preprocessing/genotype/GRM.R --step apex_list \\\n --input $[_input:r,] \\\n --chroms $[\" \".join(genotypes.keys())] \\\n --output $[_output]\n"
+ "# Generate APEX input list\n",
+ "[grm_4]\n",
+ "input: group_by = \"all\"\n",
+ "output: f'{cwd}/{path(list(genotypes.values())[1]):bnn}.loco_grm_list.txt'\n",
+ "bash: expand= \"$[ ]\", stderr = f'{_output}.stderr', stdout = f'{_output}.stdout'\n",
+ " Rscript $[modular_script_dir:a]/data_preprocessing/genotype/GRM.R --step apex_list \\\n",
+ " --input $[_input:r,] \\\n",
+ " --chroms $[\" \".join(genotypes.keys())] \\\n",
+ " --output $[_output]\n"
]
}
],
diff --git a/code/SoS/data_preprocessing/genotype/GWAS_QC.ipynb b/code/SoS/data_preprocessing/genotype/GWAS_QC.ipynb
index b7af43e26..50c01cfaa 100644
--- a/code/SoS/data_preprocessing/genotype/GWAS_QC.ipynb
+++ b/code/SoS/data_preprocessing/genotype/GWAS_QC.ipynb
@@ -6,7 +6,9 @@
"kernel": "SoS"
},
"source": [
- "# Genotype PLINK File Quality Control\n\nThis workflow implements some preliminary data QC steps for PLINK input files. It supports both PLINK1 binary format (BED/BIM/FAM) and PLINK2 format (PGEN/PVAR/PSAM). VCF format of inputs will be converted to PLINK before performing QC."
+ "# Genotype PLINK File Quality Control\n",
+ "\n",
+ "Runs kinship estimation, variant- and sample-level filtering, and LD pruning over a merged PLINK genotype set, producing the QC-ed genotypes and the pruned variant list used for PCA."
]
},
{
@@ -15,7 +17,32 @@
"kernel": "SoS"
},
"source": [
- "## Description"
+ "## Overview\n",
+ "\n",
+ "This module is the standard quality-control pass over a merged PLINK genotype set. It estimates kinship to identify related individuals, filters variants and samples on allele frequency, missingness and Hardy-Weinberg equilibrium, and LD-prunes what survives into the variant set used for PCA. Input may be PLINK1 (`bed/bim/fam`) or PLINK2 (`pgen/pvar/psam`); VCF has to be converted first by `genotype_formatting`.\n",
+ "\n",
+ "Four workflows are available, and how they are combined depends on whether the cohort contains relatives.\n",
+ "\n",
+ "- `king` estimates kinship with KING, flags pairs above `--kinship`, and splits the cohort into an `*.unrelated` and a `*.related` PLINK set. When no pair passes the threshold it writes an empty related-ID list and stops without splitting.\n",
+ "- `qc` applies the variant- and sample-level filters and then LD-prunes, writing both the filtered set and the `.prune.in` variant list.\n",
+ "- `qc_no_prune` is that same filtering step without the pruning stage, which is what to run when the pruned variant list already exists and only has to be extracted.\n",
+ "- `genotype_phenotype_sample_overlap` intersects the genotype sample list with the samples in a molecular phenotype file. A two-column `--sample-participant-lookup` (`genotype_id`, `sample_id`) translates between naming schemes; without one, the names are assumed to match already.\n",
+ "\n",
+ "The usual sequence is `qc_no_prune` for basic filtering, `genotype_phenotype_sample_overlap` to restrict to samples that have phenotype data, `king` to detect relatives, and finally `qc` on the unrelated set to produce the pruned variants for PCA. If KING finds no relatives there is nothing to split, and `qc` runs on the full filtered set with `--keep-samples` instead.\n",
+ "\n",
+ "The defaults follow Table 1 of [this GWAS quality-control tutorial](https://dx.doi.org/10.1002%2Fmpr.1608) and are deliberately permissive, because the appropriate thresholds depend on the analysis that follows:\n",
+ "\n",
+ "- kinship coefficient 0.0625, which is third-degree relatives and closer\n",
+ "- MAF and MAC both 0, so common and rare variants are kept\n",
+ "- variant-level and sample-level missingness both 0.1\n",
+ "- HWE 1e-15, which is very lenient\n",
+ "- LD pruning over a 50-variant window, shifting 10 variants at a time, at r2 0.1\n",
+ "\n",
+ "For PCA a MAF cutoff of 0.01 is the usual choice, since [common variants are the appropriate basis](https://bmcgenomdata.biomedcentral.com/articles/10.1186/s12863-020-0833-x); for single-variant association a MAC floor of 5 is typical.\n",
+ "\n",
+ "One constraint is structural: because both sample- and variant-level QC are applied together, all samples and chromosomes have to be merged into a single file first. That has been run at the scale of 200K exomes and 15 million variants on one merged PLINK set.\n",
+ "\n",
+ "**When to run it.** After `genotype_formatting` has merged the per-chromosome genotypes, and before PCA and any association scan. The sample-overlap workflow additionally needs the molecular phenotype file to exist."
]
},
{
@@ -24,7 +51,53 @@
"kernel": "SoS"
},
"source": [
- "This notebook includes workflow for\n\n- Compute kinship matrix in sample and estimate related individuals\n- Genotype and sample QC: by MAF, missing data and HWE\n- LD pruning for follow up PCA analysis on genotype, as needed\n\nA potential limitation is that the workflow requires all samples and chromosomes to be merged as one single file, in order to perform both sample and variant level QC. However, in our experience using this pipeline with 200K exomes with 15 million variants, this pipeline works on the single merged PLINK file."
+ "## Input\n",
+ "\n",
+ "* `--genoFile` -- the merged genotype set, PLINK1 `bed/bim/fam` or PLINK2 `pgen/pvar/psam`, e.g. `output/genotype_formatting/plink/protocol_example.genotype.merged.bed`. `king` takes the QC-ed `.bed`, while `genotype_phenotype_sample_overlap` takes the `.fam` instead. Samples are identified by the FID and IID columns:\n",
+ "\n",
+ " ```\n",
+ " 0\tSAMPLE_001\t0\t0\t0\t-9\n",
+ " 0\tSAMPLE_002\t0\t0\t0\t-9\n",
+ " 0\tSAMPLE_003\t0\t0\t0\t-9\n",
+ " ```\n",
+ "\n",
+ "* `--phenoFile` -- the molecular phenotype whose samples are matched against the genotype, `bed.gz` or tsv, e.g. `input/rnaseq/protocol_example.rnaseq.bed.gz`. Sample IDs are the column names after the four positional columns:\n",
+ "\n",
+ " ```\n",
+ " #chr\tstart\tend\tID\tSAMPLE_001\tSAMPLE_002\tSAMPLE_003\tSAMPLE_004\tSAMPLE_005\tSAMPLE_006\tSAMP\n",
+ " ```\n",
+ "\n",
+ "* `--name` -- string identifying the run; it is inserted into every output file name.\n",
+ "* `--cwd` -- output directory, default `output`.\n",
+ "\n",
+ "Restricting samples and variants:\n",
+ "\n",
+ "* `--keep-samples` / `--remove-samples` -- FID/IID lists limiting or excluding samples; `--keep-samples` is how the QC step is confined to, say, one ancestry group, or to the samples that have phenotype data.\n",
+ "* `--keep-variants` / `--exclude-variants` -- variant ID lists. Supplying `--keep-variants` also inserts `.extracted` into the output file name.\n",
+ "* `--sample-participant-lookup` -- two-column table (`genotype_id`, `sample_id`) used when genotype and phenotype name their samples differently.\n",
+ "\n",
+ "Filter thresholds:\n",
+ "\n",
+ "* `--kinship` -- kinship coefficient above which a pair counts as related, default `0.0625`.\n",
+ "* `--kin-maf` -- MAF floor applied before the KING estimate, default `0.01`.\n",
+ "* `--maf-filter` / `--maf-max-filter` and `--mac-filter` / `--mac-max-filter` -- allele frequency and count bounds; `0` disables a bound.\n",
+ "* `--geno-filter` -- maximum missingness per variant, default `0.1`.\n",
+ "* `--mind-filter` -- maximum missingness per sample, default `0.1`.\n",
+ "* `--hwe-filter` -- Hardy-Weinberg p-value cutoff, default `1e-15`.\n",
+ "* `--rm-dups` -- drop duplicate variants.\n",
+ "* `--treat-dosage-missing` -- handle dosage data.\n",
+ "* `--meta-only` -- write only the SNP and sample lists rather than a PLINK binary set.\n",
+ "* `--other-args` -- extra PLINK arguments passed through, such as `snps_only` or `write-samples`.\n",
+ "\n",
+ "LD pruning:\n",
+ "\n",
+ "* `--window`, `--shift`, `--r2` -- pruning window in variants (default `50`), how far it moves each time (`10`), and the r2 ceiling (`0.1`). Setting `--r2 0` skips pruning altogether.\n",
+ "* `--bad-ld` -- pass PLINK `--bad-ld`, which suppresses the pruning diagnostics that otherwise abort on regions of extreme LD.\n",
+ "\n",
+ "Runtime:\n",
+ "\n",
+ "* `--numThreads`, `--job-size`, `--walltime`, `--mem` -- threads (default `20`) and cluster resources.\n",
+ "* `--modular-script-dir` -- location of the shell and R drivers, default `code/script`."
]
},
{
@@ -33,16 +106,58 @@
"kernel": "SoS"
},
"source": [
- "## Methods\n\nDepending on the context of your problem, the workflow can be executed in two ways:\n\n1. Run `qc` command to perform genotype data QC and LD pruning to generate a subset of variants in preparation for analysis such as PCA.\n2. Run `king` first on either the original or a subset of common variants to identify unrelated individuals. The `king` pipeline will split samples to related and unrelated individuals. Then you perform `qc` on these individuals only and finally extract the same set of QC-ed variants for related individuals."
+ "## Output\n",
+ "\n",
+ "* `..plink_qc.{bed,bim,fam}` -- the filtered genotype set, written by `qc_no_prune` and by the filtering step of `qc`. `.extracted` is inserted when `--keep-variants` was supplied, and `--meta-only` produces a `.snplist` instead of a binary set.\n",
+ "* `..plink_qc.prune.{bed,bim,fam}` and `..plink_qc.prune.in` -- the LD-pruned genotype set and the variant list that defines it. The `.prune.in` list is what PCA consumes:\n",
+ "\n",
+ " ```\n",
+ " chr1:820919_CACTACCTGCTTGTCCAGCAGGTCCACCCTGTCTACACTACCTGCCTGCAAAGCAGATCCACCCTGTCTACACTACCTGGCTGG\n",
+ " chr1:820929_TTGTCCAGCAGGTCCACCCTGTCTACACTACCTGCCTGCAAAGCAGATCCACCCTGTCTACACTACCTGGCTGGCCAGTAGATC\n",
+ " chr1:820929_TTGTCCAGCAGGTCCACCCTGTCTACACTACCTGCCTGCAAAGCAGATCCACCCTGTCTACACTACCTGGCTGGCCAGTAGATC\n",
+ " ```\n",
+ "\n",
+ "* `..kin0` -- the KING kinship table, one row per pair above the threshold, with the kinship coefficient in the last column:\n",
+ "\n",
+ " ```\n",
+ " #FID1\tIID1\tFID2\tIID2\tNSNP\tHETHET\tIBS0\tKINSHIP\n",
+ " 0\tSAMPLE_060\t0\tSAMPLE_059\t59818\t0.108178\t0\t0.248649\n",
+ " ```\n",
+ "\n",
+ "* `..related_id` -- the individuals dropped to leave a maximal unrelated set; the file is created empty when no pair passes `--kinship`:\n",
+ "\n",
+ " ```\n",
+ " 0 SAMPLE_060\n",
+ " ```\n",
+ "\n",
+ "* `..unrelated.{bed,bim,fam}` and `..related.{bed,bim,fam}` -- the split cohort, written only when relatives were found.\n",
+ "* `.sample_genotypes.txt` -- FID/IID of the samples present in both genotype and phenotype, in the form PLINK `--keep` expects:\n",
+ "\n",
+ " ```\n",
+ " 0\tSAMPLE_001\n",
+ " 0\tSAMPLE_002\n",
+ " 0\tSAMPLE_003\n",
+ " ```\n",
+ "\n",
+ "* `.sample_overlap.txt` -- the same overlap as a `genotype_id` / `sample_id` table:\n",
+ "\n",
+ " ```\n",
+ " genotype_id\tsample_id\n",
+ " SAMPLE_001\tSAMPLE_001\n",
+ " SAMPLE_002\tSAMPLE_002\n",
+ " ```\n",
+ "\n",
+ "Each step also writes PLINK `.log` files and `.stdout` / `.stderr` beside its output. Example results for the toy data are under `output/gwas_qc/`."
]
},
{
"cell_type": "markdown",
"metadata": {
- "kernel": "SoS"
+ "kernel": "SoS",
+ "tags": []
},
"source": [
- "## Default Parameters: QC\n\n- Kinship coefficient for related individuals: 0.0625\n- MAF and MAC default: 0\n - Above default includes both common and are variant\n - Recommand MAF for PCA: 0.01, [we should stick to common variants](https://bmcgenomdata.biomedcentral.com/articles/10.1186/s12863-020-0833-x)\n - Recommand MAC for single variant analysis: 5.\n- Variant level missingness threshold: 0.1\n- Sample level missingness threshold: 0.1\n- LD pruning via PLINK for PCA analysis:\n - window 50 \n - shift 10 \n - r2 0.1\n- HWE default: 1E-15 which is very lenient"
+ "## Minimal Working Example"
]
},
{
@@ -51,7 +166,9 @@
"kernel": "SoS"
},
"source": [
- "## Input Files\n\n| File | Description |\n|------|-------------|\n| `output/genotype_formatting/plink/protocol_example.genotype.merged.{bed,bim,fam}` | Merged genome-wide PLINK bundle from genotype_formatting (Step 1 input) |\n| `input/rnaseq/protocol_example.rnaseq.bed.gz` | Toy RNA-seq expression phenotype (60 samples SAMPLE_001..060) for sample matching |\n\nGenotype input may be PLINK1 (bed/bim/fam) or PLINK2 (pgen/pvar/psam). For VCF input, first convert with the `genotype_formatting` pipeline.\n"
+ "### Genotype QC of the protocol example data\n",
+ "\n",
+ "The `chr1_chr6` set was merged from the `chr1` and `chr6` data with the `merge_plink` command in [genotype formatting](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/genotype/genotype_formatting.html). The steps below run in order, each consuming what the earlier ones produced."
]
},
{
@@ -60,17 +177,9 @@
"kernel": "SoS"
},
"source": [
- "## Output Files\n\n| File | Description |\n|------|-------------|\n| `output/gwas_qc/plink/protocol_example.genotype.merged.plink_qc.{bed,bim,fam}` | Basic-QC genotype (Step 1) |\n| `output/gwas_qc/genotype/protocol_example.rnaseq.bed.sample_genotypes.txt` | Genotype-sample list overlapping the phenotype (Step 2) |\n| `output/gwas_qc/kinship/*.king.unrelated.{bed,bim,fam}` and `*.king.related.{bed,bim,fam}` | KING-split unrelated/related sets (Step 3) |\n| `output/gwas_qc/genotype/*.king.unrelated.plink_qc.prune.in` | LD-pruned variant list for PCA (Step 4) |\n| `output/gwas_qc/cache/*.for_pca.{bed,bim,fam}` | Pruned genotype set prepared for PCA (Step 5) |\n"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS",
- "tags": []
- },
- "source": [
- "## Minimal Working Example\n\nMinimal working example data-set can be downloaded from [Synapse](https://www.synapse.org/#!Synapse:syn69670658/files/).\n\nThe `chr1_chr6` data-set was merged from `chr1` and `chr6` data, using `merge_plink` command from [genotype formatting](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/genotype/genotype_formatting.html) pipeline."
+ "#### Step 1. Basic QC (rare and common variants)\n",
+ "\n",
+ "Apply the variant- and sample-level filters: missingness, HWE and MAC."
]
},
{
@@ -79,16 +188,7 @@
"kernel": "SoS"
},
"source": [
- "### **Step 1.** Basic QC (rare and common variants)\n"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "Apply variant- and sample-level filters (missingness, HWE, MAC). Timing: <1 min on toy data.\n"
+ "**Timing**: <1 min (on toy dataset)"
]
},
{
@@ -101,7 +201,14 @@
},
"outputs": [],
"source": [
- "sos run pipeline/GWAS_QC.ipynb qc_no_prune \\\n --cwd output/gwas_qc/plink \\\n --genoFile output/genotype_formatting/plink/protocol_example.genotype.merged.bed \\\n --name protocol_example \\\n --geno-filter 0.1 \\\n --mind-filter 0.1 \\\n --hwe-filter 1e-08 \\\n --mac-filter 0"
+ "sos run pipeline/GWAS_QC.ipynb qc_no_prune \\\n",
+ " --cwd output/gwas_qc/plink \\\n",
+ " --genoFile output/genotype_formatting/plink/protocol_example.genotype.merged.bed \\\n",
+ " --name protocol_example \\\n",
+ " --geno-filter 0.1 \\\n",
+ " --mind-filter 0.1 \\\n",
+ " --hwe-filter 1e-08 \\\n",
+ " --mac-filter 0"
]
},
{
@@ -110,7 +217,9 @@
"kernel": "SoS"
},
"source": [
- "### **Step 2.** Sample Match with Phenotype\n"
+ "#### Step 2. Sample match with phenotype\n",
+ "\n",
+ "Find the samples shared between genotype and phenotype and write the overlapping sample lists."
]
},
{
@@ -119,7 +228,7 @@
"kernel": "SoS"
},
"source": [
- "Find samples shared between genotype and phenotype, writing the overlapping sample lists. Timing: <1 min.\n"
+ "**Timing**: <1 min (on toy dataset)"
]
},
{
@@ -133,7 +242,11 @@
},
"outputs": [],
"source": [
- "sos run pipeline/GWAS_QC.ipynb genotype_phenotype_sample_overlap \\\n --cwd output/gwas_qc/genotype \\\n --genoFile output/gwas_qc/plink/protocol_example.genotype.merged.plink_qc.fam \\\n --phenoFile input/rnaseq/protocol_example.rnaseq.bed.gz \\\n --name protocol_example"
+ "sos run pipeline/GWAS_QC.ipynb genotype_phenotype_sample_overlap \\\n",
+ " --cwd output/gwas_qc/genotype \\\n",
+ " --genoFile output/gwas_qc/plink/protocol_example.genotype.merged.plink_qc.fam \\\n",
+ " --phenoFile input/rnaseq/protocol_example.rnaseq.bed.gz \\\n",
+ " --name protocol_example"
]
},
{
@@ -143,7 +256,9 @@
"tags": []
},
"source": [
- "### **Step 3.** Kinship QC\n\nEstimate kinship with KING and split samples into related and unrelated sets. In this toy dataset SAMPLE_059 and SAMPLE_060 are a parent-offspring pair, so KING **does** detect related individuals and produces both `*.related.bed` and `*.unrelated.bed`.\n"
+ "#### Step 3. Kinship QC\n",
+ "\n",
+ "Estimate kinship with KING and split the samples into related and unrelated sets. In the toy data SAMPLE_059 and SAMPLE_060 are a parent-offspring pair, so KING does find relatives and writes both `*.related.bed` and `*.unrelated.bed`."
]
},
{
@@ -152,7 +267,7 @@
"kernel": "SoS"
},
"source": [
- "Timing: <2 min on toy data.\n"
+ "**Timing**: <2 min (on toy dataset)"
]
},
{
@@ -165,7 +280,11 @@
},
"outputs": [],
"source": [
- "sos run pipeline/GWAS_QC.ipynb king \\\n --cwd output/gwas_qc/kinship \\\n --genoFile output/gwas_qc/plink/protocol_example.genotype.merged.plink_qc.bed \\\n --name protocol_example.king \\\n --keep-samples output/gwas_qc/genotype/protocol_example.rnaseq.bed.sample_genotypes.txt"
+ "sos run pipeline/GWAS_QC.ipynb king \\\n",
+ " --cwd output/gwas_qc/kinship \\\n",
+ " --genoFile output/gwas_qc/plink/protocol_example.genotype.merged.plink_qc.bed \\\n",
+ " --name protocol_example.king \\\n",
+ " --keep-samples output/gwas_qc/genotype/protocol_example.rnaseq.bed.sample_genotypes.txt"
]
},
{
@@ -174,7 +293,9 @@
"kernel": "SoS"
},
"source": [
- "### **Step 4.** Prepare Unrelated Individuals for PCA\n\nBecause related individuals were detected (Step 3), run `qc` on the KING `*.unrelated.bed` to produce the LD-pruned, unrelated genotype set used downstream for PCA.\n"
+ "#### Step 4. Prepare unrelated individuals for PCA\n",
+ "\n",
+ "Because Step 3 found related individuals, run `qc` on the KING `*.unrelated.bed` to produce the LD-pruned, unrelated genotype set used downstream for PCA."
]
},
{
@@ -183,7 +304,7 @@
"kernel": "SoS"
},
"source": [
- "Timing: <1 min on toy data.\n"
+ "**Timing**: <1 min (on toy dataset)"
]
},
{
@@ -197,18 +318,23 @@
},
"outputs": [],
"source": [
- "sos run pipeline/GWAS_QC.ipynb qc \\\n --cwd output/gwas_qc/genotype \\\n --genoFile output/gwas_qc/kinship/protocol_example.genotype.merged.plink_qc.protocol_example.king.unrelated.bed \\\n --name protocol_example \\\n --mac-filter 5"
+ "sos run pipeline/GWAS_QC.ipynb qc \\\n",
+ " --cwd output/gwas_qc/genotype \\\n",
+ " --genoFile output/gwas_qc/kinship/protocol_example.genotype.merged.plink_qc.protocol_example.king.unrelated.bed \\\n",
+ " --name protocol_example \\\n",
+ " --mac-filter 5"
]
},
{
"cell_type": "markdown",
"metadata": {
- "jp-MarkdownHeadingCollapsed": true,
"kernel": "SoS",
"tags": []
},
"source": [
- "**Alternative (no related individuals):** if KING reports `No related individuals detected` and produces no `*.unrelated.bed`, run `qc` on the original QC genotype with `--keep-samples` instead. For this toy dataset related individuals ARE present, so the Step 4 path above applies; the command below is shown for reference.\n"
+ "#### Alternative to Step 4: no related individuals\n",
+ "\n",
+ "If KING reports no related individuals and writes no `*.unrelated.bed`, run `qc` on the QC-ed genotype with `--keep-samples` instead. Relatives are present in this toy data, so Step 4 is the path that applies here and the command below is shown only for reference."
]
},
{
@@ -217,7 +343,7 @@
"kernel": "SoS"
},
"source": [
- "Timing: <1 min"
+ "**Timing**: <1 min (on toy dataset)"
]
},
{
@@ -228,7 +354,11 @@
},
"outputs": [],
"source": [
- "sos run pipeline/GWAS_QC.ipynb qc \\\n --cwd output/gwas_qc/genotype \\\n --genoFile output/gwas_qc/plink/protocol_example.genotype.merged.plink_qc.bed \\\n --name protocol_example \\\n --mac-filter 5"
+ "sos run pipeline/GWAS_QC.ipynb qc \\\n",
+ " --cwd output/gwas_qc/genotype \\\n",
+ " --genoFile output/gwas_qc/plink/protocol_example.genotype.merged.plink_qc.bed \\\n",
+ " --name protocol_example \\\n",
+ " --mac-filter 5"
]
},
{
@@ -237,7 +367,18 @@
"kernel": "Bash"
},
"source": [
- "### **Step 5.** Extract Pruned Variants for PCA\n\nExtract the LD-pruned variants (from Step 4) out of the full genotype set, applying only sample-level missingness, in preparation for PCA.\n"
+ "#### Step 5. Extract pruned variants for PCA\n",
+ "\n",
+ "Extract the LD-pruned variants from Step 4 out of the full genotype set, applying only sample-level missingness, in preparation for PCA."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
]
},
{
@@ -248,7 +389,14 @@
},
"outputs": [],
"source": [
- "sos run pipeline/GWAS_QC.ipynb qc_no_prune \\\n --cwd output/gwas_qc/cache \\\n --genoFile output/genotype_formatting/plink/protocol_example.genotype.merged.bed \\\n --geno-filter 0 \\\n --mind-filter 0.1 \\\n --maf-filter 0 \\\n --keep-variants output/gwas_qc/genotype/protocol_example.genotype.merged.plink_qc.protocol_example.king.unrelated.plink_qc.prune.in \\\n --name for_pca"
+ "sos run pipeline/GWAS_QC.ipynb qc_no_prune \\\n",
+ " --cwd output/gwas_qc/cache \\\n",
+ " --genoFile output/genotype_formatting/plink/protocol_example.genotype.merged.bed \\\n",
+ " --geno-filter 0 \\\n",
+ " --mind-filter 0.1 \\\n",
+ " --maf-filter 0 \\\n",
+ " --keep-variants output/gwas_qc/genotype/protocol_example.genotype.merged.plink_qc.protocol_example.king.unrelated.plink_qc.prune.in \\\n",
+ " --name for_pca"
]
},
{
@@ -269,18 +417,130 @@
},
"outputs": [],
"source": [
- "sos run GWAS_QC.ipynb -h"
+ "sos run pipeline/GWAS_QC.ipynb -h"
]
},
{
- "cell_type": "code",
- "execution_count": null,
+ "cell_type": "markdown",
"metadata": {
"kernel": "SoS"
},
- "outputs": [],
"source": [
- "[global]\nparameter: modular_script_dir = path('code/script') # override with --modular-script-dir\n# the output directory for generated files\nparameter: cwd = path(\"output\")\n# A string to identify your analysis run\nparameter: name = str\n# PLINK binary files (either BED/BIM/FAM or PGEN/PVAR/PSAM format)\nparameter: genoFile = paths\n# The path to the file that contains the list of samples to remove (format FID, IID)\nparameter: remove_samples = path('.')\n# The path to the file that contains the list of samples to keep (format FID, IID)\nparameter: keep_samples = path('.')\n# The path to the file that contains the list of variants to keep\nparameter: keep_variants = path('.')\n# The path to the file that contains the list of variants to exclude\nparameter: exclude_variants = path('.')\n# Kinship coefficient threshold for related individuals\n# (e.g first degree above 0.25, second degree above 0.125, third degree above 0.0625)\nparameter: kinship = 0.0625\n# For cluster jobs, number commands to run per job\nparameter: job_size = 1\n# Wall clock time expected\nparameter: walltime = \"5h\"\n# Memory expected\nparameter: mem = \"16G\"\n# Number of threads\nparameter: numThreads = 20\n# use this function to edit memory string for PLINK input\nfrom sos.utils import expand_size\ncwd = path(f\"{cwd:a}\")\n\n# Determine if the file is in PLINK1 (BED/BIM/FAM) or PLINK2 (PGEN/PVAR/PSAM) format\ndef determine_plink_format(file_path):\n \"\"\"\n Determine the PLINK file format based on file extensions and companion files.\n \n Args:\n file_path (str or Path): Path to the input file\n \n Returns:\n str: 'plink1' or 'plink2'\n \"\"\"\n # Convert to string if it's a Path object\n file_path = str(file_path)\n \n # Check direct file extensions\n if file_path.endswith('.bed'):\n return 'plink1'\n elif file_path.endswith('.pgen'):\n return 'plink2'\n \n # If the file doesn't have a standard extension, try to infer format\n try:\n # Remove the file extension if present\n base_path = file_path.rsplit('.', 1)[0] if '.' in file_path else file_path\n \n # Check for PLINK1 companion files\n plink1_companion_files = [\n f\"{base_path}.bim\",\n f\"{base_path}.fam\"\n ]\n \n # Check for PLINK2 companion files\n plink2_companion_files = [\n f\"{base_path}.pvar\",\n f\"{base_path}.psam\"\n ]\n \n # Check PLINK1 format\n if all(os.path.exists(f) for f in plink1_companion_files):\n return 'plink1'\n \n # Check PLINK2 format\n if all(os.path.exists(f) for f in plink2_companion_files):\n return 'plink2'\n \n except Exception as e:\n print(f\"Error determining PLINK format: {e}\")\n \n # Default to PLINK1 if can't determine\n return 'plink1'\n\n\n# Get the appropriate PLINK command based on the input file format\ndef get_plink_command_prefix(file_path):\n format_type = determine_plink_format(file_path)\n if format_type == 'plink1':\n return \"--bfile\"\n else: # plink2\n return \"--pfile\"\n \n# Generate the appropriate file extension based on the requested format\ndef get_output_extension(output_format, is_prune=False):\n if output_format == 'plink1':\n return '.bed' if not is_prune else '.prune.bed'\n else: # plink2\n return '.pgen' if not is_prune else '.prune.pgen'\n \n# Choose the make-bed or make-pgen command based on desired output format\ndef get_make_command(output_format):\n if output_format == 'plink1':\n return '--make-bed'\n else: # plink2\n return '--make-pgen'\n\ndef get_other_args_flags(other_args):\n import re\n if not other_args:\n return \"\"\n args = [other_args] if isinstance(other_args, str) else list(other_args)\n flags = []\n for arg in args:\n arg = str(arg)\n if not re.fullmatch(r\"[A-Za-z0-9][A-Za-z0-9_.:-]*\", arg):\n raise ValueError(f\"Cannot safely pass PLINK other_args value: {arg}\")\n flags.append(f\"--other-arg {arg}\")\n return \" \".join(flags)"
+ "```\n",
+ "usage: sos run pipeline/GWAS_QC.ipynb\n",
+ " [workflow_name | -t targets] [options] [workflow_options]\n",
+ " workflow_name: Single or combined workflows defined in this script\n",
+ " targets: One or more targets to generate\n",
+ " options: Single-hyphen sos parameters (see \"sos run -h\" for details)\n",
+ " workflow_options: Double-hyphen workflow-specific parameters\n",
+ "\n",
+ "Workflows:\n",
+ " king\n",
+ " qc_no_prune\n",
+ " qc\n",
+ " genotype_phenotype_sample_overlap\n",
+ "\n",
+ "Global Workflow Options:\n",
+ " --modular-script-dir code/script (as path)\n",
+ " --cwd output (as path)\n",
+ " the output directory for generated files\n",
+ " --name VAL (as str, required)\n",
+ " A string to identify your analysis run\n",
+ " --genoFile paths\n",
+ "\n",
+ " PLINK binary files (either BED/BIM/FAM or PGEN/PVAR/PSAM\n",
+ " format)\n",
+ " --remove-samples . (as path)\n",
+ " The path to the file that contains the list of samples\n",
+ " to remove (format FID, IID)\n",
+ " --keep-samples . (as path)\n",
+ " The path to the file that contains the list of samples\n",
+ " to keep (format FID, IID)\n",
+ " --keep-variants . (as path)\n",
+ " The path to the file that contains the list of variants\n",
+ " to keep\n",
+ " --exclude-variants . (as path)\n",
+ " The path to the file that contains the list of variants\n",
+ " to exclude\n",
+ " --kinship 0.0625 (as float)\n",
+ " Kinship coefficient threshold for related individuals\n",
+ " (e.g first degree above 0.25, second degree above 0.125,\n",
+ " third degree above 0.0625)\n",
+ " --job-size 1 (as int)\n",
+ " For cluster jobs, number commands to run per job\n",
+ " --walltime 5h\n",
+ " Wall clock time expected\n",
+ " --mem 16G\n",
+ " Memory expected\n",
+ " --numThreads 20 (as int)\n",
+ " Number of threads\n",
+ "\n",
+ "Sections\n",
+ " king_1: Inference of relationships in the sample to identify\n",
+ " closely related individuals\n",
+ " Workflow Options:\n",
+ " --kin-maf 0.01 (as float)\n",
+ " PLINK binary file\n",
+ " king_2: Select a list of unrelated individual with an attempt to\n",
+ " maximize the unrelated individuals selected from the\n",
+ " data\n",
+ " king_3: Split genotype data into related and unrelated samples,\n",
+ " if related individuals are detected\n",
+ " qc_no_prune, qc_1: Filter SNPs and select individuals\n",
+ " Workflow Options:\n",
+ " --maf-filter 0.0 (as float)\n",
+ " minimum MAF filter to use. 0 means do not apply this\n",
+ " filter.\n",
+ " --maf-max-filter 0.0 (as float)\n",
+ " maximum MAF filter to use. 0 means do not apply this\n",
+ " filter.\n",
+ " --mac-filter 0.0 (as float)\n",
+ " minimum MAC filter to use. 0 means do not apply this\n",
+ " filter.\n",
+ " --mac-max-filter 0.0 (as float)\n",
+ " maximum MAC filter to use. 0 means do not apply this\n",
+ " filter.\n",
+ " --geno-filter 0.1 (as float)\n",
+ " Maximum missingess per-variant\n",
+ " --mind-filter 0.1 (as float)\n",
+ " Maximum missingness per-sample\n",
+ " --hwe-filter 1e-15 (as float)\n",
+ " HWE filter -- a very lenient one\n",
+ " --other-args (as list)\n",
+ " Other PLINK arguments e.g snps_only, write-samples, etc\n",
+ " --[no-]meta-only (default to False)\n",
+ " Only output SNP and sample list, rather than the PLINK\n",
+ " binary format of subset data\n",
+ " --[no-]rm-dups (default to False)\n",
+ " Remove duplicate variants\n",
+ " --[no-]treat-dosage-missing (default to False)\n",
+ " Add option to process dosage\n",
+ " qc_2: LD prunning and remove related individuals (both ind of\n",
+ " a pair) Plink2 has multi-threaded calculation for LD\n",
+ " prunning\n",
+ " Workflow Options:\n",
+ " --window 50 (as int)\n",
+ " Window size\n",
+ " --shift 10 (as int)\n",
+ " Shift window every 10 snps\n",
+ " --r2 0.1 (as float)\n",
+ " --mac-filter 0.0 (as float)\n",
+ " --other-args (as list)\n",
+ " --[no-]bad-ld (default to False)\n",
+ " Use PLINK --bad-ld flag (skip LD pruning diagnostics for\n",
+ " regions with extreme LD)\n",
+ " genotype_phenotype_sample_overlap: This workflow extracts overlapping samples\n",
+ " for genotype data with phenotype data, and output the\n",
+ " filtered sample genotype list as well as sample\n",
+ " phenotype list\n",
+ " Workflow Options:\n",
+ " --phenoFile VAL (as path, required)\n",
+ " A phenotype file, can be bed.gz or tsv\n",
+ " --sample-participant-lookup . (as path)\n",
+ " If this file is provided, a genotype/phenotype sample\n",
+ " name match will be performed It must contain two column\n",
+ " names: genotype_id, sample_id\n",
+ "```"
]
},
{
@@ -289,7 +549,9 @@
"kernel": "SoS"
},
"source": [
- "## Estimate kinship in the sample\n\nThe output is a list of related individuals, as well as the kinship matrix"
+ "## Workflow implementation\n",
+ "\n",
+ "The SoS workflow definitions below are unchanged from the original protocol."
]
},
{
@@ -300,18 +562,134 @@
},
"outputs": [],
"source": [
- "# Inference of relationships in the sample to identify closely related individuals\n[king_1]\n# PLINK binary file\nparameter: kin_maf = 0.01\ninput: genoFile\noutput: f'{cwd}/{_input:bn}{(\".\"+name) if name else \"\"}.kin0'\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'\nplink_command = get_plink_command_prefix(genoFile)\nbash: expand= \"${ }\", stderr = f'{_output}.stderr', stdout = f'{_output}.stdout'\n bash ${modular_script_dir}/data_preprocessing/genotype/GWAS_QC.sh king \\\n --cwd \"${cwd}\" \\\n --genoFile \"${genoFile}\" \\\n --plink-command \"${plink_command}\" \\\n --out-prefix \"${_output:n}\" \\\n --keep-samples \"${keep_samples}\" \\\n --remove-samples \"${remove_samples}\" \\\n --name \"${name}\" \\\n --kinship ${kinship} \\\n --kin-maf ${kin_maf} \\\n --numThreads ${numThreads}"
+ "[global]\n",
+ "parameter: modular_script_dir = path('code/script') # override with --modular-script-dir\n",
+ "# the output directory for generated files\n",
+ "parameter: cwd = path(\"output\")\n",
+ "# A string to identify your analysis run\n",
+ "parameter: name = str\n",
+ "# PLINK binary files (either BED/BIM/FAM or PGEN/PVAR/PSAM format)\n",
+ "parameter: genoFile = paths\n",
+ "# The path to the file that contains the list of samples to remove (format FID, IID)\n",
+ "parameter: remove_samples = path('.')\n",
+ "# The path to the file that contains the list of samples to keep (format FID, IID)\n",
+ "parameter: keep_samples = path('.')\n",
+ "# The path to the file that contains the list of variants to keep\n",
+ "parameter: keep_variants = path('.')\n",
+ "# The path to the file that contains the list of variants to exclude\n",
+ "parameter: exclude_variants = path('.')\n",
+ "# Kinship coefficient threshold for related individuals\n",
+ "# (e.g first degree above 0.25, second degree above 0.125, third degree above 0.0625)\n",
+ "parameter: kinship = 0.0625\n",
+ "# For cluster jobs, number commands to run per job\n",
+ "parameter: job_size = 1\n",
+ "# Wall clock time expected\n",
+ "parameter: walltime = \"5h\"\n",
+ "# Memory expected\n",
+ "parameter: mem = \"16G\"\n",
+ "# Number of threads\n",
+ "parameter: numThreads = 20\n",
+ "# use this function to edit memory string for PLINK input\n",
+ "from sos.utils import expand_size\n",
+ "cwd = path(f\"{cwd:a}\")\n",
+ "\n",
+ "# Determine if the file is in PLINK1 (BED/BIM/FAM) or PLINK2 (PGEN/PVAR/PSAM) format\n",
+ "def determine_plink_format(file_path):\n",
+ " \"\"\"\n",
+ " Determine the PLINK file format based on file extensions and companion files.\n",
+ " \n",
+ " Args:\n",
+ " file_path (str or Path): Path to the input file\n",
+ " \n",
+ " Returns:\n",
+ " str: 'plink1' or 'plink2'\n",
+ " \"\"\"\n",
+ " # Convert to string if it's a Path object\n",
+ " file_path = str(file_path)\n",
+ " \n",
+ " # Check direct file extensions\n",
+ " if file_path.endswith('.bed'):\n",
+ " return 'plink1'\n",
+ " elif file_path.endswith('.pgen'):\n",
+ " return 'plink2'\n",
+ " \n",
+ " # If the file doesn't have a standard extension, try to infer format\n",
+ " try:\n",
+ " # Remove the file extension if present\n",
+ " base_path = file_path.rsplit('.', 1)[0] if '.' in file_path else file_path\n",
+ " \n",
+ " # Check for PLINK1 companion files\n",
+ " plink1_companion_files = [\n",
+ " f\"{base_path}.bim\",\n",
+ " f\"{base_path}.fam\"\n",
+ " ]\n",
+ " \n",
+ " # Check for PLINK2 companion files\n",
+ " plink2_companion_files = [\n",
+ " f\"{base_path}.pvar\",\n",
+ " f\"{base_path}.psam\"\n",
+ " ]\n",
+ " \n",
+ " # Check PLINK1 format\n",
+ " if all(os.path.exists(f) for f in plink1_companion_files):\n",
+ " return 'plink1'\n",
+ " \n",
+ " # Check PLINK2 format\n",
+ " if all(os.path.exists(f) for f in plink2_companion_files):\n",
+ " return 'plink2'\n",
+ " \n",
+ " except Exception as e:\n",
+ " print(f\"Error determining PLINK format: {e}\")\n",
+ " \n",
+ " # Default to PLINK1 if can't determine\n",
+ " return 'plink1'\n",
+ "\n",
+ "\n",
+ "# Get the appropriate PLINK command based on the input file format\n",
+ "def get_plink_command_prefix(file_path):\n",
+ " format_type = determine_plink_format(file_path)\n",
+ " if format_type == 'plink1':\n",
+ " return \"--bfile\"\n",
+ " else: # plink2\n",
+ " return \"--pfile\"\n",
+ " \n",
+ "# Generate the appropriate file extension based on the requested format\n",
+ "def get_output_extension(output_format, is_prune=False):\n",
+ " if output_format == 'plink1':\n",
+ " return '.bed' if not is_prune else '.prune.bed'\n",
+ " else: # plink2\n",
+ " return '.pgen' if not is_prune else '.prune.pgen'\n",
+ " \n",
+ "# Choose the make-bed or make-pgen command based on desired output format\n",
+ "def get_make_command(output_format):\n",
+ " if output_format == 'plink1':\n",
+ " return '--make-bed'\n",
+ " else: # plink2\n",
+ " return '--make-pgen'\n",
+ "\n",
+ "def get_other_args_flags(other_args):\n",
+ " import re\n",
+ " if not other_args:\n",
+ " return \"\"\n",
+ " args = [other_args] if isinstance(other_args, str) else list(other_args)\n",
+ " flags = []\n",
+ " for arg in args:\n",
+ " arg = str(arg)\n",
+ " if not re.fullmatch(r\"[A-Za-z0-9][A-Za-z0-9_.:-]*\", arg):\n",
+ " raise ValueError(f\"Cannot safely pass PLINK other_args value: {arg}\")\n",
+ " flags.append(f\"--other-arg {arg}\")\n",
+ " return \" \".join(flags)"
]
},
{
- "cell_type": "code",
- "execution_count": null,
+ "cell_type": "markdown",
"metadata": {
"kernel": "SoS"
},
- "outputs": [],
"source": [
- "# Select a list of unrelated individual with an attempt to maximize the unrelated individuals selected from the data \n[king_2: shared = \"related_id\" ]\nrelated_id = [x.strip() for x in open(_input).readlines() if not x.startswith(\"#\")]\noutput: f'{_input:n}.related_id'\nwith open(_output, 'a'):\n pass\ndone_if(len(related_id) == 0, msg = f\"No related individuals detected from {_input}.\")\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'\nbash: expand= \"${ }\", stderr = f'{_output}.stderr', stdout = f'{_output}.stdout'\n Rscript ${modular_script_dir}/data_preprocessing/genotype/GWAS_QC.R \\\n --step king_2 \\\n --input \"${_input}\" \\\n --output \"${_output}\" \\\n --kinship ${kinship}"
+ "### Kinship estimation and cohort split\n",
+ "\n",
+ "`king_1` runs KING to produce the kinship table, `king_2` chooses which individuals to drop, and `king_3` writes the related and unrelated PLINK sets."
]
},
{
@@ -322,16 +700,50 @@
},
"outputs": [],
"source": [
- "# Split genotype data into related and unrelated samples, if related individuals are detected\n[king_3]\ndepends: sos_variable(\"related_id\")\ninput: output_from(2), genoFile\noutput_format = determine_plink_format(_input[1])\noutput_ext = get_output_extension(output_format)\noutput: unrelated_bed = f'{cwd}/{_input[0]:bn}.unrelated{output_ext}',\n related_bed = f'{cwd}/{_input[0]:bn}.related{output_ext}'\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output[0]:bn}'\nbash: expand= \"${ }\", stderr = f'{_output[0]:n}.stderr', stdout = f'{_output[0]:n}.stdout'\n bash ${modular_script_dir}/data_preprocessing/genotype/GWAS_QC.sh king_split \\\n --cwd \"${cwd}\" \\\n --genoFile \"${_input[1]}\" \\\n --remove-samples \"${_input[0]}\" \\\n ${('--keep-samples \"%s\"' % keep_samples) if keep_samples.is_file() else \"\"} \\\n --out-unrelated \"${_output[0]:n}\" \\\n --out-related \"${_output[1]:n}\" \\\n --related-output \"${_output[1]}\" \\\n --numThreads ${numThreads}\n"
+ "# Inference of relationships in the sample to identify closely related individuals\n",
+ "[king_1]\n",
+ "# PLINK binary file\n",
+ "parameter: kin_maf = 0.01\n",
+ "input: genoFile\n",
+ "output: f'{cwd}/{_input:bn}{(\".\"+name) if name else \"\"}.kin0'\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'\n",
+ "plink_command = get_plink_command_prefix(genoFile)\n",
+ "bash: expand= \"${ }\", stderr = f'{_output}.stderr', stdout = f'{_output}.stdout'\n",
+ " bash ${modular_script_dir}/data_preprocessing/genotype/GWAS_QC.sh king \\\n",
+ " --cwd \"${cwd}\" \\\n",
+ " --genoFile \"${genoFile}\" \\\n",
+ " --plink-command \"${plink_command}\" \\\n",
+ " --out-prefix \"${_output:n}\" \\\n",
+ " --keep-samples \"${keep_samples}\" \\\n",
+ " --remove-samples \"${remove_samples}\" \\\n",
+ " --name \"${name}\" \\\n",
+ " --kinship ${kinship} \\\n",
+ " --kin-maf ${kin_maf} \\\n",
+ " --numThreads ${numThreads}"
]
},
{
- "cell_type": "markdown",
+ "cell_type": "code",
+ "execution_count": null,
"metadata": {
"kernel": "SoS"
},
+ "outputs": [],
"source": [
- "## Genotype and sample QC\n\nQC the genetic data based on MAF, sample and variant missigness and Hardy-Weinberg Equilibrium (HWE).\n\nIn this step you may also provide a list of samples to keep, for example in the case when you would like to subset a sample based on their ancestries to perform independent analyses on each of these groups.\n\nThe default parameters are set to reflect some suggestions in Table 1 of [this paper](https://dx.doi.org/10.1002%2Fmpr.1608)."
+ "# Select a list of unrelated individual with an attempt to maximize the unrelated individuals selected from the data \n",
+ "[king_2: shared = \"related_id\" ]\n",
+ "related_id = [x.strip() for x in open(_input).readlines() if not x.startswith(\"#\")]\n",
+ "output: f'{_input:n}.related_id'\n",
+ "with open(_output, 'a'):\n",
+ " pass\n",
+ "done_if(len(related_id) == 0, msg = f\"No related individuals detected from {_input}.\")\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'\n",
+ "bash: expand= \"${ }\", stderr = f'{_output}.stderr', stdout = f'{_output}.stdout'\n",
+ " Rscript ${modular_script_dir}/data_preprocessing/genotype/GWAS_QC.R \\\n",
+ " --step king_2 \\\n",
+ " --input \"${_input}\" \\\n",
+ " --output \"${_output}\" \\\n",
+ " --kinship ${kinship}"
]
},
{
@@ -342,7 +754,36 @@
},
"outputs": [],
"source": [
- "# Filter SNPs and select individuals \n[qc_no_prune, qc_1 (basic QC filters)]\n# minimum MAF filter to use. 0 means do not apply this filter.\nparameter: maf_filter = 0.0\n# maximum MAF filter to use. 0 means do not apply this filter.\nparameter: maf_max_filter = 0.0\n# minimum MAC filter to use. 0 means do not apply this filter.\nparameter: mac_filter = 0.0\n# maximum MAC filter to use. 0 means do not apply this filter.\nparameter: mac_max_filter = 0.0 \n# Maximum missingess per-variant\nparameter: geno_filter = 0.1\n# Maximum missingness per-sample\nparameter: mind_filter = 0.1\n# HWE filter -- a very lenient one\nparameter: hwe_filter = 1e-15\n# Other PLINK arguments e.g snps_only, write-samples, etc\nparameter: other_args = []\n# Only output SNP and sample list, rather than the PLINK binary format of subset data\nparameter: meta_only = False\n# Remove duplicate variants\nparameter: rm_dups = False\n# Add option to process dosage\nparameter: treat_dosage_missing = False\n\nfail_if(not (keep_samples.is_file() or keep_samples == path('.')), msg = f'Cannot find ``{keep_samples}``')\nfail_if(not (keep_variants.is_file() or keep_variants == path('.')), msg = f'Cannot find ``{keep_variants}``')\nfail_if(not (remove_samples.is_file() or remove_samples == path('.')), msg = f'Cannot find ``{remove_samples}``')\n\ninput: genoFile, group_by=1\nplink_command = get_plink_command_prefix(_input)\noutput_format = determine_plink_format(_input)\nmake_command = get_make_command(output_format) if not meta_only else \"--write-snplist --write-samples\"\noutput_ext = get_output_extension(output_format) if not meta_only else \".snplist\"\nother_args_flags = get_other_args_flags(other_args)\noutput: f'{cwd}/{_input:bn}{(\".\" + name) if name else \"\"}.plink_qc{\".extracted\" if keep_variants.is_file() else \"\"}{output_ext}'\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'\nbash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout'\n bash ${modular_script_dir}/data_preprocessing/genotype/GWAS_QC.sh qc_no_prune \\\n --cwd \"${cwd}\" \\\n --genoFile \"${_input}\" \\\n --plink-command \"${plink_command}\" \\\n --output-format \"${output_format}\" \\\n --make-command \"${make_command}\" \\\n --out-prefix \"${_output:n}\" \\\n --name \"${name}\" \\\n --mac-filter ${mac_filter} \\\n --maf-filter ${maf_filter} \\\n --maf-max-filter ${maf_max_filter} \\\n --mac-max-filter ${mac_max_filter} \\\n --geno-filter ${geno_filter} \\\n --mind-filter ${mind_filter} \\\n --hwe-filter ${hwe_filter} \\\n ${\"--keep-samples \" + str(keep_samples) if keep_samples.is_file() else \"\"} \\\n ${\"--remove-samples \" + str(remove_samples) if remove_samples.is_file() else \"\"} \\\n ${\"--exclude-variants \" + str(exclude_variants) if exclude_variants.is_file() else \"\"} \\\n ${\"--keep-variants \" + str(keep_variants) if keep_variants.is_file() else \"\"} \\\n ${\"--meta-only\" if meta_only else \"\"} \\\n ${\"--rm-dups\" if rm_dups else \"\"} \\\n ${\"--treat-dosage-missing\" if treat_dosage_missing else \"\"} \\\n ${other_args_flags} \\\n --numThreads ${numThreads}"
+ "# Split genotype data into related and unrelated samples, if related individuals are detected\n",
+ "[king_3]\n",
+ "depends: sos_variable(\"related_id\")\n",
+ "input: output_from(2), genoFile\n",
+ "output_format = determine_plink_format(_input[1])\n",
+ "output_ext = get_output_extension(output_format)\n",
+ "output: unrelated_bed = f'{cwd}/{_input[0]:bn}.unrelated{output_ext}',\n",
+ " related_bed = f'{cwd}/{_input[0]:bn}.related{output_ext}'\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output[0]:bn}'\n",
+ "bash: expand= \"${ }\", stderr = f'{_output[0]:n}.stderr', stdout = f'{_output[0]:n}.stdout'\n",
+ " bash ${modular_script_dir}/data_preprocessing/genotype/GWAS_QC.sh king_split \\\n",
+ " --cwd \"${cwd}\" \\\n",
+ " --genoFile \"${_input[1]}\" \\\n",
+ " --remove-samples \"${_input[0]}\" \\\n",
+ " ${('--keep-samples \"%s\"' % keep_samples) if keep_samples.is_file() else \"\"} \\\n",
+ " --out-unrelated \"${_output[0]:n}\" \\\n",
+ " --out-related \"${_output[1]:n}\" \\\n",
+ " --related-output \"${_output[1]}\" \\\n",
+ " --numThreads ${numThreads}\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### Variant and sample filtering\n",
+ "\n",
+ "`qc_no_prune`, which doubles as the first step of `qc`, applies the frequency, missingness and HWE filters; `qc_2` performs the LD pruning."
]
},
{
@@ -353,16 +794,114 @@
},
"outputs": [],
"source": [
- "# LD prunning and remove related individuals (both ind of a pair)\n# Plink2 has multi-threaded calculation for LD prunning\n[qc_2 (LD pruning)]\n# Window size\nparameter: window = 50\n# Shift window every 10 snps\nparameter: shift = 10\nparameter: r2 = 0.1\nparameter: mac_filter = 0.0\nparameter: other_args = []\n# Use PLINK --bad-ld flag (skip LD pruning diagnostics for regions with extreme LD)\nparameter: bad_ld = False\nstop_if(r2==0)\nplink_command = get_plink_command_prefix(_input)\noutput_format = determine_plink_format(_input)\nmake_command = get_make_command(output_format)\noutput_ext = get_output_extension(output_format, is_prune=True)\nother_args_flags = get_other_args_flags(other_args)\noutput: bed=f'{cwd}/{_input:bn}{output_ext}', prune=f'{cwd}/{_input:bn}.prune.in'\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output[0]:bn}'\nbash: expand= \"${ }\", stderr = f'{_output[0]:n}.stderr', stdout = f'{_output[0]:n}.stdout'\n bash ${modular_script_dir}/data_preprocessing/genotype/GWAS_QC.sh qc \\\n --cwd \"${cwd}\" \\\n --genoFile \"${_input}\" \\\n --plink-command \"${plink_command}\" \\\n --output-format \"${output_format}\" \\\n --make-command \"${make_command}\" \\\n --prune-prefix \"${_output['prune']:nn}\" \\\n --out-prefix \"${_output['bed']:n}\" \\\n --mac-filter ${mac_filter} \\\n --window ${window} \\\n --shift ${shift} \\\n --r2 ${r2} \\\n ${\"--bad-ld\" if bad_ld else \"\"} \\\n ${other_args_flags} \\\n --numThreads ${numThreads}"
+ "# Filter SNPs and select individuals \n",
+ "[qc_no_prune, qc_1 (basic QC filters)]\n",
+ "# minimum MAF filter to use. 0 means do not apply this filter.\n",
+ "parameter: maf_filter = 0.0\n",
+ "# maximum MAF filter to use. 0 means do not apply this filter.\n",
+ "parameter: maf_max_filter = 0.0\n",
+ "# minimum MAC filter to use. 0 means do not apply this filter.\n",
+ "parameter: mac_filter = 0.0\n",
+ "# maximum MAC filter to use. 0 means do not apply this filter.\n",
+ "parameter: mac_max_filter = 0.0 \n",
+ "# Maximum missingess per-variant\n",
+ "parameter: geno_filter = 0.1\n",
+ "# Maximum missingness per-sample\n",
+ "parameter: mind_filter = 0.1\n",
+ "# HWE filter -- a very lenient one\n",
+ "parameter: hwe_filter = 1e-15\n",
+ "# Other PLINK arguments e.g snps_only, write-samples, etc\n",
+ "parameter: other_args = []\n",
+ "# Only output SNP and sample list, rather than the PLINK binary format of subset data\n",
+ "parameter: meta_only = False\n",
+ "# Remove duplicate variants\n",
+ "parameter: rm_dups = False\n",
+ "# Add option to process dosage\n",
+ "parameter: treat_dosage_missing = False\n",
+ "\n",
+ "fail_if(not (keep_samples.is_file() or keep_samples == path('.')), msg = f'Cannot find ``{keep_samples}``')\n",
+ "fail_if(not (keep_variants.is_file() or keep_variants == path('.')), msg = f'Cannot find ``{keep_variants}``')\n",
+ "fail_if(not (remove_samples.is_file() or remove_samples == path('.')), msg = f'Cannot find ``{remove_samples}``')\n",
+ "\n",
+ "input: genoFile, group_by=1\n",
+ "plink_command = get_plink_command_prefix(_input)\n",
+ "output_format = determine_plink_format(_input)\n",
+ "make_command = get_make_command(output_format) if not meta_only else \"--write-snplist --write-samples\"\n",
+ "output_ext = get_output_extension(output_format) if not meta_only else \".snplist\"\n",
+ "other_args_flags = get_other_args_flags(other_args)\n",
+ "output: f'{cwd}/{_input:bn}{(\".\" + name) if name else \"\"}.plink_qc{\".extracted\" if keep_variants.is_file() else \"\"}{output_ext}'\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'\n",
+ "bash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout'\n",
+ " bash ${modular_script_dir}/data_preprocessing/genotype/GWAS_QC.sh qc_no_prune \\\n",
+ " --cwd \"${cwd}\" \\\n",
+ " --genoFile \"${_input}\" \\\n",
+ " --plink-command \"${plink_command}\" \\\n",
+ " --output-format \"${output_format}\" \\\n",
+ " --make-command \"${make_command}\" \\\n",
+ " --out-prefix \"${_output:n}\" \\\n",
+ " --name \"${name}\" \\\n",
+ " --mac-filter ${mac_filter} \\\n",
+ " --maf-filter ${maf_filter} \\\n",
+ " --maf-max-filter ${maf_max_filter} \\\n",
+ " --mac-max-filter ${mac_max_filter} \\\n",
+ " --geno-filter ${geno_filter} \\\n",
+ " --mind-filter ${mind_filter} \\\n",
+ " --hwe-filter ${hwe_filter} \\\n",
+ " ${\"--keep-samples \" + str(keep_samples) if keep_samples.is_file() else \"\"} \\\n",
+ " ${\"--remove-samples \" + str(remove_samples) if remove_samples.is_file() else \"\"} \\\n",
+ " ${\"--exclude-variants \" + str(exclude_variants) if exclude_variants.is_file() else \"\"} \\\n",
+ " ${\"--keep-variants \" + str(keep_variants) if keep_variants.is_file() else \"\"} \\\n",
+ " ${\"--meta-only\" if meta_only else \"\"} \\\n",
+ " ${\"--rm-dups\" if rm_dups else \"\"} \\\n",
+ " ${\"--treat-dosage-missing\" if treat_dosage_missing else \"\"} \\\n",
+ " ${other_args_flags} \\\n",
+ " --numThreads ${numThreads}"
]
},
{
- "cell_type": "markdown",
+ "cell_type": "code",
+ "execution_count": null,
"metadata": {
"kernel": "SoS"
},
+ "outputs": [],
"source": [
- "## Extract genotype based on overlap with phenotype\n\nThis is an auxiliary step to match genotype and phenotype based on the data and look-up table. The look up table should contain two columns: `sample_id`, `genotype_id`. If the look up table is not provided or look-up table file not found, then we will assume the names have already been matched."
+ "# LD prunning and remove related individuals (both ind of a pair)\n",
+ "# Plink2 has multi-threaded calculation for LD prunning\n",
+ "[qc_2 (LD pruning)]\n",
+ "# Window size\n",
+ "parameter: window = 50\n",
+ "# Shift window every 10 snps\n",
+ "parameter: shift = 10\n",
+ "parameter: r2 = 0.1\n",
+ "parameter: mac_filter = 0.0\n",
+ "parameter: other_args = []\n",
+ "# Use PLINK --bad-ld flag (skip LD pruning diagnostics for regions with extreme LD)\n",
+ "parameter: bad_ld = False\n",
+ "stop_if(r2==0)\n",
+ "plink_command = get_plink_command_prefix(_input)\n",
+ "output_format = determine_plink_format(_input)\n",
+ "make_command = get_make_command(output_format)\n",
+ "output_ext = get_output_extension(output_format, is_prune=True)\n",
+ "other_args_flags = get_other_args_flags(other_args)\n",
+ "output: bed=f'{cwd}/{_input:bn}{output_ext}', prune=f'{cwd}/{_input:bn}.prune.in'\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output[0]:bn}'\n",
+ "bash: expand= \"${ }\", stderr = f'{_output[0]:n}.stderr', stdout = f'{_output[0]:n}.stdout'\n",
+ " bash ${modular_script_dir}/data_preprocessing/genotype/GWAS_QC.sh qc \\\n",
+ " --cwd \"${cwd}\" \\\n",
+ " --genoFile \"${_input}\" \\\n",
+ " --plink-command \"${plink_command}\" \\\n",
+ " --output-format \"${output_format}\" \\\n",
+ " --make-command \"${make_command}\" \\\n",
+ " --prune-prefix \"${_output['prune']:nn}\" \\\n",
+ " --out-prefix \"${_output['bed']:n}\" \\\n",
+ " --mac-filter ${mac_filter} \\\n",
+ " --window ${window} \\\n",
+ " --shift ${shift} \\\n",
+ " --r2 ${r2} \\\n",
+ " ${\"--bad-ld\" if bad_ld else \"\"} \\\n",
+ " ${other_args_flags} \\\n",
+ " --numThreads ${numThreads}"
]
},
{
@@ -371,7 +910,9 @@
"kernel": "SoS"
},
"source": [
- "## Anticipated Results\n\nThe pipeline produces output files in the `output/` subdirectory named after the workflow step. Verify success by checking that output files exist and are non-empty. See the **Output** section above for the expected file names and formats."
+ "### Genotype-phenotype sample matching\n",
+ "\n",
+ "An auxiliary step matching genotype to phenotype samples through the optional look-up table; if no table is given, or the file is not found, the names are assumed to match already."
]
},
{
@@ -382,7 +923,26 @@
},
"outputs": [],
"source": [
- "# This workflow extracts overlapping samples for genotype data with phenotype data, and output the filtered sample genotype list as well as sample phenotype list\n[genotype_phenotype_sample_overlap]\n# A genotype fam file\nparameter: genoFile = path\n# A phenotype file, can be bed.gz or tsv\nparameter: phenoFile = path\n# If this file is provided, a genotype/phenotype sample name match will be performed\n# It must contain two column names: genotype_id, sample_id\nparameter: sample_participant_lookup = path(\".\")\ndepends: executable('tabix'), executable('bgzip')\ninput: genoFile, phenoFile\noutput: f'{cwd:a}/{path(_input[1]):bn}.sample_overlap.txt', f'{cwd:a}/{path(_input[1]):bn}.sample_genotypes.txt'\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'\nbash: expand= \"${ }\", stderr = f'{_output[0]:n}.stderr', stdout = f'{_output[0]:n}.stdout'\n bash ${modular_script_dir}/data_preprocessing/genotype/GWAS_QC.sh genotype_phenotype_sample_overlap \\\n --cwd \"${cwd}\" \\\n --genoFile \"${genoFile}\" \\\n --phenoFile \"${phenoFile}\" \\\n --name \"${name}\" \\\n --numThreads ${numThreads}"
+ "# This workflow extracts overlapping samples for genotype data with phenotype data, and output the filtered sample genotype list as well as sample phenotype list\n",
+ "[genotype_phenotype_sample_overlap]\n",
+ "# A genotype fam file\n",
+ "parameter: genoFile = path\n",
+ "# A phenotype file, can be bed.gz or tsv\n",
+ "parameter: phenoFile = path\n",
+ "# If this file is provided, a genotype/phenotype sample name match will be performed\n",
+ "# It must contain two column names: genotype_id, sample_id\n",
+ "parameter: sample_participant_lookup = path(\".\")\n",
+ "depends: executable('tabix'), executable('bgzip')\n",
+ "input: genoFile, phenoFile\n",
+ "output: f'{cwd:a}/{path(_input[1]):bn}.sample_overlap.txt', f'{cwd:a}/{path(_input[1]):bn}.sample_genotypes.txt'\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'\n",
+ "bash: expand= \"${ }\", stderr = f'{_output[0]:n}.stderr', stdout = f'{_output[0]:n}.stdout'\n",
+ " bash ${modular_script_dir}/data_preprocessing/genotype/GWAS_QC.sh genotype_phenotype_sample_overlap \\\n",
+ " --cwd \"${cwd}\" \\\n",
+ " --genoFile \"${genoFile}\" \\\n",
+ " --phenoFile \"${phenoFile}\" \\\n",
+ " --name \"${name}\" \\\n",
+ " --numThreads ${numThreads}"
]
}
],
diff --git a/code/SoS/data_preprocessing/genotype/PCA.ipynb b/code/SoS/data_preprocessing/genotype/PCA.ipynb
index 82fba5af7..16ac94c3a 100644
--- a/code/SoS/data_preprocessing/genotype/PCA.ipynb
+++ b/code/SoS/data_preprocessing/genotype/PCA.ipynb
@@ -13,15 +13,41 @@
},
{
"cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
+ "metadata": {},
"source": [
- "## Description\n",
+ "## Overview\n",
+ "\n",
+ "Population structure is the classic confounder in genetic association: if ancestry\n",
+ "correlates with both genotype and phenotype, unadjusted tests return associations that are\n",
+ "real but not causal. The remedy is to compute principal components of the genotype matrix\n",
+ "and carry the leading ones as covariates.\n",
+ "\n",
+ "Components are computed on **unrelated** individuals and the remaining related samples are\n",
+ "projected back into that space, so relatives cannot distort the axes but every sample still\n",
+ "gets coordinates. The sequence is: remove related individuals, LD-prune the variants, run\n",
+ "PCA on the unrelated set, then exclude PCA-space outliers. Relatedness estimation and\n",
+ "sample QC happen upstream in `GWAS_QC.ipynb`.\n",
+ "\n",
+ "The workflows cover the whole job rather than just the decomposition: `pca_plink` and\n",
+ "`flashpca` compute components, `project_samples` places new samples onto axes computed\n",
+ "elsewhere, `plot_pca` produces the scree and scatter plots you inspect, and\n",
+ "`detect_outliers` flags samples that sit far from any cluster.\n",
+ "\n",
+ "Steps to generate a PCA include:\n",
"\n",
- "This notebook performs PCA on genotype data of **unrelated** individuals and projects the remaining (related) samples back into that PCA space. The main steps are: remove related individuals, LD-prune the variants, run PCA on the unrelated set, and exclude PCA-space outliers.\n",
+ "- removing related individuals\n",
+ "- pruning variants in linkage disequilibrium (LD)\n",
+ "- performing PCA analysis on the genotypes of the unrelated individuals\n",
+ "- excluding outlier samples in the PCA space for individuals of homogeneous self-reported ancestry. These outliers may suggest poor genotyping quality or distant relatedness.\n",
"\n",
- "Relatedness estimation and sample QC are done upstream in `GWAS_QC.ipynb`."
+ "Limitations:\n",
+ "\n",
+ "1. Some of the PCs may capture LD structure rather than population structure, which decreases power to detect associations in these regions of high LD.\n",
+ "2. When projecting a new study dataset onto the PCA space computed from a reference dataset, the projected PCs are shrunk toward 0 in the new dataset.\n",
+ "3. PC scores may capture outliers arising from family structure, population structure or other reasons. Detecting and removing these individuals can be worthwhile, either to maximise the population structure captured by the PCA when only a few outliers are involved, or to restrict analyses to genetically homogeneous samples.\n",
+ "\n",
+ "**When to run it.** After genotype QC and LD pruning, before covariate preprocessing; the\n",
+ "components become part of the covariate file used by the association scan."
]
},
{
@@ -30,15 +56,27 @@
"kernel": "SoS"
},
"source": [
- "## Input Files\n",
+ "## Input\n",
"\n",
- "| File | Description |\n",
- "|------|-------------|\n",
- "| `output/gwas_qc/genotype/protocol_example.genotype.merged.plink_qc.protocol_example.king.unrelated.plink_qc.prune.bed` | LD-pruned PLINK bundle for **unrelated** individuals (from GWAS_QC). PCA is computed on these samples. |\n",
- "| `output/gwas_qc/kinship/protocol_example.genotype.merged.plink_qc.protocol_example.king.related.bed` | PLINK bundle for **related** individuals (from GWAS_QC), projected back onto the PCA model. |\n",
- "| `input/covariate/protocol_example.pca_pheno.txt` | Phenotype/label table with an `IID` column (plus optional `FID`) used to label/colour the projected samples. A PLINK `.fam` file may be used instead. |\n",
+ "- `--genoFile` `output/gwas_qc/genotype/protocol_example.genotype.merged.plink_qc.protocol_example.king.unrelated.plink_qc.prune.bed` (the LD-pruned PLINK bundle for the **unrelated** individuals, as produced by GWAS QC. PCA is computed on these samples)\n",
+ "- `--phenoFile`: a phenotype or label table carrying an `IID` column, optionally `FID`, used to label and colour the projected samples. A PLINK `.fam` may be used instead. Its extra columns are what `--pop-col` and `--label-col` select from. Example `input/covariate/protocol_example.pca_pheno.txt`, 60 samples and 7 columns:\n",
"\n",
- "Genotypes must be in PLINK format (common variants, LD pruned) and split into related and unrelated sets. The phenotype file may also carry population/disease columns for labelling the PCA plots."
+ " ```\n",
+ " FID IID msex age_death pmi study race\n",
+ " SAMPLE_001 SAMPLE_001 1 90.97 10.57 1 1\n",
+ " SAMPLE_002 SAMPLE_002 1 80.24 7.87 0 1\n",
+ " SAMPLE_003 SAMPLE_003 1 83.9 9.93 0 1\n",
+ " ```\n",
+ "- `--pca-model`: the fitted PCA model to project onto, used by the projection step rather than the fitting step.\n",
+ "- `--name`: the stem of the output files. Required.\n",
+ "- `--cwd`: the directory outputs are written to.\n",
+ "- `--k`: how many principal components to compute, `20` by default.\n",
+ "- `--maha-k`: how many components the Mahalanobis outlier distance is computed over, `5` by default.\n",
+ "- `--pop-col` and `--pops`: the phenotype column naming each sample's population and the subset of populations to analyse, for admixed or multi-ancestry data.\n",
+ "- `--label-col`: the phenotype column used to colour points in the PCA plots.\n",
+ "- `--keep-samples` and `--remove-samples`: sample lists to restrict or exclude before fitting.\n",
+ "- `--keep-variants`: a variant list to restrict to.\n",
+ "- `--maf-filter`, `--mac-filter`, `--geno-filter` and `--mind-filter`: the PLINK frequency, count, variant-missingness and sample-missingness thresholds applied before PCA."
]
},
{
@@ -47,16 +85,31 @@
"kernel": "SoS"
},
"source": [
- "## Output Files\n",
+ "## Output\n",
+ "\n",
+ "- `{cwd}/{name}.pca.rds` and `{cwd}/{name}.pca.txt` - the fitted PCA model and the PC scores for the unrelated samples. Every path below sits under `--cwd` and shares the stem set by `--name`.\n",
+ "- `{name}.pca.scree.txt` and `{name}.pca.scree.png` - variance explained per component, as a table and a plot. The table is what `covariate_formatting` reads to choose how many PCs to keep:\n",
+ "\n",
+ " ```\n",
+ " PCs PVE PVE_cum\n",
+ " 1 0.05 0.05\n",
+ " 2 0.05 0.1\n",
+ " ```\n",
+ "\n",
+ "- `{name}.pca.mahalanobis.rds`, `.mahalanobis_hist.png` and `.mahalanobis_qq.png` - the per-population Mahalanobis distances and their diagnostic plots.\n",
+ "- `{name}.pca.outliers` - the samples flagged as PC outliers, an index and a sample ID per line:\n",
+ "\n",
+ " ```\n",
+ " 0 SAMPLE_001\n",
+ " 0 SAMPLE_026\n",
+ " 0 SAMPLE_050\n",
+ " ```\n",
+ "\n",
+ "- `{name}.pca.pc.png` - the PCA plot.\n",
+ "- `{name}.projected.rds`, `.projected.txt` and `.projected.pc.png` - the related individuals projected back onto the model, and the plot including them.\n",
+ "- `{name}.pca.analysis_summary.md` - a written summary of the run.\n",
"\n",
- "| File | Description |\n",
- "|------|-------------|\n",
- "| `output/pca_uf/*.pca.rds` | PCA model + PC scores for the unrelated samples (RDS). |\n",
- "| `output/pca_uf/*.pca.txt` | PC scores table for the unrelated samples. |\n",
- "| `output/pca_uf/*.pca.scree.png` | Scree plot of variance explained. |\n",
- "| `output/pca_uf/*.pca.mahalanobis.*`, `*.pca.outliers` | Mahalanobis distances per population and the list of PC outliers. |\n",
- "| `output/pca_uf/*.projected.rds` / `*.projected.txt` | Projected PC scores for the related individuals. |\n",
- "| `output/pca_uf/*.projected.pc.png` | PCA plot including the projected related samples. |"
+ "The `pca_plink` sanity-check workflow declares `{genoFile}.pca.eigenvec`, but no `.eigenvec` file ships in `output/pca_uf`; only the flashpca products are present."
]
},
{
@@ -67,9 +120,7 @@
"source": [
"## Minimal Working Example\n",
"\n",
- "The data can be found on [Synapse](https://www.synapse.org/#!Synapse:syn69670658/files/).\n",
- "\n",
- "**Note: parameters set for the MWE are meant to let the MWE work to show the workflow procedures. They may be unrealistic and should not be used in practice. The pipeline has reasonable default values for what we suggest to use in practice for most of the parameters.**"
+ "**Note: parameters set for the MWE are meant to let the MWE work to show the workflow procedures. They may be unrealistic and should not be used in practice. The pipeline has reasonable default values for what is suggested in practice for most of the parameters.**"
]
},
{
@@ -80,7 +131,16 @@
"source": [
"### **Step 1.** Estimate kinship in the sample (prerequisite)\n",
"\n",
- "Before PCA we need to know which individuals are related, because PCA must be computed on an **unrelated** subset and the related individuals are projected back afterwards. This step runs the `king` workflow from GWAS_QC to estimate pairwise kinship and split the samples into *unrelated* and *related* sets. It is an upstream prerequisite \u2014 its outputs (the `king.unrelated` and `king.related` PLINK bundles) feed the PCA steps below."
+ "Before PCA it is necessary to know which individuals are related, because PCA must be computed on an **unrelated** subset and the related individuals are projected back afterwards. This step runs the `king` workflow from GWAS_QC to estimate pairwise kinship and split the samples into *unrelated* and *related* sets. It is an upstream prerequisite — its outputs (the `king.unrelated` and `king.related` PLINK bundles) feed the PCA steps below."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
]
},
{
@@ -94,7 +154,11 @@
},
"outputs": [],
"source": [
- "sos run pipeline/GWAS_QC.ipynb king \\\n --cwd output/gwas_qc/kinship \\\n --genoFile output/gwas_qc/plink/protocol_example.genotype.merged.plink_qc.bed \\\n --name protocol_example.king \\\n --keep-samples output/gwas_qc/genotype/protocol_example.rnaseq.bed.sample_genotypes.txt"
+ "sos run pipeline/GWAS_QC.ipynb king \\\n",
+ " --cwd output/gwas_qc/kinship \\\n",
+ " --genoFile output/gwas_qc/plink/protocol_example.genotype.merged.plink_qc.bed \\\n",
+ " --name protocol_example.king \\\n",
+ " --keep-samples output/gwas_qc/genotype/protocol_example.rnaseq.bed.sample_genotypes.txt"
]
},
{
@@ -105,7 +169,7 @@
"source": [
"### **Step 2.** Sample selection and QC of the genotype data (prerequisite)\n",
"\n",
- "QC the genotypes that will go into PCA: filter on MAF, sample/variant missingness and Hardy-Weinberg equilibrium, and LD-prune the variants. You can restrict to one population or to the related / unrelated split. Here we QC and LD-prune the **unrelated** individuals to build the PCA reference, and separately extract the same variants for the related individuals so they can be projected later. These `qc` / `qc_no_prune` calls come from GWAS_QC and are upstream prerequisites for the PCA steps."
+ "QC the genotypes that will go into PCA: filter on MAF, sample/variant missingness and Hardy-Weinberg equilibrium, and LD-prune the variants. You can restrict to one population or to the related / unrelated split. Here the QC and LD-pruning are applied to the **unrelated** individuals to build the PCA reference, and separately extract the same variants for the related individuals so they can be projected later. These `qc` / `qc_no_prune` calls come from GWAS_QC and are upstream prerequisites for the PCA steps."
]
},
{
@@ -118,6 +182,15 @@
"#### Variant level and sample level QC on unrelated individuals, in preparation for PCA analysis"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ]
+ },
{
"cell_type": "code",
"execution_count": null,
@@ -129,7 +202,10 @@
},
"outputs": [],
"source": [
- "sos run pipeline/GWAS_QC.ipynb qc \\\n --cwd output/gwas_qc/genotype \\\n --genoFile output/gwas_qc/kinship/protocol_example.genotype.merged.plink_qc.protocol_example.king.unrelated.bed \\\n --mac-filter 5"
+ "sos run pipeline/GWAS_QC.ipynb qc \\\n",
+ " --cwd output/gwas_qc/genotype \\\n",
+ " --genoFile output/gwas_qc/kinship/protocol_example.genotype.merged.plink_qc.protocol_example.king.unrelated.bed \\\n",
+ " --mac-filter 5"
]
},
{
@@ -141,6 +217,15 @@
"Extract the previously selected (LD-pruned) variants from the **related** individuals, applying only a sample-level missingness filter so the related samples carry exactly the same variant set as the unrelated reference."
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ]
+ },
{
"cell_type": "code",
"execution_count": null,
@@ -152,7 +237,12 @@
},
"outputs": [],
"source": [
- "sos run pipeline/GWAS_QC.ipynb qc_no_prune \\\n --cwd output/pca_related \\\n --genoFile output/gwas_qc/kinship/protocol_example.genotype.merged.plink_qc.protocol_example.king.related.bed \\\n --geno-filter 0 --mind-filter 0.1 --maf-filter 0 \\\n --keep-variants output/gwas_qc/genotype/protocol_example.genotype.merged.plink_qc.protocol_example.king.unrelated.plink_qc.prune.in \\\n --name for_pca"
+ "sos run pipeline/GWAS_QC.ipynb qc_no_prune \\\n",
+ " --cwd output/pca_related \\\n",
+ " --genoFile output/gwas_qc/kinship/protocol_example.genotype.merged.plink_qc.protocol_example.king.related.bed \\\n",
+ " --geno-filter 0 --mind-filter 0.1 --maf-filter 0 \\\n",
+ " --keep-variants output/gwas_qc/genotype/protocol_example.genotype.merged.plink_qc.protocol_example.king.unrelated.plink_qc.prune.in \\\n",
+ " --name for_pca"
]
},
{
@@ -175,6 +265,15 @@
"The `--name` argument tags all output files; point `--genoFile` at the LD-pruned unrelated bed produced upstream."
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ]
+ },
{
"cell_type": "code",
"execution_count": null,
@@ -186,7 +285,10 @@
},
"outputs": [],
"source": [
- "sos run pipeline/PCA.ipynb flashpca \\\n --cwd output/pca_uf \\\n --genoFile output/gwas_qc/genotype/protocol_example.genotype.merged.plink_qc.protocol_example.king.unrelated.plink_qc.prune.bed \\\n --name protocol_example"
+ "sos run pipeline/PCA.ipynb flashpca \\\n",
+ " --cwd output/pca_uf \\\n",
+ " --genoFile output/gwas_qc/genotype/protocol_example.genotype.merged.plink_qc.protocol_example.king.unrelated.plink_qc.prune.bed \\\n",
+ " --name protocol_example"
]
},
{
@@ -195,7 +297,7 @@
"kernel": "SoS"
},
"source": [
- "**What you should see:** the workflow runs `flashpca_1` (PCA), then `detect_outliers` (Mahalanobis distance per population) and `plot_pca`, finishing with \"executed successfully with 5 completed steps\". The PCA model and PC scores are written to the `--cwd` directory. The scree plot below shows the proportion of variance explained by each PC \u2014 use it to decide how many PCs to keep."
+ "**What you should see:** the workflow runs `flashpca_1` (PCA), then `detect_outliers` (Mahalanobis distance per population) and `plot_pca`, finishing with \"executed successfully with 5 completed steps\". The PCA model and PC scores are written to the `--cwd` directory. The scree plot below shows the proportion of variance explained by each PC — use it to decide how many PCs to keep."
]
},
{
@@ -217,7 +319,16 @@
"source": [
"### **Step 4.** Project the related individuals onto the PCA model\n",
"\n",
- "The related individuals were held out of the PCA, so we now project them onto the model fitted from the unrelated samples. First reduce the related genotypes to **exactly** the same pruned variant set used to build the model (the `plink2 --extract` cell below) \u2014 otherwise the projection errors with \"Input number of variants should be the same as used in the previous PCA model\". Then run `project_samples`, passing a phenotype table (must contain an `IID` column) and, if available, a population/ethnicity column via `--label-col` / `--pop-col` (here the `race` column) so the projected samples can be coloured and the outlier detection can run."
+ "The related individuals were held out of the PCA, so they are now projected onto the model fitted from the unrelated samples. First reduce the related genotypes to **exactly** the same pruned variant set used to build the model (the `plink2 --extract` cell below) — otherwise the projection errors with \"Input number of variants should be the same as used in the previous PCA model\". Then run `project_samples`, passing a phenotype table (must contain an `IID` column) and, if available, a population/ethnicity column via `--label-col` / `--pop-col` (here the `race` column) so the projected samples can be coloured and the outlier detection can run."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
]
},
{
@@ -231,7 +342,15 @@
},
"outputs": [],
"source": [
- "sos run pipeline/PCA.ipynb project_samples \\\n --cwd output/pca_uf \\\n --genoFile output/pca_related/protocol_example.genotype.merged.plink_qc.protocol_example.king.related.for_pca.plink_qc.extracted.bed \\\n --phenoFile input/covariate/protocol_example.pca_pheno.txt \\\n --pca-model output/pca_uf/protocol_example.genotype.merged.plink_qc.protocol_example.king.unrelated.plink_qc.prune.protocol_example.pca.rds \\\n --label-col race \\\n --pop-col race \\\n --name protocol_example \\\n --maha-k 2"
+ "sos run pipeline/PCA.ipynb project_samples \\\n",
+ " --cwd output/pca_uf \\\n",
+ " --genoFile output/pca_related/protocol_example.genotype.merged.plink_qc.protocol_example.king.related.for_pca.plink_qc.extracted.bed \\\n",
+ " --phenoFile input/covariate/protocol_example.pca_pheno.txt \\\n",
+ " --pca-model output/pca_uf/protocol_example.genotype.merged.plink_qc.protocol_example.king.unrelated.plink_qc.prune.protocol_example.pca.rds \\\n",
+ " --label-col race \\\n",
+ " --pop-col race \\\n",
+ " --name protocol_example \\\n",
+ " --maha-k 2"
]
},
{
@@ -245,6 +364,15 @@
"%preview output/pca_uf/protocol_example.pca_pheno.pca.projected.pc.png"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ]
+ },
{
"cell_type": "code",
"execution_count": null,
@@ -256,7 +384,11 @@
},
"outputs": [],
"source": [
- "sos run pipeline/GWAS_QC.ipynb qc_no_prune \\\n --cwd output/pca_related \\\n --genoFile output/pca_related/protocol_example.genotype.merged.plink_qc.protocol_example.king.related.for_pca.plink_qc.extracted.bed \\\n --remove-samples output/pca_uf/protocol_example.pca_pheno.pca.projected.outliers \\\n --name no_outlier"
+ "sos run pipeline/GWAS_QC.ipynb qc_no_prune \\\n",
+ " --cwd output/pca_related \\\n",
+ " --genoFile output/pca_related/protocol_example.genotype.merged.plink_qc.protocol_example.king.related.for_pca.plink_qc.extracted.bed \\\n",
+ " --remove-samples output/pca_uf/protocol_example.pca_pheno.pca.projected.outliers \\\n",
+ " --name no_outlier"
]
},
{
@@ -279,6 +411,15 @@
"Finally, merge back related and unrelated individuals as finalized genotype in PLINK format,"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ]
+ },
{
"cell_type": "code",
"execution_count": null,
@@ -290,7 +431,11 @@
},
"outputs": [],
"source": [
- "sos run pipeline/genotype_formatting.ipynb merge_plink \\\n --genoFile output/gwas_qc/genotype/protocol_example.genotype.merged.plink_qc.protocol_example.king.unrelated.plink_qc.prune.bed \\\n output/pca_related/protocol_example.genotype.merged.plink_qc.protocol_example.king.related.for_pca.plink_qc.extracted.no_outlier.plink_qc.bed \\\n --cwd output/genotype_final \\\n --name protocol_example.qced"
+ "sos run pipeline/genotype_formatting.ipynb merge_plink \\\n",
+ " --genoFile output/gwas_qc/genotype/protocol_example.genotype.merged.plink_qc.protocol_example.king.unrelated.plink_qc.prune.bed \\\n",
+ " output/pca_related/protocol_example.genotype.merged.plink_qc.protocol_example.king.related.for_pca.plink_qc.extracted.no_outlier.plink_qc.bed \\\n",
+ " --cwd output/genotype_final \\\n",
+ " --name protocol_example.qced"
]
},
{
@@ -308,20 +453,9 @@
"kernel": "SoS"
},
"source": [
- "## Optional \u2014 Analysis by population (admixed / multi-ancestry data)\n",
- "\n",
- "The Steps 1\u20134 above produce a single PCA for the whole cohort, which is appropriate for a **homogeneous** population. If your cohort contains **multiple ancestry groups**, run the per-population workflow below instead: split the samples by population, then repeat the same QC \u2192 PCA \u2192 projection steps **separately within each population**.\n",
+ "### Analysis by population (admixed / multi-ancestry data)\n",
"\n",
- "The sub-steps below mirror Steps 1\u20134 exactly, just looped over each population (`for i in race1 race3`). Follow them in order:\n",
- "\n",
- "| Sub-step | What it does |\n",
- "| --- | --- |\n",
- "| **P0. Split by population** | Write a per-population sample-ID list |\n",
- "| **P1. QC unrelated** | Variant/sample QC within each population (unrelated samples) |\n",
- "| **P2. Extract variants in related** | Keep the P1 pruned variants in the related samples |\n",
- "| **P3. PCA on unrelated** | Run `flashpca` per population |\n",
- "| **P4. Project related** | Project related samples onto each population\u2019s PCA |\n",
- "| **P5. Finalize** | Per-population QC to finalize genotypes |"
+ "The Steps 1–4 above produce a single PCA for the whole cohort, which is appropriate for a **homogeneous** population. If your cohort contains **multiple ancestry groups**, run the per-population workflow below instead: split the samples by population, then repeat the same QC → PCA → projection steps **separately within each population**.\n"
]
},
{
@@ -333,7 +467,17 @@
},
"outputs": [],
"source": [
- "# Build per-population sample keep-lists from the phenotype table.\n# One file per ancestry group, named output/pca_uf/protocol_example.ID..txt\n# Each file holds two columns (FID, IID) with no header, matching plink's --keep format.\nsetwd(\"/mnt/lustre/lab/gwang/home/rl3328/evo2/xqtl-protocol\")\npheno <- read.table(\"input/covariate/protocol_example.pca_pheno.txt\", header = TRUE, stringsAsFactors = FALSE)\n\nif (all(pheno$FID == pheno$IID)) pheno$FID <- \"0\"\n\nfor (i in 1:3) {\n race <- subset(pheno, race == i)\n race_id <- cbind(race$FID, race$IID)\n write.table(race_id,\n paste0(\"output/pca_uf/protocol_example.ID.\", i, \".txt\"),\n quote = FALSE, sep = \"\\t\", col.names = FALSE, row.names = FALSE)\n}"
+ "pheno <- read.table(\"input/covariate/protocol_example.pca_pheno.txt\", header = TRUE, stringsAsFactors = FALSE)\n",
+ "\n",
+ "if (all(pheno$FID == pheno$IID)) pheno$FID <- \"0\"\n",
+ "\n",
+ "for (i in 1:3) {\n",
+ " race <- subset(pheno, race == i)\n",
+ " race_id <- cbind(race$FID, race$IID)\n",
+ " write.table(race_id,\n",
+ " paste0(\"output/pca_uf/protocol_example.ID.\", i, \".txt\"),\n",
+ " quote = FALSE, sep = \"\\t\", col.names = FALSE, row.names = FALSE)\n",
+ "}"
]
},
{
@@ -342,9 +486,9 @@
"kernel": "SoS"
},
"source": [
- "### P1 \u2014 QC on unrelated samples, per population\n",
+ "### Step 1. QC on unrelated samples, per population\n",
"\n",
- "Same as Step 2 (QC), but looped over each population. Below we only show Populations 1 and 3 as an example. **Next:** once each population has a QC\u2019d unrelated set, extract those variants from the related samples (P2)."
+ "Same as Step 2 previously (QC), but looped over each population. Below only Populations 1 and 3 are shown as an example. "
]
},
{
@@ -358,7 +502,15 @@
},
"outputs": [],
"source": [
- "for i in 3; do\n sos run pipeline/GWAS_QC.ipynb qc \\\n --cwd output/pca_uf \\\n --genoFile output/gwas_qc/genotype/protocol_example.genotype.merged.plink_qc.protocol_example.king.unrelated.plink_qc.bed \\\n --keep-samples output/pca_uf/protocol_example.ID.$i.txt \\\n --mac-filter 5 \\\n --bad-ld True \\\n --name pop_$i\ndone"
+ "for i in 3; do\n",
+ " sos run pipeline/GWAS_QC.ipynb qc \\\n",
+ " --cwd output/pca_uf \\\n",
+ " --genoFile output/gwas_qc/genotype/protocol_example.genotype.merged.plink_qc.protocol_example.king.unrelated.plink_qc.bed \\\n",
+ " --keep-samples output/pca_uf/protocol_example.ID.$i.txt \\\n",
+ " --mac-filter 5 \\\n",
+ " --bad-ld True \\\n",
+ " --name pop_$i\n",
+ "done"
]
},
{
@@ -367,11 +519,11 @@
"kernel": "SoS"
},
"source": [
- "### P2 \u2014 Extract the selected variants from related samples, per population\n",
+ "### Step 2. Extract the selected variants from related samples, per population\n",
"\n",
- "Same as the extract part of Step 4, looped per population (sample-level missingness filter only).\n",
+ "Same as the extract part of Step 4 above, looped per population (sample-level missingness filter only).\n",
"\n",
- "> Note: Population 1 may have **no related samples**, in which case its `*.related.for_pca_race1.filtered.extracted.stderr` log will say `Error: No people remaining after --keep.` That is expected \u2014 just continue with the populations that do have related samples. **Next:** run PCA on each population\u2019s unrelated set (P3)."
+ "> Note: Population 1 may have **no related samples**, in which case its `*.related.for_pca_race1.filtered.extracted.stderr` log will say `Error: No people remaining after --keep.` That is expected — just continue with the populations that do have related samples. "
]
},
{
@@ -385,7 +537,15 @@
},
"outputs": [],
"source": [
- "for i in 3; do\n sos run pipeline/GWAS_QC.ipynb qc_no_prune \\\n --cwd output/pca_uf \\\n --genoFile output/pca_related/protocol_example.genotype.merged.plink_qc.protocol_example.king.related.for_pca.plink_qc.extracted.bed \\\n --keep-variants output/pca_uf/protocol_example.genotype.merged.plink_qc.protocol_example.king.unrelated.plink_qc.pop_$i.plink_qc.prune.in \\\n --keep-samples output/pca_uf/protocol_example.ID.$i.txt \\\n --maf-filter 0 --geno-filter 0 --mind-filter 0.1 --hwe-filter 0 \\\n --name pop_$i\ndone"
+ "for i in 3; do\n",
+ " sos run pipeline/GWAS_QC.ipynb qc_no_prune \\\n",
+ " --cwd output/pca_uf \\\n",
+ " --genoFile output/pca_related/protocol_example.genotype.merged.plink_qc.protocol_example.king.related.for_pca.plink_qc.extracted.bed \\\n",
+ " --keep-variants output/pca_uf/protocol_example.genotype.merged.plink_qc.protocol_example.king.unrelated.plink_qc.pop_$i.plink_qc.prune.in \\\n",
+ " --keep-samples output/pca_uf/protocol_example.ID.$i.txt \\\n",
+ " --maf-filter 0 --geno-filter 0 --mind-filter 0.1 --hwe-filter 0 \\\n",
+ " --name pop_$i\n",
+ "done"
]
},
{
@@ -394,9 +554,9 @@
"kernel": "SoS"
},
"source": [
- "### P3 \u2014 Run PCA (flashpca) on unrelated samples, per population\n",
+ "### Step 3. Run PCA (flashpca) on unrelated samples, per population\n",
"\n",
- "Same as Step 3, looped per population. Here the number of PCs is set to 5 (`--k 5`) because each population\u2019s sample size is small. **Next:** project the related samples onto each population\u2019s PCA model (P4)."
+ "Same as Step 3 above, looped per population. Here the number of PCs is set to 5 (`--k 5`) because each population’s sample size is small. "
]
},
{
@@ -410,7 +570,17 @@
},
"outputs": [],
"source": [
- "for i in 3; do\n sos run pipeline/PCA.ipynb flashpca \\\n --name pop_$i \\\n --cwd output/pca_uf \\\n --genoFile output/pca_uf/protocol_example.genotype.merged.plink_qc.protocol_example.king.unrelated.plink_qc.pop_$i.plink_qc.prune.bed \\\n --phenoFile input/covariate/protocol_example.pca_pheno.txt \\\n --label-col race \\\n --pop-col race \\\n --maha-k 2 \\\n --k 5\ndone"
+ "for i in 3; do\n",
+ " sos run pipeline/PCA.ipynb flashpca \\\n",
+ " --name pop_$i \\\n",
+ " --cwd output/pca_uf \\\n",
+ " --genoFile output/pca_uf/protocol_example.genotype.merged.plink_qc.protocol_example.king.unrelated.plink_qc.pop_$i.plink_qc.prune.bed \\\n",
+ " --phenoFile input/covariate/protocol_example.pca_pheno.txt \\\n",
+ " --label-col race \\\n",
+ " --pop-col race \\\n",
+ " --maha-k 2 \\\n",
+ " --k 5\n",
+ "done"
]
},
{
@@ -419,9 +589,9 @@
"kernel": "SoS"
},
"source": [
- "### P4 \u2014 Project related samples onto each population\u2019s PCA, per population\n",
+ "### Step 4. Project related samples onto each population’s PCA, per population\n",
"\n",
- "Same as Step 4 (projection), looped per population. Only populations that actually have related samples are projected (here, Population 3 only). **Next:** finalize genotypes per population (P5)."
+ "Same as Step 4 in homogenous dataset PCA (projection), looped per population. Only populations that actually have related samples are projected (here, Population 3 only). "
]
},
{
@@ -435,7 +605,18 @@
},
"outputs": [],
"source": [
- "for i in 3; do\n sos run pipeline/PCA.ipynb project_samples \\\n --name pop_$i \\\n --cwd output/pca_uf \\\n --genoFile output/pca_uf/protocol_example.genotype.merged.plink_qc.protocol_example.king.related.for_pca.plink_qc.extracted.pop_$i.plink_qc.extracted.bed \\\n --phenoFile input/covariate/protocol_example.pca_pheno.txt \\\n --pca-model output/pca_uf/protocol_example.pca_pheno.pop_$i.pca.rds \\\n --label-col race \\\n --pop-col race \\\n --maha-k 2 \\\n --k 5\ndone"
+ "for i in 3; do\n",
+ " sos run pipeline/PCA.ipynb project_samples \\\n",
+ " --name pop_$i \\\n",
+ " --cwd output/pca_uf \\\n",
+ " --genoFile output/pca_uf/protocol_example.genotype.merged.plink_qc.protocol_example.king.related.for_pca.plink_qc.extracted.pop_$i.plink_qc.extracted.bed \\\n",
+ " --phenoFile input/covariate/protocol_example.pca_pheno.txt \\\n",
+ " --pca-model output/pca_uf/protocol_example.pca_pheno.pop_$i.pca.rds \\\n",
+ " --label-col race \\\n",
+ " --pop-col race \\\n",
+ " --maha-k 2 \\\n",
+ " --k 5\n",
+ "done"
]
},
{
@@ -465,9 +646,7 @@
"kernel": "SoS"
},
"source": [
- "### P5 \u2014 Finalize genotypes per population\n",
- "\n",
- "Same as the **Finalize genotype QC by PCA** step for a homogeneous population, applied per population and taking the detected outliers into account. See the `GWAS_QC.ipynb` documentation for the available QC options and recommendations. This is the end of the per-population workflow."
+ "Finalizing genotypes per population is the same as the **Finalize genotype QC by PCA** step for a homogeneous population, applied per population and taking the detected outliers into account. It is not run from this notebook; see the `GWAS_QC.ipynb` documentation for the available QC options and recommendations."
]
},
{
@@ -482,12 +661,127 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "kernel": "Bash"
- },
+ "metadata": {},
"outputs": [],
"source": [
- "sos run PCA.ipynb -h"
+ "sos run pipeline/PCA.ipynb -h"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "```\n",
+ "usage: sos run pipeline/PCA.ipynb [workflow_name | -t targets] [options] [workflow_options]\n",
+ " workflow_name: Single or combined workflows defined in this script\n",
+ " targets: One or more targets to generate\n",
+ " options: Single-hyphen sos parameters (see \"sos run -h\" for details)\n",
+ " workflow_options: Double-hyphen workflow-specific parameters\n",
+ "\n",
+ "Workflows:\n",
+ " pca_plink\n",
+ " flashpca\n",
+ " project_samples\n",
+ " plot_pca\n",
+ " detect_outliers\n",
+ "\n",
+ "Global Workflow Options:\n",
+ " --modular-script-dir code/script (as path)\n",
+ " --cwd output (as path)\n",
+ " the output directory for generated files\n",
+ " --name VAL (as str, required)\n",
+ " A string to identify your analysis run\n",
+ " --pop-col ''\n",
+ " Name of the population column in the phenoFile\n",
+ " --pops (as list)\n",
+ " Name of the populations (from the population column) you\n",
+ " would like to plot and show on the PCA plot\n",
+ " --label-col ''\n",
+ " Name of the color label column in the phenoFile; can be\n",
+ " the same as population column. Can also be a separate\n",
+ " column eg a \"super population\" column as a way to enable\n",
+ " you to combine selected populations based on another\n",
+ " column.\n",
+ " --k 20 (as int)\n",
+ " Number of Principal Components to output,must be\n",
+ " consistant between flashpca run and project samples run\n",
+ " (flashpca partial PCA method).\n",
+ " --maha-k 5 (as int)\n",
+ " Number of Principal Components based on which outliers\n",
+ " should be evaluated. Default is 5 but this should be\n",
+ " based on examine the scree plot\n",
+ " --[no-]homogeneous (default to False)\n",
+ " Homogeneity of populations. Set to --homogeneous when\n",
+ " true and --no-homogeneous when false\n",
+ " --job-size 1 (as int)\n",
+ " For cluster jobs, number commands to run per job\n",
+ " --walltime 5h\n",
+ " Wall clock time expected\n",
+ " --mem 16G\n",
+ " Memory expected\n",
+ " --numThreads 10 (as int)\n",
+ " Number of threads\n",
+ "\n",
+ "Sections\n",
+ " pca_plink: PCA command with PLINK, as a sanity check\n",
+ " Workflow Options:\n",
+ " --genoFile VAL (as path, required)\n",
+ " PLINK binary file\n",
+ " flashpca_1: Run PCA analysis using flashpca\n",
+ " Workflow Options:\n",
+ " --genoFile VAL (as path, required)\n",
+ " Plink binary file\n",
+ " --phenoFile path(f'{genoFile}'.replace(\".bed\", \".fam\").replace(\".pgen\", \".psam\"))\n",
+ "\n",
+ " The phenotypic file\n",
+ " --min-pop-size 2 (as int)\n",
+ " minimum population size to consider in the analysis\n",
+ " --stand binom2\n",
+ " How to standardize X before PCA\n",
+ " project_samples_1: Project back to PCA model additional samples\n",
+ " Workflow Options:\n",
+ " --genoFile VAL (as path, required)\n",
+ " Plink binary file\n",
+ " --phenoFile path(f'{genoFile}'.replace(\".bed\", \".fam\").replace(\".pgen\", \".psam\"))\n",
+ "\n",
+ " The phenotypic file\n",
+ " --stand binom2\n",
+ " How to standardize X before PCA\n",
+ " --pca-model f'{cwd}/{phenoFile:bn}{(\".\"+name) if name else \"\"}.{(suffix+\".\") if suffix != \"\" else \"\"}pca.rds'\n",
+ "\n",
+ " plot_pca: Plot PCA results. Can be used independently as\n",
+ " \"plot_pca\" or combined with other workflow as eg\n",
+ " \"flashpca+plot_pca\"\n",
+ " Workflow Options:\n",
+ " --outlier-file . (as path)\n",
+ " --plot-data VAL (as path, required)\n",
+ " --min-axis ''\n",
+ " --max-axis ''\n",
+ " detect_outliers: Calculate Mahalanobis distance per population and report\n",
+ " outliers\n",
+ " Workflow Options:\n",
+ " --prob 0.997 (as float)\n",
+ " Set the probability to remove outliers eg 0.95 or 0.997\n",
+ " --pval 0.05 (as float)\n",
+ " Mahalanobis distance p-value cutoff\n",
+ " --[no-]robust (default to True)\n",
+ " Robust Mahalanobis to outliers\n",
+ " --pca-result VAL (as path, required)\n",
+ " flashpca_2, project_samples_2:\n",
+ " Workflow Options:\n",
+ " --prob 0.997 (as float)\n",
+ " Set the probability to remove outliers eg 0.95 or 0.997\n",
+ " --[no-]robust (default to True)\n",
+ " Robust Mahalanobis to outliers\n",
+ " flashpca_3, project_samples_3:\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Workflow implementation"
]
},
{
@@ -498,7 +792,121 @@
},
"outputs": [],
"source": [
- "[global]\nparameter: modular_script_dir = path('code/script') # override with --modular-script-dir\n# the output directory for generated files\nparameter: cwd = path(\"output\")\n# A string to identify your analysis run\nparameter: name = str\n# Name of the population column in the phenoFile\nparameter: pop_col = \"\"\n# Name of the populations (from the population column) you would like to plot and show on the PCA plot\nparameter: pops = []\n# Name of the color label column in the phenoFile; can be the same as population column. Can also be a separate column eg a \"super population\" column as a way to enable you to combine selected populations based on another column.\nparameter: label_col = \"\"\n# Number of Principal Components to output,must be consistant between flashpca run and project samples run (flashpca partial PCA method).\nparameter: k = 20\n# Number of Principal Components based on which outliers should be evaluated. Default is 5 but this should be based on examine the scree plot\nparameter: maha_k = 5\n# Homogeneity of populations. Set to --homogeneous when true and --no-homogeneous when false\nparameter: homogeneous = False\nimport re\n# For cluster jobs, number commands to run per job\nparameter: job_size = 1\n# Wall clock time expected\nparameter: walltime = \"5h\"\n# Memory expected\nparameter: mem = \"16G\"\n# Number of threads\nparameter: numThreads = 10\nsuffix = '_'.join(pops)\ncwd = path(f\"{cwd:a}\")\nif not pop_col:\n homogeneous = True\n\n# Determine if the file is in PLINK1 (BED/BIM/FAM) or PLINK2 (PGEN/PVAR/PSAM) format\ndef determine_plink_format(file_path):\n \"\"\"\n Determine the PLINK file format based on file extensions and companion files.\n \n Args:\n file_path (str or Path): Path to the input file\n \n Returns:\n str: 'plink1' or 'plink2'\n \"\"\"\n # Convert to string if it's a Path object\n file_path = str(file_path)\n \n # Check direct file extensions\n if file_path.endswith('.bed'):\n return 'plink1'\n elif file_path.endswith('.pgen'):\n return 'plink2'\n \n # If the file doesn't have a standard extension, try to infer format\n try:\n # Remove the file extension if present\n base_path = file_path.rsplit('.', 1)[0] if '.' in file_path else file_path\n \n # Check for PLINK1 companion files\n plink1_companion_files = [\n f\"{base_path}.bim\",\n f\"{base_path}.fam\"\n ]\n \n # Check for PLINK2 companion files\n plink2_companion_files = [\n f\"{base_path}.pvar\",\n f\"{base_path}.psam\"\n ]\n \n # Check PLINK1 format\n if all(os.path.exists(f) for f in plink1_companion_files):\n return 'plink1'\n \n # Check PLINK2 format\n if all(os.path.exists(f) for f in plink2_companion_files):\n return 'plink2'\n \n except Exception as e:\n print(f\"Error determining PLINK format: {e}\")\n \n # Default to PLINK1 if can't determine\n return 'plink1'\n\n# Get the appropriate PLINK command prefix\ndef get_plink_command_prefix(file_path):\n format_type = determine_plink_format(file_path)\n if format_type == 'plink1':\n return \"--bfile\"\n else: # plink2\n return \"--pfile\"\n\ndef normalize_phenoFile(pheno_path, out_dir):\n \"\"\"Return a phenoFile path where FID=0 if the file has FID==IID in every data row.\n Writes a normalized copy to out_dir when needed; otherwise returns the original path.\"\"\"\n import os, csv\n pheno_path = str(pheno_path)\n with open(pheno_path) as fh:\n rows = [r for r in csv.reader(fh, delimiter='\\t')]\n if not rows:\n return path(pheno_path)\n # Detect header: first row is a header when its first cell is not a sample value\n has_header = rows[0][0].upper() in ('FID', '#FID', 'IID', 'SAMPLE', 'SAMPLE_ID')\n data_rows = rows[1:] if has_header else rows\n if not data_rows or not all(len(r) >= 2 and r[0] == r[1] for r in data_rows):\n return path(pheno_path)\n # FID==IID everywhere \u2014 write a normalized copy to out_dir with FID=0\n out_path = os.path.join(str(out_dir), os.path.basename(pheno_path))\n with open(out_path, 'w', newline='') as fh:\n w = csv.writer(fh, delimiter='\\t')\n if has_header:\n w.writerow(rows[0])\n for r in data_rows:\n r[0] = '0'\n w.writerow(r)\n return path(out_path)"
+ "[global]\n",
+ "parameter: modular_script_dir = path('code/script') # override with --modular-script-dir\n",
+ "# the output directory for generated files\n",
+ "parameter: cwd = path(\"output\")\n",
+ "# A string to identify your analysis run\n",
+ "parameter: name = str\n",
+ "# Name of the population column in the phenoFile\n",
+ "parameter: pop_col = \"\"\n",
+ "# Name of the populations (from the population column) you would like to plot and show on the PCA plot\n",
+ "parameter: pops = []\n",
+ "# Name of the color label column in the phenoFile; can be the same as population column. Can also be a separate column eg a \"super population\" column as a way to enable you to combine selected populations based on another column.\n",
+ "parameter: label_col = \"\"\n",
+ "# Number of Principal Components to output,must be consistant between flashpca run and project samples run (flashpca partial PCA method).\n",
+ "parameter: k = 20\n",
+ "# Number of Principal Components based on which outliers should be evaluated. Default is 5 but this should be based on examine the scree plot\n",
+ "parameter: maha_k = 5\n",
+ "# Homogeneity of populations. Set to --homogeneous when true and --no-homogeneous when false\n",
+ "parameter: homogeneous = False\n",
+ "import re\n",
+ "# For cluster jobs, number commands to run per job\n",
+ "parameter: job_size = 1\n",
+ "# Wall clock time expected\n",
+ "parameter: walltime = \"5h\"\n",
+ "# Memory expected\n",
+ "parameter: mem = \"16G\"\n",
+ "# Number of threads\n",
+ "parameter: numThreads = 10\n",
+ "suffix = '_'.join(pops)\n",
+ "cwd = path(f\"{cwd:a}\")\n",
+ "if not pop_col:\n",
+ " homogeneous = True\n",
+ "\n",
+ "# Determine if the file is in PLINK1 (BED/BIM/FAM) or PLINK2 (PGEN/PVAR/PSAM) format\n",
+ "def determine_plink_format(file_path):\n",
+ " \"\"\"\n",
+ " Determine the PLINK file format based on file extensions and companion files.\n",
+ " \n",
+ " Args:\n",
+ " file_path (str or Path): Path to the input file\n",
+ " \n",
+ " Returns:\n",
+ " str: 'plink1' or 'plink2'\n",
+ " \"\"\"\n",
+ " # Convert to string if it's a Path object\n",
+ " file_path = str(file_path)\n",
+ " \n",
+ " # Check direct file extensions\n",
+ " if file_path.endswith('.bed'):\n",
+ " return 'plink1'\n",
+ " elif file_path.endswith('.pgen'):\n",
+ " return 'plink2'\n",
+ " \n",
+ " # If the file doesn't have a standard extension, try to infer format\n",
+ " try:\n",
+ " # Remove the file extension if present\n",
+ " base_path = file_path.rsplit('.', 1)[0] if '.' in file_path else file_path\n",
+ " \n",
+ " # Check for PLINK1 companion files\n",
+ " plink1_companion_files = [\n",
+ " f\"{base_path}.bim\",\n",
+ " f\"{base_path}.fam\"\n",
+ " ]\n",
+ " \n",
+ " # Check for PLINK2 companion files\n",
+ " plink2_companion_files = [\n",
+ " f\"{base_path}.pvar\",\n",
+ " f\"{base_path}.psam\"\n",
+ " ]\n",
+ " \n",
+ " # Check PLINK1 format\n",
+ " if all(os.path.exists(f) for f in plink1_companion_files):\n",
+ " return 'plink1'\n",
+ " \n",
+ " # Check PLINK2 format\n",
+ " if all(os.path.exists(f) for f in plink2_companion_files):\n",
+ " return 'plink2'\n",
+ " \n",
+ " except Exception as e:\n",
+ " print(f\"Error determining PLINK format: {e}\")\n",
+ " \n",
+ " # Default to PLINK1 if can't determine\n",
+ " return 'plink1'\n",
+ "\n",
+ "# Get the appropriate PLINK command prefix\n",
+ "def get_plink_command_prefix(file_path):\n",
+ " format_type = determine_plink_format(file_path)\n",
+ " if format_type == 'plink1':\n",
+ " return \"--bfile\"\n",
+ " else: # plink2\n",
+ " return \"--pfile\"\n",
+ "\n",
+ "def normalize_phenoFile(pheno_path, out_dir):\n",
+ " \"\"\"Return a phenoFile path where FID=0 if the file has FID==IID in every data row.\n",
+ " Writes a normalized copy to out_dir when needed; otherwise returns the original path.\"\"\"\n",
+ " import os, csv\n",
+ " pheno_path = str(pheno_path)\n",
+ " with open(pheno_path) as fh:\n",
+ " rows = [r for r in csv.reader(fh, delimiter='\\t')]\n",
+ " if not rows:\n",
+ " return path(pheno_path)\n",
+ " # Detect header: first row is a header when its first cell is not a sample value\n",
+ " has_header = rows[0][0].upper() in ('FID', '#FID', 'IID', 'SAMPLE', 'SAMPLE_ID')\n",
+ " data_rows = rows[1:] if has_header else rows\n",
+ " if not data_rows or not all(len(r) >= 2 and r[0] == r[1] for r in data_rows):\n",
+ " return path(pheno_path)\n",
+ " # FID==IID everywhere — write a normalized copy to out_dir with FID=0\n",
+ " out_path = os.path.join(str(out_dir), os.path.basename(pheno_path))\n",
+ " with open(out_path, 'w', newline='') as fh:\n",
+ " w = csv.writer(fh, delimiter='\\t')\n",
+ " if has_header:\n",
+ " w.writerow(rows[0])\n",
+ " for r in data_rows:\n",
+ " r[0] = '0'\n",
+ " w.writerow(r)\n",
+ " return path(out_path)"
]
},
{
@@ -509,7 +917,16 @@
},
"outputs": [],
"source": [
- "# PCA command with PLINK, as a sanity check\n[pca_plink]\n# PLINK binary file\nparameter: genoFile = path\ninput: genoFile\nplink_command = get_plink_command_prefix(_input)\noutput: f'{cwd}/{genoFile:bn}.pca.eigenvec'\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output[0]:bn}'\nbash: expand = \"${ }\", stderr = f'{_output[0]:n}.stderr', stdout = f'{_output[0]:n}.stdout'\n plink2 ${plink_command} ${_input:n} --out ${_output:n} --pca ${k}"
+ "# PCA command with PLINK, as a sanity check\n",
+ "[pca_plink]\n",
+ "# PLINK binary file\n",
+ "parameter: genoFile = path\n",
+ "input: genoFile\n",
+ "plink_command = get_plink_command_prefix(_input)\n",
+ "output: f'{cwd}/{genoFile:bn}.pca.eigenvec'\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output[0]:bn}'\n",
+ "bash: expand = \"${ }\", stderr = f'{_output[0]:n}.stderr', stdout = f'{_output[0]:n}.stdout'\n",
+ " plink2 ${plink_command} ${_input:n} --out ${_output:n} --pca ${k}"
]
},
{
@@ -518,7 +935,7 @@
"kernel": "SoS"
},
"source": [
- "## PCA analysis"
+ "### PCA analysis"
]
},
{
@@ -529,7 +946,37 @@
},
"outputs": [],
"source": [
- "# Run PCA analysis using flashpca \n[flashpca_1]\n# Plink binary file\nparameter: genoFile = path\n# The phenotypic file\nparameter: phenoFile = path(f'{genoFile}'.replace(\".bed\", \".fam\").replace(\".pgen\", \".psam\"))\n# minimum population size to consider in the analysis\nparameter: min_pop_size = 2\n# How to standardize X before PCA\nparameter: stand = \"binom2\"\n## Input genoFile here is for unrelated samples\nphenoFile = normalize_phenoFile(phenoFile, cwd)\ninput: genoFile, phenoFile\noutput: f'{cwd}/{phenoFile:bn}{(\".\"+name) if name else \"\"}.{(suffix+\".\") if suffix != \"\" else \"\"}pca.rds'\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output[0]:bn}'\nbash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout'\n Rscript ${modular_script_dir}/data_preprocessing/genotype/PCA.R \\\n --step flashpca \\\n --cwd \"${cwd}\" \\\n --genoFile \"${_input[0]}\" \\\n --phenoFile \"${_input[1]}\" \\\n --output \"${_output}\" \\\n --stand \"${stand}\" \\\n --min-pop-size ${min_pop_size} \\\n --homogeneous \"${homogeneous}\" \\\n --pop-col \"${pop_col}\" \\\n --label-col \"${label_col}\" \\\n --pops \"${\",\".join([str(x) for x in pops])}\" \\\n --k ${k} \\\n --maha-k ${maha_k} \\\n --numThreads ${numThreads}"
+ "# Run PCA analysis using flashpca \n",
+ "[flashpca_1]\n",
+ "# Plink binary file\n",
+ "parameter: genoFile = path\n",
+ "# The phenotypic file\n",
+ "parameter: phenoFile = path(f'{genoFile}'.replace(\".bed\", \".fam\").replace(\".pgen\", \".psam\"))\n",
+ "# minimum population size to consider in the analysis\n",
+ "parameter: min_pop_size = 2\n",
+ "# How to standardize X before PCA\n",
+ "parameter: stand = \"binom2\"\n",
+ "## Input genoFile here is for unrelated samples\n",
+ "phenoFile = normalize_phenoFile(phenoFile, cwd)\n",
+ "input: genoFile, phenoFile\n",
+ "output: f'{cwd}/{phenoFile:bn}{(\".\"+name) if name else \"\"}.{(suffix+\".\") if suffix != \"\" else \"\"}pca.rds'\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output[0]:bn}'\n",
+ "bash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout'\n",
+ " Rscript ${modular_script_dir}/data_preprocessing/genotype/PCA.R \\\n",
+ " --step flashpca \\\n",
+ " --cwd \"${cwd}\" \\\n",
+ " --genoFile \"${_input[0]}\" \\\n",
+ " --phenoFile \"${_input[1]}\" \\\n",
+ " --output \"${_output}\" \\\n",
+ " --stand \"${stand}\" \\\n",
+ " --min-pop-size ${min_pop_size} \\\n",
+ " --homogeneous \"${homogeneous}\" \\\n",
+ " --pop-col \"${pop_col}\" \\\n",
+ " --label-col \"${label_col}\" \\\n",
+ " --pops \"${\",\".join([str(x) for x in pops])}\" \\\n",
+ " --k ${k} \\\n",
+ " --maha-k ${maha_k} \\\n",
+ " --numThreads ${numThreads}"
]
},
{
@@ -538,7 +985,7 @@
"kernel": "SoS"
},
"source": [
- "## Project related individuals back"
+ "### Project related individuals back"
]
},
{
@@ -549,7 +996,34 @@
},
"outputs": [],
"source": [
- "# Project back to PCA model additional samples\n[project_samples_1]\n# Plink binary file\nparameter: genoFile = path\n# The phenotypic file\nparameter: phenoFile = path(f'{genoFile}'.replace(\".bed\", \".fam\").replace(\".pgen\", \".psam\"))\n# How to standardize X before PCA\nparameter: stand = \"binom2\"\nparameter: pca_model = f'{cwd}/{phenoFile:bn}{(\".\"+name) if name else \"\"}.{(suffix+\".\") if suffix != \"\" else \"\"}pca.rds'\n## Input genoFile here is for related samples\nphenoFile = normalize_phenoFile(phenoFile, cwd)\ninput: genoFile, phenoFile, pca_model\noutput: f'{cwd}/{phenoFile:bn}{(\".\"+name) if name else \"\"}.pca.projected.rds'\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output[0]:bn}'\nbash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout'\n Rscript ${modular_script_dir}/data_preprocessing/genotype/PCA.R \\\n --step project_samples \\\n --cwd \"${cwd}\" \\\n --genoFile \"${_input[0]}\" \\\n --phenoFile \"${_input[1]}\" \\\n --output \"${_output}\" \\\n --stand \"${stand}\" \\\n --pop-col \"${pop_col}\" \\\n --label-col \"${label_col}\" \\\n --pops \"${\",\".join([str(x) for x in pops])}\" \\\n --pca-model \"${_input[2]}\" \\\n --maha-k ${maha_k} \\\n --numThreads ${numThreads}"
+ "# Project back to PCA model additional samples\n",
+ "[project_samples_1]\n",
+ "# Plink binary file\n",
+ "parameter: genoFile = path\n",
+ "# The phenotypic file\n",
+ "parameter: phenoFile = path(f'{genoFile}'.replace(\".bed\", \".fam\").replace(\".pgen\", \".psam\"))\n",
+ "# How to standardize X before PCA\n",
+ "parameter: stand = \"binom2\"\n",
+ "parameter: pca_model = f'{cwd}/{phenoFile:bn}{(\".\"+name) if name else \"\"}.{(suffix+\".\") if suffix != \"\" else \"\"}pca.rds'\n",
+ "## Input genoFile here is for related samples\n",
+ "phenoFile = normalize_phenoFile(phenoFile, cwd)\n",
+ "input: genoFile, phenoFile, pca_model\n",
+ "output: f'{cwd}/{phenoFile:bn}{(\".\"+name) if name else \"\"}.pca.projected.rds'\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output[0]:bn}'\n",
+ "bash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout'\n",
+ " Rscript ${modular_script_dir}/data_preprocessing/genotype/PCA.R \\\n",
+ " --step project_samples \\\n",
+ " --cwd \"${cwd}\" \\\n",
+ " --genoFile \"${_input[0]}\" \\\n",
+ " --phenoFile \"${_input[1]}\" \\\n",
+ " --output \"${_output}\" \\\n",
+ " --stand \"${stand}\" \\\n",
+ " --pop-col \"${pop_col}\" \\\n",
+ " --label-col \"${label_col}\" \\\n",
+ " --pops \"${\",\".join([str(x) for x in pops])}\" \\\n",
+ " --pca-model \"${_input[2]}\" \\\n",
+ " --maha-k ${maha_k} \\\n",
+ " --numThreads ${numThreads}"
]
},
{
@@ -558,7 +1032,7 @@
"kernel": "SoS"
},
"source": [
- "## Plot PCA results\n"
+ "### Plot PCA results\n"
]
},
{
@@ -569,7 +1043,34 @@
},
"outputs": [],
"source": [
- "# Plot PCA results. \n# Can be used independently as \"plot_pca\" or combined with other workflow as eg \"flashpca+plot_pca\"\n[plot_pca]\nparameter: outlier_file = path()\nparameter: plot_data = path\nparameter: min_axis = \"\"\nparameter: max_axis = \"\"\ninput: plot_data\noutput: f'{cwd}/{_input:bn}.pc.png',\n f'{cwd}/{_input:bn}.scree.png',\n f'{cwd}/{_input:bn}.scree.txt'\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = 1, tags = f'{step_name}_{_output[0]:bn}'\nbash: expand= \"${ }\", stderr = f'{_output[0]:n}.stderr', stdout = f'{_output[0]:n}.stdout'\n Rscript ${modular_script_dir}/data_preprocessing/genotype/PCA.R \\\n --step plot_pca \\\n --cwd \"${cwd}\" \\\n --plot-data \"${_input}\" \\\n --outlier-file \"${outlier_file}\" \\\n --min-axis \"${min_axis}\" \\\n --max-axis \"${max_axis}\" \\\n --pop-col \"${pop_col}\" \\\n --label-col \"${label_col}\" \\\n --pops \"${\",\".join([str(x) for x in pops])}\" \\\n --k ${k} \\\n --output-pc-plot \"${_output[0]}\" \\\n --output-scree-plot \"${_output[1]}\" \\\n --output-scree-text \"${_output[2]}\" \\\n --numThreads ${numThreads}"
+ "# Plot PCA results. \n",
+ "# Can be used independently as \"plot_pca\" or combined with other workflow as eg \"flashpca+plot_pca\"\n",
+ "[plot_pca]\n",
+ "parameter: outlier_file = path()\n",
+ "parameter: plot_data = path\n",
+ "parameter: min_axis = \"\"\n",
+ "parameter: max_axis = \"\"\n",
+ "input: plot_data\n",
+ "output: f'{cwd}/{_input:bn}.pc.png',\n",
+ " f'{cwd}/{_input:bn}.scree.png',\n",
+ " f'{cwd}/{_input:bn}.scree.txt'\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = 1, tags = f'{step_name}_{_output[0]:bn}'\n",
+ "bash: expand= \"${ }\", stderr = f'{_output[0]:n}.stderr', stdout = f'{_output[0]:n}.stdout'\n",
+ " Rscript ${modular_script_dir}/data_preprocessing/genotype/PCA.R \\\n",
+ " --step plot_pca \\\n",
+ " --cwd \"${cwd}\" \\\n",
+ " --plot-data \"${_input}\" \\\n",
+ " --outlier-file \"${outlier_file}\" \\\n",
+ " --min-axis \"${min_axis}\" \\\n",
+ " --max-axis \"${max_axis}\" \\\n",
+ " --pop-col \"${pop_col}\" \\\n",
+ " --label-col \"${label_col}\" \\\n",
+ " --pops \"${\",\".join([str(x) for x in pops])}\" \\\n",
+ " --k ${k} \\\n",
+ " --output-pc-plot \"${_output[0]}\" \\\n",
+ " --output-scree-plot \"${_output[1]}\" \\\n",
+ " --output-scree-text \"${_output[2]}\" \\\n",
+ " --numThreads ${numThreads}"
]
},
{
@@ -578,7 +1079,7 @@
"kernel": "SoS"
},
"source": [
- "## Detect outliers"
+ "### Detect outliers"
]
},
{
@@ -589,7 +1090,38 @@
},
"outputs": [],
"source": [
- "# Calculate Mahalanobis distance per population and report outliers\n[detect_outliers]\n# Set the probability to remove outliers eg 0.95 or 0.997\nparameter: prob = 0.997\n# Mahalanobis distance p-value cutoff\nparameter: pval = 0.05\n# Robust Mahalanobis to outliers\nparameter: robust = True\nparameter: pca_result = path\ninput: pca_result\noutput: distance=f'{_input:n}.mahalanobis.rds',\n identified_outliers=f'{_input:n}.outliers',\n analysis_summary=f'{_input:n}.analysis_summary.md',\n qqplot_mahalanobis=f'{_input:n}.mahalanobis_qq.png',\n hist_mahalanobis=f'{_input:n}.mahalanobis_hist.png'\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = 1, tags = f'{step_name}_{_output[0]:bn}'\nbash: expand= \"${ }\", stderr = f'{_output[0]:n}.stderr', stdout = f'{_output[0]:n}.stdout'\n Rscript ${modular_script_dir}/data_preprocessing/genotype/PCA.R \\\n --step detect_outliers \\\n --cwd \"${cwd}\" \\\n --pca-result \"${_input}\" \\\n --prob ${prob} \\\n --pval ${pval} \\\n --robust ${\"TRUE\" if robust else \"FALSE\"} \\\n --pop-col \"${pop_col}\" \\\n --k ${k} \\\n --distance-output \"${_output['distance']}\" \\\n --identified-outliers-output \"${_output['identified_outliers']}\" \\\n --analysis-summary-output \"${_output['analysis_summary']}\" \\\n --qqplot-output \"${_output['qqplot_mahalanobis']}\" \\\n --hist-output \"${_output['hist_mahalanobis']}\" \\\n --numThreads ${numThreads}"
+ "# Calculate Mahalanobis distance per population and report outliers\n",
+ "[detect_outliers]\n",
+ "# Set the probability to remove outliers eg 0.95 or 0.997\n",
+ "parameter: prob = 0.997\n",
+ "# Mahalanobis distance p-value cutoff\n",
+ "parameter: pval = 0.05\n",
+ "# Robust Mahalanobis to outliers\n",
+ "parameter: robust = True\n",
+ "parameter: pca_result = path\n",
+ "input: pca_result\n",
+ "output: distance=f'{_input:n}.mahalanobis.rds',\n",
+ " identified_outliers=f'{_input:n}.outliers',\n",
+ " analysis_summary=f'{_input:n}.analysis_summary.md',\n",
+ " qqplot_mahalanobis=f'{_input:n}.mahalanobis_qq.png',\n",
+ " hist_mahalanobis=f'{_input:n}.mahalanobis_hist.png'\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = 1, tags = f'{step_name}_{_output[0]:bn}'\n",
+ "bash: expand= \"${ }\", stderr = f'{_output[0]:n}.stderr', stdout = f'{_output[0]:n}.stdout'\n",
+ " Rscript ${modular_script_dir}/data_preprocessing/genotype/PCA.R \\\n",
+ " --step detect_outliers \\\n",
+ " --cwd \"${cwd}\" \\\n",
+ " --pca-result \"${_input}\" \\\n",
+ " --prob ${prob} \\\n",
+ " --pval ${pval} \\\n",
+ " --robust ${\"TRUE\" if robust else \"FALSE\"} \\\n",
+ " --pop-col \"${pop_col}\" \\\n",
+ " --k ${k} \\\n",
+ " --distance-output \"${_output['distance']}\" \\\n",
+ " --identified-outliers-output \"${_output['identified_outliers']}\" \\\n",
+ " --analysis-summary-output \"${_output['analysis_summary']}\" \\\n",
+ " --qqplot-output \"${_output['qqplot_mahalanobis']}\" \\\n",
+ " --hist-output \"${_output['hist_mahalanobis']}\" \\\n",
+ " --numThreads ${numThreads}"
]
},
{
@@ -598,18 +1130,7 @@
"kernel": "SoS"
},
"source": [
- "## Add plot and outlier detection to PCA steps"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "## Anticipated Results\n",
- "\n",
- "The pipeline produces output files in the `output/` subdirectory named after the workflow step. Verify success by checking that output files exist and are non-empty. See the **Output** section above for the expected file names and formats."
+ "### Add plot and outlier detection to PCA steps"
]
},
{
@@ -620,7 +1141,24 @@
},
"outputs": [],
"source": [
- "[flashpca_2, project_samples_2]\n# Set the probability to remove outliers eg 0.95 or 0.997\nparameter: prob = 0.997\n# Robust Mahalanobis to outliers\nparameter: robust = True\noutput: distance=f'{_input:n}.mahalanobis.rds',\n identified_outliers=f'{_input:n}.outliers',\n analysis_summary=f'{_input:n}.analysis_summary.md',\n qqplot_mahalanobis=f'{_input:n}.mahalanobis_qq.png',\n hist_mahalanobis=f'{_input:n}.mahalanobis_hist.png'\nsos_run(\"detect_outliers\", pca_result=_input, prob=prob, robust=robust)\n\n[flashpca_3, project_samples_3]\ninput: output_from(1), output_from(2)['identified_outliers']\noutliers = [x.strip() for x in open(_input[1]).readlines() if x.strip()]\noutput: f\"{cwd}/{_input[0]:bn}.pc.png\",\n f\"{cwd}/{_input[0]:bn}.scree.png\"\nsos_run(\"plot_pca\", plot_data = _input[0], outlier_file = _input[1] if len(outliers) else path())"
+ "[flashpca_2, project_samples_2]\n",
+ "# Set the probability to remove outliers eg 0.95 or 0.997\n",
+ "parameter: prob = 0.997\n",
+ "# Robust Mahalanobis to outliers\n",
+ "parameter: robust = True\n",
+ "output: distance=f'{_input:n}.mahalanobis.rds',\n",
+ " identified_outliers=f'{_input:n}.outliers',\n",
+ " analysis_summary=f'{_input:n}.analysis_summary.md',\n",
+ " qqplot_mahalanobis=f'{_input:n}.mahalanobis_qq.png',\n",
+ " hist_mahalanobis=f'{_input:n}.mahalanobis_hist.png'\n",
+ "sos_run(\"detect_outliers\", pca_result=_input, prob=prob, robust=robust)\n",
+ "\n",
+ "[flashpca_3, project_samples_3]\n",
+ "input: output_from(1), output_from(2)['identified_outliers']\n",
+ "outliers = [x.strip() for x in open(_input[1]).readlines() if x.strip()]\n",
+ "output: f\"{cwd}/{_input[0]:bn}.pc.png\",\n",
+ " f\"{cwd}/{_input[0]:bn}.scree.png\"\n",
+ "sos_run(\"plot_pca\", plot_data = _input[0], outlier_file = _input[1] if len(outliers) else path())"
]
}
],
@@ -636,7 +1174,7 @@
"mimetype": "text/x-r-source",
"name": "R",
"pygments_lexer": "r",
- "version": "4.4.3"
+ "version": "4.5.3"
},
"nbdime-conflicts": {
"local_diff": [
diff --git a/code/SoS/data_preprocessing/genotype/VCF_QC.ipynb b/code/SoS/data_preprocessing/genotype/VCF_QC.ipynb
index ae1471e13..cf79e3e2b 100644
--- a/code/SoS/data_preprocessing/genotype/VCF_QC.ipynb
+++ b/code/SoS/data_preprocessing/genotype/VCF_QC.ipynb
@@ -18,38 +18,67 @@
"kernel": "SoS"
},
"source": [
- "## Description\n"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "A major challenge in biomedical research is the quality control (QC) of sequencing data. False positive variant calls can hinder the ability to detect disease associated variants or introduce spurious associations, therefore the need for a rigorous QC. Our pipeline focuses on QC after the variant calling stage and requires project Variant Calling Format (pVCF) as input files. We have defined default thresholds for genotype and variant-level hard filtering based on recommendations from the UK Biobank team and a thorough review of the literature [[cf. Carson et al. BMC Bioinformatics (2014)](https://doi.org/10.1186/1471-2105-15-125),[cf. Lek et al. Nature (2016)](https://doi.org/10.1038/nature19057),[cf. Szustakowski et al. Nature Genetics (2021)](https://doi.org/10.1038/s41588-021-00885-0)]. Bcftools is used in our QC steps. We first handle multi-allelic sites by splitting them into bi-allelic records. We include an optional workflow to keep only bi-allelic sites in the data. Variants are then annotated based on dbSNP data. Genotypes are kept if they have a Genotype Depth (DP) >= 10 and a Genotype Quality (GQ) >= 20. Variants are included if at least one sample has an allelic balance (AB) >= 0.15 for Single Nucleotide Variants (SNVs) and AB>=0.2 for indels, variant missigness is below 20% and Hardy-Weinberg Equilibrium p-value is > 1e-08. Allele balance is calculated for heterozygotes as the number of bases supporting the least-represented allele over the total number of base observations. Output summary statistics, such as transistion/transversion ratios (TS/TV ratio) are calculated to determine the effectiveness of QC. "
+ "## Overview\n",
+ "\n",
+ "Genotype problems are cheap to fix here and expensive to find later. Chromosome naming\n",
+ "differs between references (`1` versus `chr1`), the same variant can be represented several\n",
+ "ways, and low-depth or low-quality calls look like real genotypes to every tool downstream.\n",
+ "This step normalises naming, annotates variants against dbSNP so identifiers are consistent\n",
+ "across cohorts, and applies the depth and quality filters, so everything after it works from\n",
+ "one convention.\n",
+ "\n",
+ "A major challenge in biomedical research is the quality control (QC) of sequencing data. False positive variant calls can hinder the ability to detect disease associated variants or introduce spurious associations, therefore the need for a rigorous QC. This module focuses on QC after the variant calling stage and requires project Variant Calling Format (pVCF) as input files. Default thresholds are defined for genotype and variant-level hard filtering based on recommendations from the UK Biobank team and a thorough review of the literature [[cf. Carson et al. BMC Bioinformatics (2014)](https://doi.org/10.1186/1471-2105-15-125),[cf. Lek et al. Nature (2016)](https://doi.org/10.1038/nature19057),[cf. Szustakowski et al. Nature Genetics (2021)](https://doi.org/10.1038/s41588-021-00885-0)]. Bcftools performs the QC steps. Multi-allelic sites are first split into bi-allelic records. An optional workflow keeps only bi-allelic sites in the data. Variants are then annotated based on dbSNP data. Genotypes are kept if they have a Genotype Depth (DP) >= 10 and a Genotype Quality (GQ) >= 20. Variants are included if at least one sample has an allelic balance (AB) >= 0.15 for Single Nucleotide Variants (SNVs) and AB>=0.2 for indels, variant missigness is below 20% and, when an HWE cutoff is supplied, the Hardy-Weinberg Equilibrium p-value exceeds it. Allele balance is calculated for heterozygotes as the number of bases supporting the least-represented allele over the total number of base observations. Output summary statistics, such as transistion/transversion ratios (TS/TV ratio) are calculated to determine the effectiveness of QC. \n",
+ "\n",
+ "**When to run it.** First, on the raw VCF, before formatting, PCA or association testing.\n",
+ "A meta-analysis across cohorts is where naming and identifier mismatches surface, and by\n",
+ "then it is far harder to trace."
]
},
{
"cell_type": "markdown",
"metadata": {
- "kernel": "SoS"
+ "kernel": "SoS",
+ "tags": []
},
"source": [
- "## Default Parameters: VCF QC Filters\n",
+ "## Input\n",
+ "\n",
+ "- `--genoFile input/genotype/protocol_example.genotype.chr*.vcf.gz` (+ `.tbi`): the per-chromosome\n",
+ " pVCFs to QC, from upstream genotype calling. A manifest may be given instead --\n",
+ " `input/genotype/protocol_example.genotype.vcf_list.txt`, two columns, chromosome number and VCF\n",
+ " path -- which lets the chromosomes run in parallel. This is the only required data input;\n",
+ " everything else is reference material or a threshold.\n",
+ "- `--dbsnp-variants input/reference_data/00-All.add_chr.variants.gz` (+ `.tbi`) (required for `qc`):\n",
+ " chr-renamed dbSNP table, `CHROM/POS/ID/REF/ALT`, used to split variants into known and novel.\n",
+ "- `--reference-genome input/reference_data/GRCh38_full_analysis_set_plus_decoy_hla.noALT_noHLA_noDecoy_ERCC.fasta`\n",
+ " (+ `.fai`) (required for `qc`): FASTA for left-normalization and the REF check.\n",
+ "- `--cwd` (default `output`): work directory for the analysis.\n",
+ "- `--modular-script-dir` (default `code/script`): directory holding the modular wrapper scripts.\n",
+ "- `--remove-samples` / `--keep-samples` (both default `.`): optional sample lists applied before QC.\n",
+ "- `--skip-vcf-header-filtering` (default off): skip the header-based contig filtering.\n",
+ "- `--gt-only-vcf-qc` (default off): treat the input as genotype-only, so the depth and quality\n",
+ " filters below are not applied. Required for the toy `protocol_example.genotype.*` VCFs, whose\n",
+ " FORMAT carries only `GT` -- with no `DP`, `GQ` or `AD` those filters have nothing to read.\n",
"\n",
+ "Variant preprocessing (`qc_1`):\n",
"\n",
- "1. Genotype depth filters: For WES data, UK Biobank recommends **SNPs DP>10 and Indels DP>10 for indels.** However we think for WGS we can be less stringent, or simply rely on GQ. Users can set it to 1 eg, `--DP 1 --DP-indel 1 `\n",
- "2. Genotype quality GQ>20.\n",
- "3. At least one sample per site passed the allele balance threshold >= 0.15 for SNPs and >=0.20 for indels (heterozygous variants). \n",
- " - Allele balance is calculated for heterozygotes as the number of bases supporting the least-represented allele over the total number of base observations.\n",
- "4. This module also allows for filtering by HWE and missingness although by default we don't filter on them.\n",
+ "- `--bi-allelic` (default off): keep only bi-allelic sites.\n",
+ "- `--snp-only` (default off): drop indels.\n",
"\n",
- "Filtering are done with `bcftools`. Here is a [useful cheatsheet from github user @elowy01](https://gist.github.com/elowy01/93922762e131d7abd3c7e8e166a74a0b).\n",
+ "Variant- and genotype-level filters (`qc_2`):\n",
"\n",
- "### A note on TS/TV summary from VCF genotype data\n",
+ "- `--geno-filter` (default `0.2`): maximum per-variant missingness.\n",
+ "- `--DP-snp` (default `10`) and `--DP-indel` (default `10`): minimum read depth per genotype.\n",
+ "- `--GQ` (default `20`): minimum genotype quality.\n",
+ "- `--AB-snp` (default `0.15`) and `--AB-indel` (default `0.2`): allele-balance thresholds for\n",
+ " heterozygous calls.\n",
+ "- `--hwe-filter` (default `0.0`): HWE p-value cutoff. The parameter help calls `0.0` \"no HWE\n",
+ " filter\", but the script applies `HWE > ` literally, so `0.0` still drops any site whose HWE\n",
+ " p-value is exactly 0 or whose `HWE` tag is missing.\n",
"\n",
- "`bcftools stats` command provides useful summary statistics including TS/TV ratio, which is routinely used as a quality measure of variant calls. With dbSNP based annotation of novel and known variants, `bcftools` can compute TS/TV for novel and known variants at variant level, and at sample level. It should be noted that variant level TS/TV does not take sample genotype into consideration -- it simply counts the TS and TV event for observed SNPs in the data. Other tools, such as `snpsift`, implements variant level TS/TV by counting TS and TV events in sample genotypes and compute the ratio after summing up TS and TV across all samples. See [here](https://github.com/samtools/bcftools/issues/1526) some discussions on this issue. We provide these TS/TV calculations before and after QC but users should be aware of the difference when interpreting the results."
+ "Cluster resources: `--numThreads` (default `1`), `--job-size` (default `1`), `--walltime` (default\n",
+ "`24h`), `--mem` (default `60G`). A whole-genome VCF needs the full 60G; `qc_1` and `qc_2` process\n",
+ "roughly 14G of data per hour, so scale `--walltime` with input size."
]
},
{
@@ -58,29 +87,54 @@
"kernel": "SoS"
},
"source": [
- "## Default Parameters: Resource Usage\n",
+ "### On choosing the filter thresholds\n",
+ "\n",
+ "For WES data the UK Biobank recommendation is DP>10 for both SNPs and indels. For WGS the depth\n",
+ "thresholds can be relaxed, or GQ relied on alone -- `--DP-snp 1 --DP-indel 1` effectively disables\n",
+ "the depth filter.\n",
+ "\n",
+ "The allele-balance filters keep a site if at least one sample passes the threshold. Allele balance is\n",
+ "computed for heterozygotes as the number of bases supporting the least-represented allele over the\n",
+ "total number of base observations.\n",
"\n",
- "- Memory: Usually a whole genome VCF.gz file has the size of 200+GB, after testing, a minimum of 60GB of mem is requried.\n",
- "- Walltimes: For every hour qc_1 or qc_2 can process ~14G of data. The default is set to be 24h, corresponding to ~300GB of input. Please set the --walltime parameter according to the size of your input files."
+ "Filtering is done with `bcftools`; this\n",
+ "[cheatsheet](https://gist.github.com/elowy01/93922762e131d7abd3c7e8e166a74a0b) is a useful\n",
+ "reference."
]
},
{
"cell_type": "markdown",
"metadata": {
- "kernel": "SoS",
- "tags": []
+ "kernel": "SoS"
},
"source": [
- "## Input Files\n",
+ "## Output\n",
"\n",
- "| File | Produced by | Used as |\n",
- "|------|-------------|---------|\n",
- "| `input/genotype/protocol_example.genotype.chr*.vcf.gz` (+ `.tbi`) | upstream genotype calling | per-chromosome target VCFs to QC |\n",
- "| `input/genotype/protocol_example.genotype.vcf_list.txt` | manifest (2 columns: chrom number, VCF path) | list of VCFs for parallel QC |\n",
- "| `input/reference_data/00-All.add_chr.variants.gz` (+ `.tbi`) | dbSNP, `chr`-renamed | dbSNP variants for known/novel annotation |\n",
- "| `input/reference_data/GRCh38_full_analysis_set_plus_decoy_hla.noALT_noHLA_noDecoy_ERCC.fasta` (+ `.fai`) | reference genome | reference for left-normalization / REF check |\n",
+ "- `.leftnorm.vcf.gz` (+ `.tbi`) -- from `qc_1`: left-normalized, variant-ID-assigned,\n",
+ " dbSNP-annotated VCF.\n",
+ "- `.leftnorm.bcftools_qc.vcf.gz` (+ `.tbi`) -- from `qc_2`: the same VCF after the missingness,\n",
+ " depth, quality and allele-balance filters. This is the file downstream modules read.\n",
+ "- `.leftnorm.bcftools_qc.known_variant.snipsift_tstv` and\n",
+ " `.leftnorm.bcftools_qc.novel_variant.snipsift_tstv` -- from `qc_3`: TS/TV and Hom/Het\n",
+ " summaries, split by whether the variant carries a dbSNP rsID.\n",
+ "- `.leftnorm.bcftools_qc.known_variant_sumstats` and\n",
+ " `.leftnorm.bcftools_qc.novel_variant_sumstats` -- from `qc_3`: the full `bcftools stats`\n",
+ " report per split, with the `SN` record counts and the per-sample `PSC` block.\n",
+ "- a `.stdout` / `.stderr` log and the generated shell script beside each target.\n",
"\n",
- "**Note:** the toy `protocol_example.genotype.*` VCFs carry only the `GT` FORMAT field (no `DP`/`GQ`/`AB`), so QC is run with `--skip_vcf_header_filtering True` (see Step 3)."
+ "All paths are relative to `--cwd`, and `` is the `--genoFile` stem, e.g.\n",
+ "`protocol_example.genotype.chr22`.\n",
+ "\n",
+ "With `--gt-only-vcf-qc` the intermediate and final names carry an extra `.gt_only` segment, e.g.\n",
+ "`.leftnorm.gt_only.bcftools_qc.vcf.gz`.\n",
+ "\n",
+ "**No example output is committed for the QC stages.** On the toy chr22 data every\n",
+ "`bcftools_qc.*_sumstats` file reports `number of records: 0`, with or without `--gt-only-vcf-qc`,\n",
+ "while the input to that step holds 18,455 variants of which `bcftools view -c1` retains 4,259. The\n",
+ "`qc_2` filters are discarding everything and the step still exits successfully, so the committed\n",
+ "example output is empty and no dump can be shown here yet.\n",
+ "\n",
+ "`bcftools stats` command provides useful summary statistics including TS/TV ratio, which is routinely used as a quality measure of variant calls. With dbSNP based annotation of novel and known variants, `bcftools` can compute TS/TV for novel and known variants at variant level, and at sample level. It should be noted that variant level TS/TV does not take sample genotype into consideration -- it simply counts the TS and TV event for observed SNPs in the data. Other tools, such as `snpsift`, implements variant level TS/TV by counting TS and TV events in sample genotypes and compute the ratio after summing up TS and TV across all samples. See [here](https://github.com/samtools/bcftools/issues/1526) some discussions on this issue. TS/TV is reported before and after QC, but users should be aware of the difference when interpreting the results."
]
},
{
@@ -89,15 +143,9 @@
"kernel": "SoS"
},
"source": [
- "## Output Files\n",
- "\n",
- "| File | Produced by | Description |\n",
- "|------|-------------|-------------|\n",
- "| `output/vcf_qc/protocol_example.genotype.chr*.leftnorm.vcf.gz` | `qc` | left-normalized, variant-ID-assigned, dbSNP-annotated VCF |\n",
- "| `output/vcf_qc/protocol_example.genotype.chr*.leftnorm.{known,novel}_variant.snipsift_tstv` | `qc` | Ts/Tv summary statistics (known vs novel variants) |\n",
- "| `output/vcf_qc/protocol_example.genotype.chr*.leftnorm.{known,novel}_variant_sumstats` | `qc` | per-file variant summary statistics |\n",
+ "## Minimal Working Example\n",
"\n",
- "The QC-ed genotype VCFs can be converted to PLINK format with `vcf_to_plink` in `genotype_formatting.ipynb`."
+ "The steps below run on the toy `protocol_example` genotype data. Steps 1 and 2 are optional helpers; Step 3 is the main QC workflow."
]
},
{
@@ -106,11 +154,9 @@
"kernel": "SoS"
},
"source": [
- "## Minimal Working Example\n",
+ "#### Step 1. Rename Chromosomes (optional)\n",
"\n",
- "The steps below run on the toy `protocol_example` genotype data. Steps 1 and 2 are optional helpers; Step 3 is the main QC workflow.\n",
- "\n",
- "## Steps"
+ "Add a `chr` prefix to chromosome names so they match the reference `fasta`. Timing: ~40 min on full data."
]
},
{
@@ -119,18 +165,17 @@
"kernel": "SoS"
},
"source": [
- "### **Step 1.** Rename Chromosomes (optional)\n",
- "\n",
- "Add a `chr` prefix to chromosome names so they match the reference `fasta`. Timing: ~40 min on full data."
+ "The toy genotypes are already `chr`-prefixed, so to demonstrate `rename_chrs` the protocol ships a small derived input `protocol_example.genotype.rawchr.chr22.vcf.gz` whose chromosome is named `22` (no `chr`). Running `rename_chrs` adds the prefix back, writing `protocol_example.genotype.rawchr.add_chr.vcf.gz` next to the input."
]
},
{
"cell_type": "markdown",
"metadata": {
- "kernel": "SoS"
+ "kernel": "SoS",
+ "tags": []
},
"source": [
- "The toy genotypes are already `chr`-prefixed, so to demonstrate `rename_chrs` we ship a small derived input `protocol_example.genotype.rawchr.chr22.vcf.gz` whose chromosome is named `22` (no `chr`). Running `rename_chrs` adds the prefix back, writing `protocol_example.genotype.rawchr.add_chr.vcf.gz` next to the input."
+ "**Timing**: TBD (on toy dataset)"
]
},
{
@@ -144,7 +189,9 @@
},
"outputs": [],
"source": [
- "sos run pipeline/VCF_QC.ipynb rename_chrs \\\n --genoFile input/genotype/protocol_example.genotype.rawchr.chr22.vcf.gz \\\n --cwd output/vcf_qc"
+ "sos run pipeline/VCF_QC.ipynb rename_chrs \\\n",
+ " --genoFile input/genotype/protocol_example.genotype.rawchr.chr22.vcf.gz \\\n",
+ " --cwd output/vcf_qc"
]
},
{
@@ -153,7 +200,7 @@
"kernel": "SoS"
},
"source": [
- "Extracts `CHROM/POS/ID/REF/ALT` from a VCF into the compact `.variants.gz` dbSNP table used by the QC step. Here we run it on a toy genotype VCF to produce `protocol_example.genotype.chr22.variants.gz`."
+ "Extracts `CHROM/POS/ID/REF/ALT` from a VCF into the compact `.variants.gz` dbSNP table used by the QC step. It is run here on a toy genotype VCF to produce `protocol_example.genotype.chr22.variants.gz`."
]
},
{
@@ -165,6 +212,16 @@
"Annotates against the dbSNP database in VCF format."
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS",
+ "tags": []
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ]
+ },
{
"cell_type": "code",
"execution_count": null,
@@ -176,7 +233,9 @@
},
"outputs": [],
"source": [
- "sos run pipeline/VCF_QC.ipynb dbsnp_annotate \\\n --genoFile input/genotype/protocol_example.genotype.chr22.vcf.gz \\\n --cwd output/vcf_qc"
+ "sos run pipeline/VCF_QC.ipynb dbsnp_annotate \\\n",
+ " --genoFile input/genotype/protocol_example.genotype.chr22.vcf.gz \\\n",
+ " --cwd output/vcf_qc"
]
},
{
@@ -185,19 +244,22 @@
"kernel": "Bash"
},
"source": [
- "### **Step 2.** Merge separated bed files into one\n",
- "Converting VCF to PLINK keeping only the ROSMAP samples."
+ "#### Step 2. Merge per-chromosome files (handled elsewhere)\n",
+ "\n",
+ "Converting the QC-d VCFs to PLINK format and merging them is done by `genotype_formatting.ipynb`\n",
+ "(`vcf_to_plink`, then `merge_plink`), not by this module. See that notebook for the commands and its\n",
+ "own `--name` option."
]
},
{
- "cell_type": "code",
- "execution_count": null,
+ "cell_type": "markdown",
"metadata": {
- "kernel": "Bash"
+ "kernel": "SoS"
},
- "outputs": [],
"source": [
- "sos run pipeline/genotype_formatting.ipynb vcf_to_plink \\\n --genoFile `ls input/genotype/protocol_example.genotype.chr*.vcf.gz` \\\n --cwd output/plink/\n\nsos run pipeline/genotype_formatting.ipynb merge_plink \\\n --genoFile `ls output/plink/protocol_example.genotype.chr*.bed` \\\n --name protocol_example.genotype.merged \\\n --cwd output/plink/"
+ "#### Step 3. Quality Control\n",
+ "\n",
+ "The main QC workflow: split multi-allelic sites, left-normalize indels, assign variant IDs, and annotate known/novel variants against dbSNP."
]
},
{
@@ -206,18 +268,17 @@
"kernel": "SoS"
},
"source": [
- "### **Step 3.** Quality Control\n",
- "\n",
- "The main QC workflow: split multi-allelic sites, left-normalize indels, assign variant IDs, and annotate known/novel variants against dbSNP."
+ "Run on a single chromosome (chr22) below, or in parallel over all chromosomes."
]
},
{
"cell_type": "markdown",
"metadata": {
- "kernel": "SoS"
+ "kernel": "SoS",
+ "tags": []
},
"source": [
- "Run on a single chromosome (chr22) below, or in parallel over all chromosomes."
+ "**Timing**: TBD (on toy dataset)"
]
},
{
@@ -231,7 +292,12 @@
},
"outputs": [],
"source": [
- "sos run pipeline/VCF_QC.ipynb qc \\\n --genoFile input/genotype/protocol_example.genotype.chr22.vcf.gz \\\n --dbsnp-variants input/reference_data/00-All.add_chr.variants.gz \\\n --reference-genome input/reference_data/GRCh38_full_analysis_set_plus_decoy_hla.noALT_noHLA_noDecoy_ERCC.fasta \\\n --cwd output/vcf_qc \\\n --skip_vcf_header_filtering True"
+ "sos run pipeline/VCF_QC.ipynb qc \\\n",
+ " --genoFile input/genotype/protocol_example.genotype.chr22.vcf.gz \\\n",
+ " --dbsnp-variants input/reference_data/00-All.add_chr.variants.gz \\\n",
+ " --reference-genome input/reference_data/GRCh38_full_analysis_set_plus_decoy_hla.noALT_noHLA_noDecoy_ERCC.fasta \\\n",
+ " --cwd output/vcf_qc \\\n",
+ " --skip_vcf_header_filtering True"
]
},
{
@@ -242,7 +308,17 @@
"source": [
"To run in parallel for all genotype data listed in `protocol_example.genotype.vcf_list.txt`:\n",
"\n",
- "**Note on `--skip_vcf_header_filtering True`:** the toy `protocol_example.genotype.*` VCFs contain only the `GT` (genotype) FORMAT field. The default depth/quality filtering relies on the `DP` FORMAT tag, which is absent here, so `--skip_vcf_header_filtering True` runs left-normalization, variant-ID assignment and dbSNP annotation while skipping the `DP`-based filtering \u2014 the appropriate path for genotype-only VCFs. To see the **default** path (which does apply the `DP`/`GQ`/`AD` filters), use the `protocol_example.genotype.withfmt.chr22.vcf.gz` file in **Step 3b** below, which carries those tags."
+ "**Note on `--skip_vcf_header_filtering True`:** the toy `protocol_example.genotype.*` VCFs contain only the `GT` (genotype) FORMAT field. The default depth/quality filtering relies on the `DP` FORMAT tag, which is absent here, so `--skip_vcf_header_filtering True` runs left-normalization, variant-ID assignment and dbSNP annotation while skipping the `DP`-based filtering — the appropriate path for genotype-only VCFs. To see the **default** path (which does apply the `DP`/`GQ`/`AD` filters), use the `protocol_example.genotype.withfmt.chr22.vcf.gz` file in **Step 3b** below, which carries those tags."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS",
+ "tags": []
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
]
},
{
@@ -256,7 +332,13 @@
},
"outputs": [],
"source": [
- "sos run pipeline/VCF_QC.ipynb qc \\\n --genoFile input/genotype/protocol_example.genotype.vcf_list.txt \\\n --dbsnp-variants input/reference_data/00-All.add_chr.variants.gz \\\n --reference-genome input/reference_data/GRCh38_full_analysis_set_plus_decoy_hla.noALT_noHLA_noDecoy_ERCC.fasta \\\n --cwd output/vcf_qc \\\n --skip_vcf_header_filtering True \\\n -j 2"
+ "sos run pipeline/VCF_QC.ipynb qc \\\n",
+ " --genoFile input/genotype/protocol_example.genotype.vcf_list.txt \\\n",
+ " --dbsnp-variants input/reference_data/00-All.add_chr.variants.gz \\\n",
+ " --reference-genome input/reference_data/GRCh38_full_analysis_set_plus_decoy_hla.noALT_noHLA_noDecoy_ERCC.fasta \\\n",
+ " --cwd output/vcf_qc \\\n",
+ " --skip_vcf_header_filtering True \\\n",
+ " -j 2"
]
},
{
@@ -265,9 +347,19 @@
"kernel": "SoS"
},
"source": [
- "### Step 3b \u2014 Quality Control on data with DP/GQ/AD tags (default path)\n",
+ "#### Step 3b — Quality Control on data with DP/GQ/AD tags (default path)\n",
"\n",
- "The genotype VCFs above are genotype-only (`GT`), so QC is run with `--skip_vcf_header_filtering True` to bypass the depth/quality filters. To demonstrate the **default** QC path (which filters on read depth `DP`, genotype quality `GQ`, and allele balance from `AD`), we ship `protocol_example.genotype.withfmt.chr22.vcf.gz`. Its `DP/GQ/AD/AB` values are **synthetic placeholders** (seed=22), not real measurements, and exist only to exercise the default filters."
+ "The genotype VCFs above are genotype-only (`GT`), so QC is run with `--skip_vcf_header_filtering True` to bypass the depth/quality filters. To demonstrate the **default** QC path (which filters on read depth `DP`, genotype quality `GQ`, and allele balance from `AD`), the protocol ships `protocol_example.genotype.withfmt.chr22.vcf.gz`. Its `DP/GQ/AD/AB` values are **synthetic placeholders** (seed=22), not real measurements, and exist only to exercise the default filters."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS",
+ "tags": []
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
]
},
{
@@ -278,7 +370,12 @@
},
"outputs": [],
"source": [
- "sos run pipeline/VCF_QC.ipynb qc \\\n --genoFile input/genotype/protocol_example.genotype.withfmt.chr22.vcf.gz \\\n --dbsnp-variants input/reference_data/00-All.add_chr.variants.gz \\\n --reference-genome input/reference_data/GRCh38_full_analysis_set_plus_decoy_hla.noALT_noHLA_noDecoy_ERCC.fasta \\\n --cwd output/vcf_qc_withfmt \\\n -j 2"
+ "sos run pipeline/VCF_QC.ipynb qc \\\n",
+ " --genoFile input/genotype/protocol_example.genotype.withfmt.chr22.vcf.gz \\\n",
+ " --dbsnp-variants input/reference_data/00-All.add_chr.variants.gz \\\n",
+ " --reference-genome input/reference_data/GRCh38_full_analysis_set_plus_decoy_hla.noALT_noHLA_noDecoy_ERCC.fasta \\\n",
+ " --cwd output/vcf_qc_withfmt \\\n",
+ " -j 2"
]
},
{
@@ -359,12 +456,102 @@
"cell_type": "code",
"execution_count": null,
"metadata": {
- "kernel": "Bash",
- "tags": []
+ "kernel": "SoS"
},
"outputs": [],
"source": [
- "sos run VCF_QC.ipynb -h"
+ "sos run pipeline/VCF_QC.ipynb -h"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "```\n",
+ "usage: sos run pipeline/VCF_QC.ipynb\n",
+ " [workflow_name | -t targets] [options] [workflow_options]\n",
+ " workflow_name: Single or combined workflows defined in this script\n",
+ " targets: One or more targets to generate\n",
+ " options: Single-hyphen sos parameters (see \"sos run -h\" for details)\n",
+ " workflow_options: Double-hyphen workflow-specific parameters\n",
+ "\n",
+ "Workflows:\n",
+ " rename_chrs\n",
+ " dbsnp_annotate\n",
+ " qc\n",
+ "\n",
+ "Global Workflow Options:\n",
+ " --modular-script-dir code/script (as path)\n",
+ " --genoFile paths\n",
+ "\n",
+ " input can either be 1 vcf genoFile, or a list of vcf\n",
+ " genoFile.\n",
+ " --remove-samples . (as path)\n",
+ " The path to the file that contains the list of samples\n",
+ " to remove (format FID, IID)\n",
+ " --keep-samples . (as path)\n",
+ " The path to the file that contains the list of samples\n",
+ " to keep (format FID, IID)\n",
+ " --cwd output (as path)\n",
+ " Workdir\n",
+ " --numThreads 1 (as int)\n",
+ " Number of threads\n",
+ " --job-size 1 (as int)\n",
+ " For cluster jobs, number commands to run per job\n",
+ " --walltime 24h\n",
+ " Walltime\n",
+ " --mem 60G\n",
+ " Usually a whole genome VCF.gz file has the size of\n",
+ " 200+GB, after testing, a minimum of 60GB of mem is\n",
+ " requried.\n",
+ " --[no-]gt-only-vcf-qc (default to False)\n",
+ " Whether to run GT-only VCF QC for inputs that do not\n",
+ " carry per-sample DP/GQ/AD FORMAT fields.\n",
+ " --[no-]skip-vcf-header-filtering (default to False)\n",
+ " Deprecated alias for GT-only VCF QC.\n",
+ "\n",
+ "Sections\n",
+ " rename_chrs:\n",
+ " dbsnp_annotate:\n",
+ " qc_1: Handel multi-allelic sites, left normalization of indels\n",
+ " and add variant ID\n",
+ " Workflow Options:\n",
+ " --dbsnp-variants VAL (as path, required)\n",
+ " Path to dbSNP variants generated previously\n",
+ " --reference-genome VAL (as path, required)\n",
+ " Path to fasta file for HG reference genome, eg\n",
+ " GRCh38_full_analysis_set_plus_decoy_hla.fa\n",
+ " --[no-]bi-allelic (default to False)\n",
+ " --[no-]snp-only (default to False)\n",
+ " qc_2: genotype QC\n",
+ " Workflow Options:\n",
+ " --geno-filter 0.2 (as float)\n",
+ " Maximum missingess per-variant, default to 0.2\n",
+ " --DP-snp 10 (as int)\n",
+ " Sample level QC - read depth (DP) to filter out SNPs\n",
+ " below this value Default to 10, with WES data in mind\n",
+ " But for WGS, setting it to 2 may be fine considering the\n",
+ " WGS may have low DP but the GQ filter should be good\n",
+ " enough\n",
+ " --GQ 20 (as int)\n",
+ " Sample level QC - genotype quality (GQ) of specific\n",
+ " sample. This measure tells you how confident we are that\n",
+ " the genotype we assigned to a particular sample is\n",
+ " correct\n",
+ " --DP-indel 10 (as int)\n",
+ " Sample level QC - read depth (DP) to filter out indels\n",
+ " below this value\n",
+ " --AB-snp 0.15 (as float)\n",
+ " Allele balance for snps\n",
+ " --AB-indel 0.2 (as float)\n",
+ " Allele balance for indels\n",
+ " --hwe-filter 0.0 (as float)\n",
+ " HWE filter, default to 0.0 which means no HWE filter is\n",
+ " applied\n",
+ " qc_3:\n",
+ "```"
]
},
{
@@ -373,7 +560,7 @@
"kernel": "Bash"
},
"source": [
- "## Global parameters"
+ "## Workflow implementation"
]
},
{
@@ -384,7 +571,84 @@
},
"outputs": [],
"source": [
- "[global]\nparameter: modular_script_dir = path('code/script') # override with --modular-script-dir\n# input can either be 1 vcf genoFile, or a list of vcf genoFile.\nparameter: genoFile = paths\n# The path to the file that contains the list of samples to remove (format FID, IID)\nparameter: remove_samples = path('.')\n# The path to the file that contains the list of samples to keep (format FID, IID)\nparameter: keep_samples = path('.')\n# Workdir\nparameter: cwd = path(\"output\")\n# Number of threads\nparameter: numThreads = 1\n# For cluster jobs, number commands to run per job\nparameter: job_size = 1\n# Walltime \nparameter: walltime = '24h'\n# Usually a whole genome VCF.gz file has the size of 200+GB, after testing, a minimum of 60GB of mem is requried.\nparameter: mem = '60G'\nimport re\n# Whether to run GT-only VCF QC for inputs that do not carry per-sample DP/GQ/AD FORMAT fields.\nparameter: gt_only_vcf_qc = False\n# Deprecated alias for GT-only VCF QC.\nparameter: skip_vcf_header_filtering = False\ngt_only_vcf_qc = bool(gt_only_vcf_qc or skip_vcf_header_filtering)\nvcf_qc_mode_suffix = '.gt_only' if gt_only_vcf_qc else ''\n# use this function to edit memory string for PLINK input\nfrom sos.utils import expand_size\ncwd = path(f\"{cwd:a}\")\n\nimport os\ndef get_genotype_file(geno_file_paths):\n #\n def valid_geno_file(x):\n suffixes = path(x).suffixes\n if suffixes[-1] == '.bed':\n return True\n elif suffixes[-1] == '.vcf':\n return True\n elif len(suffixes)>1 and ''.join(suffixes[-2:]) == \".vcf.gz\" :\n return True\n return False\n #\n def complete_geno_path(x, geno_file):\n if not valid_geno_file(x):\n raise ValueError(f\"Genotype file {x} should be VCF (end with .vcf.gz) or PLINK bed file (end with .bed)\")\n if not os.path.isfile(x):\n # relative path\n if not os.path.isfile(f'{geno_file:ad}/' + x):\n raise ValueError(f\"Cannot find genotype file {x}\")\n else:\n x = f'{geno_file:ad}/' + x\n return x\n # \n def format_chrom(chrom):\n if chrom.startswith('chr'):\n chrom = chrom[3:]\n return chrom\n # Inputs are either VCF or bed, or a vector of them \n if len(geno_file_paths) > 1:\n if all([valid_geno_file(x) for x in geno_file_paths]):\n return paths(geno_file_paths)\n else: \n raise ValueError(f\"Invalid input {geno_file_paths}\")\n # Input is one genotype file or text list of genotype files\n geno_file = geno_file_paths[0]\n if valid_geno_file(geno_file):\n return paths(geno_file)\n else: \n units = [x.strip().split() for x in open(geno_file).readlines() if x.strip() and not x.strip().startswith('#')]\n if all([len(x) == 1 for x in units]):\n return paths([complete_geno_path(x[0], geno_file) for x in units])\n elif all([len(x) == 2 for x in units]):\n genos = dict([(format_chrom(x[0]), path(complete_geno_path(x[1], geno_file))) for x in units])\n else:\n raise ValueError(f\"{geno_file} should contain one column of file names, or two columns of chrom number and corresponding file name\")\n return genos\n \ngenoFile = get_genotype_file(genoFile)"
+ "[global]\n",
+ "parameter: modular_script_dir = path('code/script') # override with --modular-script-dir\n",
+ "# input can either be 1 vcf genoFile, or a list of vcf genoFile.\n",
+ "parameter: genoFile = paths\n",
+ "# The path to the file that contains the list of samples to remove (format FID, IID)\n",
+ "parameter: remove_samples = path('.')\n",
+ "# The path to the file that contains the list of samples to keep (format FID, IID)\n",
+ "parameter: keep_samples = path('.')\n",
+ "# Workdir\n",
+ "parameter: cwd = path(\"output\")\n",
+ "# Number of threads\n",
+ "parameter: numThreads = 1\n",
+ "# For cluster jobs, number commands to run per job\n",
+ "parameter: job_size = 1\n",
+ "# Walltime \n",
+ "parameter: walltime = '24h'\n",
+ "# Usually a whole genome VCF.gz file has the size of 200+GB, after testing, a minimum of 60GB of mem is requried.\n",
+ "parameter: mem = '60G'\n",
+ "import re\n",
+ "# Whether to run GT-only VCF QC for inputs that do not carry per-sample DP/GQ/AD FORMAT fields.\n",
+ "parameter: gt_only_vcf_qc = False\n",
+ "# Deprecated alias for GT-only VCF QC.\n",
+ "parameter: skip_vcf_header_filtering = False\n",
+ "gt_only_vcf_qc = bool(gt_only_vcf_qc or skip_vcf_header_filtering)\n",
+ "vcf_qc_mode_suffix = '.gt_only' if gt_only_vcf_qc else ''\n",
+ "# use this function to edit memory string for PLINK input\n",
+ "from sos.utils import expand_size\n",
+ "cwd = path(f\"{cwd:a}\")\n",
+ "\n",
+ "import os\n",
+ "def get_genotype_file(geno_file_paths):\n",
+ " #\n",
+ " def valid_geno_file(x):\n",
+ " suffixes = path(x).suffixes\n",
+ " if suffixes[-1] == '.bed':\n",
+ " return True\n",
+ " elif suffixes[-1] == '.vcf':\n",
+ " return True\n",
+ " elif len(suffixes)>1 and ''.join(suffixes[-2:]) == \".vcf.gz\" :\n",
+ " return True\n",
+ " return False\n",
+ " #\n",
+ " def complete_geno_path(x, geno_file):\n",
+ " if not valid_geno_file(x):\n",
+ " raise ValueError(f\"Genotype file {x} should be VCF (end with .vcf.gz) or PLINK bed file (end with .bed)\")\n",
+ " if not os.path.isfile(x):\n",
+ " # relative path\n",
+ " if not os.path.isfile(f'{geno_file:ad}/' + x):\n",
+ " raise ValueError(f\"Cannot find genotype file {x}\")\n",
+ " else:\n",
+ " x = f'{geno_file:ad}/' + x\n",
+ " return x\n",
+ " # \n",
+ " def format_chrom(chrom):\n",
+ " if chrom.startswith('chr'):\n",
+ " chrom = chrom[3:]\n",
+ " return chrom\n",
+ " # Inputs are either VCF or bed, or a vector of them \n",
+ " if len(geno_file_paths) > 1:\n",
+ " if all([valid_geno_file(x) for x in geno_file_paths]):\n",
+ " return paths(geno_file_paths)\n",
+ " else: \n",
+ " raise ValueError(f\"Invalid input {geno_file_paths}\")\n",
+ " # Input is one genotype file or text list of genotype files\n",
+ " geno_file = geno_file_paths[0]\n",
+ " if valid_geno_file(geno_file):\n",
+ " return paths(geno_file)\n",
+ " else: \n",
+ " units = [x.strip().split() for x in open(geno_file).readlines() if x.strip() and not x.strip().startswith('#')]\n",
+ " if all([len(x) == 1 for x in units]):\n",
+ " return paths([complete_geno_path(x[0], geno_file) for x in units])\n",
+ " elif all([len(x) == 2 for x in units]):\n",
+ " genos = dict([(format_chrom(x[0]), path(complete_geno_path(x[1], geno_file))) for x in units])\n",
+ " else:\n",
+ " raise ValueError(f\"{geno_file} should contain one column of file names, or two columns of chrom number and corresponding file name\")\n",
+ " return genos\n",
+ " \n",
+ "genoFile = get_genotype_file(genoFile)"
]
},
{
@@ -394,7 +658,7 @@
"tags": []
},
"source": [
- "## Annotate known and novel variants\n",
+ "### Annotate known and novel variants\n",
"\n",
"You can download the known variant reference from [this link](https://ftp.ncbi.nlm.nih.gov/snp/organisms/human_9606_b150_GRCh38p7/VCF/00-All.vcf.gz).\n",
"\n",
@@ -409,7 +673,15 @@
},
"outputs": [],
"source": [
- "[rename_chrs: provides = '{genoFile:nn}.add_chr.vcf.gz']\n# This file can be downloaded from https://ftp.ncbi.nlm.nih.gov/snp/organisms/human_9606_b150_GRCh38p7/VCF/00-All.vcf.gz.\ninput: genoFile\noutput: f'{_input:nn}.add_chr.vcf.gz'\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'\nbash: expand= \"${ }\", stderr = f'{_output:nn}.stderr', stdout = f'{_output:nn}.stdout'\n bash ${modular_script_dir}/data_preprocessing/genotype/VCF_QC.sh rename_chrs \\\n --genoFile \"${_input}\" \\\n --output \"${_output}\"\n"
+ "[rename_chrs: provides = '{genoFile:nn}.add_chr.vcf.gz']\n",
+ "# This file can be downloaded from https://ftp.ncbi.nlm.nih.gov/snp/organisms/human_9606_b150_GRCh38p7/VCF/00-All.vcf.gz.\n",
+ "input: genoFile\n",
+ "output: f'{_input:nn}.add_chr.vcf.gz'\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'\n",
+ "bash: expand= \"${ }\", stderr = f'{_output:nn}.stderr', stdout = f'{_output:nn}.stdout'\n",
+ " bash ${modular_script_dir}/data_preprocessing/genotype/VCF_QC.sh rename_chrs \\\n",
+ " --genoFile \"${_input}\" \\\n",
+ " --output \"${_output}\"\n"
]
},
{
@@ -420,7 +692,14 @@
},
"outputs": [],
"source": [
- "[dbsnp_annotate]\ninput: genoFile\noutput: f\"{cwd}/{_input:bnn}.variants.gz\"\ntask: trunk_workers = 1, trunk_size=job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'\nbash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout'\n bash ${modular_script_dir}/data_preprocessing/genotype/VCF_QC.sh dbsnp_annotate \\\n --genoFile \"${_input}\" \\\n --output \"${_output}\"\n"
+ "[dbsnp_annotate]\n",
+ "input: genoFile\n",
+ "output: f\"{cwd}/{_input:bnn}.variants.gz\"\n",
+ "task: trunk_workers = 1, trunk_size=job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'\n",
+ "bash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout'\n",
+ " bash ${modular_script_dir}/data_preprocessing/genotype/VCF_QC.sh dbsnp_annotate \\\n",
+ " --genoFile \"${_input}\" \\\n",
+ " --output \"${_output}\"\n"
]
},
{
@@ -429,9 +708,9 @@
"kernel": "SoS"
},
"source": [
- "## Genotype QC\n",
+ "### Genotype QC\n",
"\n",
- "This step handles multi-allelic sites and annotate variants to known and novel. We add an RS ID to variants in dbSNP. Variants without rsID are considered novel variants. For every hour it can produce ~14Gb of data, please set the --walltime parameter according to the size of your input files."
+ "This step handles multi-allelic sites and annotate variants to known and novel. An RS ID is added to variants in dbSNP. Variants without rsID are considered novel variants. For every hour it can produce ~14Gb of data, please set the --walltime parameter according to the size of your input files."
]
},
{
@@ -442,7 +721,28 @@
},
"outputs": [],
"source": [
- "# Handel multi-allelic sites, left normalization of indels and add variant ID\n[qc_1 (variant preprocessing)]\n# Path to dbSNP variants generated previously\nparameter: dbsnp_variants = path\n# Path to fasta file for HG reference genome, eg GRCh38_full_analysis_set_plus_decoy_hla.fa\nparameter: reference_genome = path\nparameter: bi_allelic = False\nparameter: snp_only = False\ninput: genoFile, group_by = 1\noutput: f'{cwd}/{_input:bnn}.{\"leftnorm\" if not bi_allelic else \"biallelic\"}{\".snp\" if snp_only else \"\"}{vcf_qc_mode_suffix}.vcf.gz'\ntask: trunk_workers = 1, trunk_size=job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'\nbash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout'\n bash ${modular_script_dir}/data_preprocessing/genotype/VCF_QC.sh qc \\\n --cwd \"${cwd}\" \\\n --genoFile \"${_input}\" \\\n --output \"${_output}\" \\\n --dbsnp-variants \"${dbsnp_variants}\" \\\n --reference-genome \"${reference_genome}\" \\\n --bi-allelic \"${bi_allelic}\" \\\n --snp-only \"${snp_only}\" \\\n --gt-only-vcf-qc \"${gt_only_vcf_qc}\" \\\n --numThreads ${numThreads}"
+ "# Handel multi-allelic sites, left normalization of indels and add variant ID\n",
+ "[qc_1 (variant preprocessing)]\n",
+ "# Path to dbSNP variants generated previously\n",
+ "parameter: dbsnp_variants = path\n",
+ "# Path to fasta file for HG reference genome, eg GRCh38_full_analysis_set_plus_decoy_hla.fa\n",
+ "parameter: reference_genome = path\n",
+ "parameter: bi_allelic = False\n",
+ "parameter: snp_only = False\n",
+ "input: genoFile, group_by = 1\n",
+ "output: f'{cwd}/{_input:bnn}.{\"leftnorm\" if not bi_allelic else \"biallelic\"}{\".snp\" if snp_only else \"\"}{vcf_qc_mode_suffix}.vcf.gz'\n",
+ "task: trunk_workers = 1, trunk_size=job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'\n",
+ "bash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout'\n",
+ " bash ${modular_script_dir}/data_preprocessing/genotype/VCF_QC.sh qc \\\n",
+ " --cwd \"${cwd}\" \\\n",
+ " --genoFile \"${_input}\" \\\n",
+ " --output \"${_output}\" \\\n",
+ " --dbsnp-variants \"${dbsnp_variants}\" \\\n",
+ " --reference-genome \"${reference_genome}\" \\\n",
+ " --bi-allelic \"${bi_allelic}\" \\\n",
+ " --snp-only \"${snp_only}\" \\\n",
+ " --gt-only-vcf-qc \"${gt_only_vcf_qc}\" \\\n",
+ " --numThreads ${numThreads}"
]
},
{
@@ -462,7 +762,40 @@
},
"outputs": [],
"source": [
- "# genotype QC\n[qc_2 (variant level QC)]\n# Maximum missingess per-variant, default to 0.2\nparameter: geno_filter = 0.2\n# Sample level QC - read depth (DP) to filter out SNPs below this value\n# Default to 10, with WES data in mind \n# But for WGS, setting it to 2 may be fine considering the WGS may have low DP but the GQ filter should be good enough\nparameter: DP_snp = 10\n# Sample level QC - genotype quality (GQ) of specific sample. This measure tells you how confident we are that the genotype we assigned to a particular sample is correct\nparameter: GQ = 20\n# Sample level QC - read depth (DP) to filter out indels below this value\nparameter: DP_indel = 10\n# Allele balance for snps\nparameter: AB_snp = 0.15\n# Allele balance for indels\nparameter: AB_indel = 0.2\n# HWE filter, default to 0.0 which means no HWE filter is applied\nparameter: hwe_filter = 0.0\noutput: f\"{_input:nn}.bcftools_qc.vcf.gz\"\ntask: trunk_workers = 1, trunk_size=job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'\nbash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout'\n bash ${modular_script_dir}/data_preprocessing/genotype/VCF_QC.sh qc_2 \\\n --cwd \"${cwd}\" \\\n --genoFile \"${_input}\" \\\n --output \"${_output}\" \\\n --gt-only-vcf-qc \"${gt_only_vcf_qc}\" \\\n --geno-filter ${geno_filter} \\\n --DP-snp ${DP_snp} \\\n --GQ ${GQ} \\\n --DP-indel ${DP_indel} \\\n --AB-snp ${AB_snp} \\\n --AB-indel ${AB_indel} \\\n --hwe-filter ${hwe_filter} \\\n --numThreads ${numThreads}"
+ "# genotype QC\n",
+ "[qc_2 (variant level QC)]\n",
+ "# Maximum missingess per-variant, default to 0.2\n",
+ "parameter: geno_filter = 0.2\n",
+ "# Sample level QC - read depth (DP) to filter out SNPs below this value\n",
+ "# Default to 10, with WES data in mind \n",
+ "# But for WGS, setting it to 2 may be fine considering the WGS may have low DP but the GQ filter should be good enough\n",
+ "parameter: DP_snp = 10\n",
+ "# Sample level QC - genotype quality (GQ) of specific sample. This measure tells you how confident we are that the genotype we assigned to a particular sample is correct\n",
+ "parameter: GQ = 20\n",
+ "# Sample level QC - read depth (DP) to filter out indels below this value\n",
+ "parameter: DP_indel = 10\n",
+ "# Allele balance for snps\n",
+ "parameter: AB_snp = 0.15\n",
+ "# Allele balance for indels\n",
+ "parameter: AB_indel = 0.2\n",
+ "# HWE filter, default to 0.0 which means no HWE filter is applied\n",
+ "parameter: hwe_filter = 0.0\n",
+ "output: f\"{_input:nn}.bcftools_qc.vcf.gz\"\n",
+ "task: trunk_workers = 1, trunk_size=job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'\n",
+ "bash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout'\n",
+ " bash ${modular_script_dir}/data_preprocessing/genotype/VCF_QC.sh qc_2 \\\n",
+ " --cwd \"${cwd}\" \\\n",
+ " --genoFile \"${_input}\" \\\n",
+ " --output \"${_output}\" \\\n",
+ " --gt-only-vcf-qc \"${gt_only_vcf_qc}\" \\\n",
+ " --geno-filter ${geno_filter} \\\n",
+ " --DP-snp ${DP_snp} \\\n",
+ " --GQ ${GQ} \\\n",
+ " --DP-indel ${DP_indel} \\\n",
+ " --AB-snp ${AB_snp} \\\n",
+ " --AB-indel ${AB_indel} \\\n",
+ " --hwe-filter ${hwe_filter} \\\n",
+ " --numThreads ${numThreads}"
]
},
{
@@ -473,18 +806,22 @@
},
"outputs": [],
"source": [
- "[qc_3 (genotype data summary statistics)]\ninput: output_from('qc_1'), output_from('qc_2'), group_by = 1\noutput: f\"{cwd}/{_input:bnn}.novel_variant_sumstats\", \n f\"{cwd}/{_input:bnn}.known_variant_sumstats\", \n f\"{cwd}/{_input:bnn}.novel_variant.snipsift_tstv\",\n f\"{cwd}/{_input:bnn}.known_variant.snipsift_tstv\"\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output[0]:bn}'\nbash: expand= \"${ }\", stderr = f'{_output[0]:n}.stderr', stdout = f'{_output[0]:n}.stdout'\n bash ${modular_script_dir}/data_preprocessing/genotype/VCF_QC.sh qc_3 \\\n --cwd \"${cwd}\" \\\n --genoFile \"${_input}\" \\\n --novel-sumstats \"${_output[0]}\" \\\n --known-sumstats \"${_output[1]}\" \\\n --novel-tstv \"${_output[2]}\" \\\n --known-tstv \"${_output[3]}\" \\\n --numThreads ${numThreads}"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "## Anticipated Results\n",
- "\n",
- "The pipeline produces output files in the `output/` subdirectory named after the workflow step. Verify success by checking that output files exist and are non-empty. See the **Output** section above for the expected file names and formats."
+ "[qc_3 (genotype data summary statistics)]\n",
+ "input: output_from('qc_1'), output_from('qc_2'), group_by = 1\n",
+ "output: f\"{cwd}/{_input:bnn}.novel_variant_sumstats\", \n",
+ " f\"{cwd}/{_input:bnn}.known_variant_sumstats\", \n",
+ " f\"{cwd}/{_input:bnn}.novel_variant.snipsift_tstv\",\n",
+ " f\"{cwd}/{_input:bnn}.known_variant.snipsift_tstv\"\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output[0]:bn}'\n",
+ "bash: expand= \"${ }\", stderr = f'{_output[0]:n}.stderr', stdout = f'{_output[0]:n}.stdout'\n",
+ " bash ${modular_script_dir}/data_preprocessing/genotype/VCF_QC.sh qc_3 \\\n",
+ " --cwd \"${cwd}\" \\\n",
+ " --genoFile \"${_input}\" \\\n",
+ " --novel-sumstats \"${_output[0]}\" \\\n",
+ " --known-sumstats \"${_output[1]}\" \\\n",
+ " --novel-tstv \"${_output[2]}\" \\\n",
+ " --known-tstv \"${_output[3]}\" \\\n",
+ " --numThreads ${numThreads}"
]
}
],
diff --git a/code/SoS/data_preprocessing/genotype/genotype_formatting.ipynb b/code/SoS/data_preprocessing/genotype/genotype_formatting.ipynb
index 3945a8608..bdf17e268 100644
--- a/code/SoS/data_preprocessing/genotype/genotype_formatting.ipynb
+++ b/code/SoS/data_preprocessing/genotype/genotype_formatting.ipynb
@@ -19,7 +19,15 @@
"tags": []
},
"source": [
- "## Description"
+ "## Overview\n",
+ "\n",
+ "Nothing here changes the genotypes; it changes how they are packaged. Tools in the protocol\n",
+ "disagree about format - some want PLINK, some want VCF - and the association and fine-mapping\n",
+ "steps run per region or per chromosome, so the genotype data has to be split the same way to\n",
+ "be processed in parallel. These workflows do those conversions and splits, plus the LD\n",
+ "matrix computation per region that summary-statistic fine-mapping needs.\n",
+ "\n",
+ "**When to run it.** Run the relevant workflow after genotype quality control and before association scanning, LD calculation, or fine-mapping that requires the corresponding genotype layout.\n"
]
},
{
@@ -28,7 +36,16 @@
"kernel": "SoS"
},
"source": [
- "We include steps for the formatting of genotype files. This includes the conversion between VCF and PLINK formats, the splitting of data (by specified input, chromosomes or genes) and the merging of data (by specified input, or by chromosomes)."
+ "This module provides workflows for the formatting of genotype files. This includes the conversion between VCF and PLINK formats, the splitting of data (by specified input, chromosomes or genes) and the merging of data (by specified input, or by chromosomes)."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "faa2bdaa",
+ "metadata": {},
+ "source": [
+ "**When to run it.** After `VCF_QC`, before PCA and association testing. Which workflows you\n",
+ "need depends on the tools you plan to run, not on your data."
]
},
{
@@ -38,15 +55,47 @@
"tags": []
},
"source": [
- "## Input Files\n",
+ "## Input\n",
+ "\n",
+ "- `--genoFile`: the genotype data to convert. Its meaning follows the workflow -- one or more PLINK\n",
+ " prefixes for `plink_to_vcf` and the `merge_*` workflows, one or more VCFs for `vcf_to_plink`.\n",
+ "- `--name` (required): output prefix for this run, e.g. `protocol_example.genotype.merged`.\n",
+ "- `--cwd` (default `output`): work directory for the analysis.\n",
+ "- `--modular-script-dir` (default `code/script`): directory holding the modular wrapper scripts.\n",
+ "\n",
+ "`vcf_to_plink` -- VCF to PLINK:\n",
+ "\n",
+ "- `--add-chr` / `--no-add-chr` (default on): prefix contig names with `chr`.\n",
+ "- `--remove-duplicates` / `--no-remove-duplicates` (default off): drop duplicated variant IDs.\n",
+ "- `--keep-dosage` / `--no-keep-dosage` (default off): retain dosages rather than hard calls.\n",
+ "- `--remove-samples` / `--keep-samples` (both default `.`): optional sample lists.\n",
+ "\n",
+ "`genotype_by_region` -- split PLINK genotypes into regions:\n",
+ "\n",
+ "- `--region-list` (required): regions to cut the genotypes into.\n",
+ "- `--window` (default `0`): flanking bp added either side of each region.\n",
+ "\n",
+ "`ld_by_region_plink` -- LD matrices per region:\n",
+ "\n",
+ "- `--region-list` (required): regions to compute LD for.\n",
+ "- `--float-type` (default `16`): float width of the stored matrix.\n",
+ "\n",
+ "`genotype_by_chrom` -- split by chromosome:\n",
+ "\n",
+ "- `--chrom` (required): the chromosomes to emit, e.g. `--chrom 21 22`.\n",
+ "\n",
+ "`write_data_list` -- write a manifest of produced files:\n",
+ "\n",
+ "- `--out` (required) and `--ext` (required): manifest path and the file extension to collect.\n",
+ "- `--data-files`: the files to list, when they are not discovered from the previous step.\n",
"\n",
- "This minimal working example starts from the per-chromosome toy genotype VCFs under `input/genotype/`:\n",
+ "`merge_plink` -- merge PLINK filesets:\n",
"\n",
- "| File | Description |\n",
- "|------|-------------|\n",
- "| `input/genotype/protocol_example.genotype.chr{1..22}.vcf.gz` | 22 per-chromosome toy genotype VCFs (60 samples) |\n",
+ "- `--keep-samples` (default `.`): optional sample list.\n",
+ "- `--extra-plink-opts`: extra options passed straight to PLINK.\n",
"\n",
- "The pipeline also accepts a single whole-genome VCF or PLINK bed/bim/fam bundle, a list of VCF/PLINK files, or a two-column (chrom, file) list.\n"
+ "Cluster resources: `--numThreads` (default `20`), `--job-size` (default `1`), `--walltime` (default\n",
+ "`5h`), `--mem` (default `16G`)."
]
},
{
@@ -55,15 +104,52 @@
"kernel": "SoS"
},
"source": [
- "## Output Files\n",
+ "## Output\n",
"\n",
- "| File | Description |\n",
- "|------|-------------|\n",
- "| `output/genotype_formatting/plink/protocol_example.genotype.chr{1..22}.{bed,bim,fam}` | Per-chromosome PLINK bundles (Step 1) |\n",
- "| `output/genotype_formatting/plink/protocol_example.genotype.merged.{bed,bim,fam}` | Single merged genome-wide PLINK bundle (Step 2) |\n",
- "| `output/genotype_formatting/genotype_by_chrom/protocol_example.genotype.merged.{1..22}.{bed,bim,fam}` | Merged bundle partitioned back by chromosome (Step 3) |\n",
+ "- `.vcf.gz` (+ `.tbi`) -- from `plink_to_vcf`: the genotypes as a bgzipped, tabix-indexed VCF.\n",
+ "- `.bed` / `.bim` / `.fam`, or `.pgen` / `.pvar` / `.psam` -- from `vcf_to_plink`, and from\n",
+ " `merge_plink` for the merged fileset: a PLINK1 or PLINK2 fileset.\n",
+ "- `..bed` / `.bim` / `.fam` -- from `genotype_by_region`: one fileset per region in\n",
+ " `--region-list`, widened by `--window`.\n",
+ "- `..ld.rds` -- from `ld_by_region_plink`: the LD matrix for a region, stored at the\n",
+ " width `--float-type` sets.\n",
+ "- `..bed` / `.bim` / `.fam` -- from `genotype_by_chrom`: one fileset per `--chrom`.\n",
+ "- the manifest named by `--out`, a plain `.txt` or `.list` file of the produced paths -- from\n",
+ " `write_data_list`. Downstream modules read this rather than globbing.\n",
+ "- a `.stdout` / `.stderr` log and the generated script beside each target.\n",
"\n",
- "The merged PLINK bundle is the input expected by downstream genotype workers (GWAS_QC, GRM, PCA).\n"
+ "All paths are relative to `--cwd`, and `` is whatever `--name` was set to.\n",
+ "\n",
+ "A `.pvar` file carries meta_file headers ahead of the variant table, so it must be read with a parser\n",
+ "that skips them rather than as a plain TSV.\n",
+ "\n",
+ "The merged fileset from Route A, `output/genotype_formatting/plink/protocol_example.genotype.merged`,\n",
+ "holds 371,802 variants across 60 samples. First rows of the `.bim`:\n",
+ "\n",
+ "```\n",
+ "1\tchr1:113886_CTCTT_C\t0\t113886\tC\tCTCTT\n",
+ "1\tchr1:191223_C_*\t0\t191223\t*\tC\n",
+ "```\n",
+ "\n",
+ "and of the `.fam`, which carries no phenotype (`-9`):\n",
+ "\n",
+ "```\n",
+ "0 SAMPLE_001 0 0 0 -9\n",
+ "0 SAMPLE_002 0 0 0 -9\n",
+ "```\n",
+ "\n",
+ "Route A step 3 writes one fileset per chromosome plus its manifest:\n",
+ "\n",
+ "```\n",
+ "protocol_example.genotype.merged.10.bed\n",
+ "protocol_example.genotype.merged.10.bim\n",
+ "protocol_example.genotype.merged.10.fam\n",
+ "protocol_example.genotype_by_chrom_files.txt\n",
+ "```\n",
+ "\n",
+ "**Routes B and C have no committed example output.** Route B (`plink_to_vcf`, `merge_vcf`) and the\n",
+ "manifest step are runnable but have not been executed here; Route C additionally needs a region list,\n",
+ "which the protocol does not yet ship."
]
},
{
@@ -72,7 +158,7 @@
"kernel": "SoS"
},
"source": [
- "## Minimal Working Example\n\nThe data can be downloaded from [Synapse](https://www.synapse.org/#!Synapse:syn69670658/files/)."
+ "## Minimal Working Example\n"
]
},
{
@@ -81,7 +167,7 @@
"kernel": "SoS"
},
"source": [
- "### **Step 1.** VCF to PLINK\n"
+ "### VCF to PLINK, merged, then split by chromosome"
]
},
{
@@ -90,7 +176,7 @@
"kernel": "SoS"
},
"source": [
- "Convert the 22 per-chromosome toy VCFs into per-chromosome PLINK bed/bim/fam bundles. Timing: <1 min on the toy data.\n"
+ "#### Step 1. VCF to PLINK\n"
]
},
{
@@ -99,7 +185,17 @@
"kernel": "SoS"
},
"source": [
- "Each input VCF becomes one PLINK bundle named after the input stem.\n"
+ "Convert the 22 per-chromosome toy VCFs into per-chromosome PLINK bed/bim/fam bundles. Each input VCF becomes one PLINK bundle named after the input stem.\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS",
+ "tags": []
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
]
},
{
@@ -113,7 +209,11 @@
},
"outputs": [],
"source": [
- "sos run pipeline/genotype_formatting.ipynb vcf_to_plink \\\n --genoFile `ls input/genotype/protocol_example.genotype.chr*.vcf.gz | grep -vE \"rawchr|withfmt|add_chr\"` \\\n --cwd output/genotype_formatting/plink \\\n --name protocol_example \\\n -j 4"
+ "sos run pipeline/genotype_formatting.ipynb vcf_to_plink \\\n",
+ " --genoFile `ls input/genotype/protocol_example.genotype.chr*.vcf.gz | grep -vE \"rawchr|withfmt|add_chr\"` \\\n",
+ " --cwd output/genotype_formatting/plink \\\n",
+ " --name protocol_example \\\n",
+ " -j 4"
]
},
{
@@ -122,7 +222,7 @@
"kernel": "SoS"
},
"source": [
- "### **Step 2.** Merge PLINK Files\n"
+ "#### Step 2. Merge PLINK Files\n"
]
},
{
@@ -131,16 +231,17 @@
"kernel": "SoS"
},
"source": [
- "Timing: <1 min on the toy data.\n"
+ "This step merges all per-chromosome PLINK files into one genome-wide bundle. Note: only `plink` (v1.9) can perform the merge — `plink2` does not support it.\n"
]
},
{
"cell_type": "markdown",
"metadata": {
- "kernel": "SoS"
+ "kernel": "SoS",
+ "tags": []
},
"source": [
- "This step merges all per-chromosome PLINK files into one genome-wide bundle. Note: only `plink` (v1.9) can perform the merge — `plink2` does not support it.\n"
+ "**Timing**: TBD (on toy dataset)"
]
},
{
@@ -154,7 +255,11 @@
},
"outputs": [],
"source": [
- "sos run pipeline/genotype_formatting.ipynb merge_plink \\\n --genoFile `ls output/genotype_formatting/plink/protocol_example.genotype.chr*.bed` \\\n --name protocol_example.genotype.merged \\\n --cwd output/genotype_formatting/plink \\\n -j 2"
+ "sos run pipeline/genotype_formatting.ipynb merge_plink \\\n",
+ " --genoFile `ls output/genotype_formatting/plink/protocol_example.genotype.chr*.bed` \\\n",
+ " --name protocol_example.genotype.merged \\\n",
+ " --cwd output/genotype_formatting/plink \\\n",
+ " -j 2"
]
},
{
@@ -163,7 +268,7 @@
"kernel": "SoS"
},
"source": [
- "### **Step 3.** Partition Genotype Data by Chromosome\n"
+ "#### Step 3. Partition Genotype Data by Chromosome\n"
]
},
{
@@ -172,7 +277,17 @@
"kernel": "SoS"
},
"source": [
- "Split the merged genome-wide bundle back into per-chromosome PLINK files. Timing: <1 min on the toy data.\n"
+ "Split the merged genome-wide bundle back into per-chromosome PLINK files.\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS",
+ "tags": []
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
]
},
{
@@ -186,7 +301,11 @@
},
"outputs": [],
"source": [
- "sos run pipeline/genotype_formatting.ipynb genotype_by_chrom \\\n --genoFile output/genotype_formatting/plink/protocol_example.genotype.merged.bed \\\n --cwd output/genotype_formatting/genotype_by_chrom \\\n --chrom `cut -f 1 output/genotype_formatting/plink/protocol_example.genotype.merged.bim | uniq | sed \"s/chr//g\"` \\\n -j 4"
+ "sos run pipeline/genotype_formatting.ipynb genotype_by_chrom \\\n",
+ " --genoFile output/genotype_formatting/plink/protocol_example.genotype.merged.bed \\\n",
+ " --cwd output/genotype_formatting/genotype_by_chrom \\\n",
+ " --chrom `cut -f 1 output/genotype_formatting/plink/protocol_example.genotype.merged.bim | uniq | sed \"s/chr//g\"` \\\n",
+ " -j 4"
]
},
{
@@ -195,50 +314,81 @@
"kernel": "SoS"
},
"source": [
- "## Command Interface"
+ "### PLINK to VCF, then merged"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "#### Step 1. PLINK to VCF\n",
+ "\n",
+ "The inverse of Step 1: `plink_to_vcf` writes one bgzipped VCF per input fileset."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
- "kernel": "Bash"
+ "kernel": "Bash",
+ "vscode": {
+ "languageId": "plaintext"
+ }
},
"outputs": [],
"source": [
- "sos run genotype_formatting.ipynb -h"
+ "sos run pipeline/genotype_formatting.ipynb plink_to_vcf \\\n",
+ " --genoFile `ls output/genotype_formatting/plink/protocol_example.genotype.chr*.bed | sed \"s/.bed//g\"` \\\n",
+ " --cwd output/genotype_formatting/vcf \\\n",
+ " --name protocol_example.genotype"
]
},
{
- "cell_type": "code",
- "execution_count": null,
+ "cell_type": "markdown",
"metadata": {
"kernel": "SoS"
},
- "outputs": [],
"source": [
- "[global]\nparameter: modular_script_dir = path('code/script') # override with --modular-script-dir\n# Work directory & output directory\nparameter: cwd = path(\"output\")\nimport re\n# For cluster jobs, number commands to run per job\nparameter: job_size = 1\n# Wall clock time expected\nparameter: walltime = \"5h\"\n# Memory expected\nparameter: mem = \"16G\"\n# Number of threads\nparameter: numThreads = 20\n# the path to a bed file or VCF file, a vector of bed files or VCF files, or a text file listing the bed files or VCF files to process\nparameter: genoFile = paths\nparameter: name = str\n# use this function to edit memory string for PLINK input\nfrom sos.utils import expand_size\ncwd = f\"{cwd:a}\"\n\nimport os\ndef get_genotype_file(geno_file_paths):\n def valid_geno_file(x):\n suffixes = path(x).suffixes\n if suffixes[-1] in ['.bed', '.pgen']:\n return True\n if len(suffixes) > 1 and ''.join(suffixes[-2:]) == \".vcf.gz\":\n return True\n return False\n #\n def complete_geno_path(x, geno_file):\n if not valid_geno_file(x):\n raise ValueError(f\"Genotype file {x} should be VCF (end with .vcf.gz) or PLINK bed file (end with .bed)\")\n if not os.path.isfile(x):\n # relative path\n if not os.path.isfile(f'{geno_file:ad}/' + x):\n raise ValueError(f\"Cannot find genotype file {x}\")\n else:\n x = f'{geno_file:ad}/' + x\n return x\n # \n def format_chrom(chrom):\n if chrom.startswith('chr'):\n chrom = chrom[3:]\n return chrom\n # Inputs are either VCF or bed, or a vector of them \n if len(geno_file_paths) > 1:\n if all([valid_geno_file(x) for x in geno_file_paths]):\n return paths(geno_file_paths)\n else: \n raise ValueError(f\"Invalid input {geno_file_paths}\")\n # Input is one genotype file or text list of genotype files\n geno_file = geno_file_paths[0]\n if valid_geno_file(geno_file):\n return paths(geno_file)\n else: \n units = [x.strip().split() for x in open(geno_file).readlines() if x.strip() and not x.strip().startswith('#')]\n if all([len(x) == 1 for x in units]):\n return paths([complete_geno_path(x[0], geno_file) for x in units])\n elif all([len(x) == 2 for x in units]):\n genos = dict([(format_chrom(x[0]), path(complete_geno_path(x[1], geno_file))) for x in units])\n else:\n raise ValueError(f\"{geno_file} should contain one column of file names, or two columns of chrom number and corresponding file name\")\n return genos\n\n# Determine if the file is in PLINK1 (BED/BIM/FAM) or PLINK2 (PGEN/PVAR/PSAM) format\ndef determine_plink_format(file_path):\n \"\"\"\n Determine the PLINK file format based on file extensions and companion files.\n \n Args:\n file_path (str or Path): Path to the input file\n \n Returns:\n str: 'plink1' or 'plink2'\n \"\"\"\n # Convert to string if it's a Path object\n file_path = str(file_path)\n \n # Check direct file extensions\n if file_path.endswith('.bed'):\n return 'plink1'\n elif file_path.endswith('.pgen'):\n return 'plink2'\n \n # If the file doesn't have a standard extension, try to infer format\n try:\n # Remove the file extension if present\n base_path = file_path.rsplit('.', 1)[0] if '.' in file_path else file_path\n \n # Check for PLINK1 companion files\n plink1_companion_files = [\n f\"{base_path}.bim\",\n f\"{base_path}.fam\"\n ]\n \n # Check for PLINK2 companion files\n plink2_companion_files = [\n f\"{base_path}.pvar\",\n f\"{base_path}.psam\"\n ]\n \n # Check PLINK1 format\n if all(os.path.exists(f) for f in plink1_companion_files):\n return 'plink1'\n \n # Check PLINK2 format\n if all(os.path.exists(f) for f in plink2_companion_files):\n return 'plink2'\n \n except Exception as e:\n print(f\"Error determining PLINK format: {e}\")\n \n # Default to PLINK1 if can't determine\n return 'plink1'\n\n# Get the appropriate PLINK command prefix\ndef get_plink_command_prefix(file_path):\n format_type = determine_plink_format(file_path)\n if format_type == 'plink1':\n return \"--bfile\"\n else: # plink2\n return \"--pfile\"\n\n# Get output file extension based on format\ndef get_output_extension(format_type):\n if format_type == 'plink1':\n return \"bed\"\n else: # plink2\n return \"pgen\" \n \n# Choose the make-bed or make-pgen command based on desired output format\ndef get_make_command(output_format):\n if output_format == 'plink1':\n return '--make-bed'\n else: # plink2\n return '--make-pgen'\ngenoFile = get_genotype_file(genoFile)"
+ "#### Step 2. Merge VCF files\n",
+ "\n",
+ "`merge_vcf` concatenates the per-chromosome VCFs into one, the VCF counterpart of Step 2."
]
},
{
"cell_type": "markdown",
"metadata": {
- "kernel": "SoS",
- "tags": []
+ "kernel": "SoS"
},
"source": [
- "## PLINK to VCF"
+ "**Timing**: TBD (on toy dataset)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
- "kernel": "SoS"
+ "kernel": "Bash",
+ "vscode": {
+ "languageId": "plaintext"
+ }
},
"outputs": [],
"source": [
- "[plink_to_vcf_1]\nif isinstance(genoFile, dict):\n genoFile = genoFile.values()\n\ninput: genoFile, group_by = 1\noutput: f'{cwd}/{_input:bn}.vcf.gz'\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output[0]:bn}'\nbash: expand= \"${ }\", stderr = f'{_output:nn}.stderr', stdout = f'{_output:nn}.stdout'\n bash ${modular_script_dir}/data_preprocessing/genotype/genotype_formatting.sh plink_to_vcf_1 \\\n --genoFile \"${_input}\" \\\n --output \"${_output}\" \\\n --mem \"${mem}\" \\\n --numThreads ${numThreads}\n\nbash: expand= \"${ }\", stderr = f'{_output[0]:n}.stderr', stdout = f'{_output[0]:n}.stdout'\n Rscript ${modular_script_dir}/data_preprocessing/genotype/genotype_formatting.R \\\n --step vcf_gz_summary \\\n --data-files \"${_output}\""
+ "sos run pipeline/genotype_formatting.ipynb merge_vcf \\\n",
+ " --genoFile output/genotype_formatting/vcf/protocol_example.genotype.chr*.vcf.gz \\\n",
+ " --cwd output/genotype_formatting/vcf \\\n",
+ " --name protocol_example.genotype.merged"
]
},
{
@@ -247,43 +397,136 @@
"kernel": "SoS"
},
"source": [
- "## VCF to PLINK\n",
+ "### Split by region and compute LD\n",
"\n",
- "Export VCF files to PLINK 1.0 format, **without keeping allele orders by default**. The resulting PLINK will lose ref/alt allele information but will go by major/minor allele, as conventionally used in standard PLINK format. Notice that PLINK 1.0 format does not allow for dosages. PLINK 2.0 format support it, but it is generally not supported by downstreams data analysis. \n",
+ "Both steps read the merged fileset from Route A step 2, so run that first."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "#### Step 1. Split PLINK genotypes into regions\n",
"\n",
- "In the following code block the option `--vcf-half-call m` treat half-call as missing."
+ "`genotype_by_region` cuts one fileset per region in `--region-list`, widened by `--window`. The protocol does not yet ship an example region list, so this command cannot be run against\n",
+ "the toy data as it stands; `input/genotype/protocol_example.genotype.region_list.txt` is the path it expects."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
- "kernel": "SoS"
+ "kernel": "Bash",
+ "vscode": {
+ "languageId": "plaintext"
+ }
},
"outputs": [],
"source": [
- "[vcf_to_plink]\nparameter: remove_duplicates = False\nparameter: add_chr = True\nparameter: keep_dosage = False\n# The path to the file that contains the list of samples to remove (format FID, IID)\nparameter: remove_samples = path('.')\n# The path to the file that contains the list of samples to keep (format FID, IID)\nparameter: keep_samples = path('.')\nfail_if(not (keep_samples.is_file() or keep_samples == path('.')), msg = f'Cannot find ``{keep_samples}``')\nfail_if(not (remove_samples.is_file() or remove_samples == path('.')), msg = f'Cannot find ``{remove_samples}``')\n\nif isinstance(genoFile, dict):\n genoFile = genoFile.values()\n\ninput: genoFile, group_by = 1\noutput: f'{cwd}/{_input:bnn}.{\"pgen\" if keep_dosage else \"bed\"}'\n\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'\nbash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout'\n bash ${modular_script_dir}/data_preprocessing/genotype/genotype_formatting.sh vcf_to_plink \\\n --cwd \"${cwd}\" \\\n --genoFile \"${_input}\" \\\n --numThreads ${numThreads}"
+ "sos run pipeline/genotype_formatting.ipynb genotype_by_region \\\n",
+ " --genoFile output/genotype_formatting/plink/protocol_example.genotype.merged \\\n",
+ " --region-list input/genotype/protocol_example.genotype.region_list.txt \\\n",
+ " --window 0 \\\n",
+ " --cwd output/genotype_formatting/by_region \\\n",
+ " --name protocol_example.genotype"
]
},
{
"cell_type": "markdown",
"metadata": {
- "kernel": "SoS",
- "tags": []
+ "kernel": "SoS"
},
"source": [
- "## Split PLINK genotypes into specified regions"
+ "#### Step 2. LD matrices per region\n",
+ "\n",
+ "`ld_by_region_plink` computes an LD matrix per region and writes a `.ld.list` index alongside. The protocol does not yet ship an example region list, so this command cannot be run against the toy data as it stands; `input/genotype/protocol_example.genotype.region_list.txt` is the path it expects."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
]
},
{
"cell_type": "code",
"execution_count": null,
+ "metadata": {
+ "kernel": "Bash",
+ "vscode": {
+ "languageId": "plaintext"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/genotype_formatting.ipynb ld_by_region_plink \\\n",
+ " --genoFile output/genotype_formatting/plink/protocol_example.genotype.merged \\\n",
+ " --region-list input/genotype/protocol_example.genotype.region_list.txt \\\n",
+ " --float-type 16 \\\n",
+ " --cwd output/genotype_formatting/ld \\\n",
+ " --name protocol_example.genotype"
+ ]
+ },
+ {
+ "cell_type": "markdown",
"metadata": {
"kernel": "SoS"
},
+ "source": [
+ "### Building a file manifest by hand"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "`write_data_list` collects paths into a manifest that downstream modules read instead of globbing.\n",
+ "The `_1` conversion steps emit their own `*_files.txt` automatically; this workflow is for building\n",
+ "one by hand."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "kernel": "Bash",
+ "vscode": {
+ "languageId": "plaintext"
+ }
+ },
"outputs": [],
"source": [
- "[genotype_by_region_1]\n# cis window size\nparameter: window = 0\n# Region definition\nparameter: region_list = path\nregions = [x.strip().split() for x in open(region_list).readlines() if x.strip() and not x.strip().startswith('#')]\ninput: genoFile, for_each = 'regions'\noutput: f'{cwd}/{region_list:bn}_genotype_by_region/{_input:bn}.{_regions[3]}.bed'\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'\nbash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout'\n bash ${modular_script_dir}/data_preprocessing/genotype/genotype_formatting.sh genotype_by_region_1 \\\n --genoFile \"${_input}\" \\\n --output \"${_output}\" \\\n --region-chrom \"${_regions[0]}\" \\\n --region-start \"${_regions[1]}\" \\\n --region-end \"${_regions[2]}\" \\\n --window ${window} \\\n --numThreads ${numThreads}\n\nbash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout'\n Rscript ${modular_script_dir}/data_preprocessing/genotype/genotype_formatting.R \\\n --step file_size_summary \\\n --data-files \"${_output}\""
+ "sos run pipeline/genotype_formatting.ipynb write_data_list \\\n",
+ " --data-files `ls output/genotype_formatting/plink/protocol_example.genotype.chr*.bed` \\\n",
+ " --out output/genotype_formatting/plink/protocol_example.genotype.plink_files.txt \\\n",
+ " --ext bed \\\n",
+ " --cwd output/genotype_formatting/plink \\\n",
+ " --name protocol_example.genotype"
]
},
{
@@ -292,11 +535,7 @@
"kernel": "SoS"
},
"source": [
- "## Compute LD matrices for given input region\n",
- "\n",
- "### PLINK based implementation\n",
- "\n",
- "**FIXME: Hao, I suggest including all contents for LD matrix storage type benchmarking into this repo, so we justify why we would like to save the data as square 0, float 16 and using the RDS format**. Perhaps we should start a folder called \"code/auxillary\" to keep notebooks such as these? You can then remove what you have in the `brain-xqtl-analysis` repository after you migrate all the relevant contents here."
+ "## Command Interface"
]
},
{
@@ -306,7 +545,9 @@
"kernel": "SoS"
},
"outputs": [],
- "source": "[ld_by_region_plink_1]\n# Region definition\nparameter: region_list = path\nparameter: float_type = 16\nregions = [x.strip().split() for x in open(region_list).readlines() if x.strip() and not x.strip().startswith('#')]\ninput: genoFile, for_each = 'regions'\noutput: f'{cwd}/{region_list:bn}_LD/{_input:bn}.{_regions[0]}_{_regions[1]}_{_regions[2]}.float{float_type}.rds'\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'\nbash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout'\n plink --bfile \"${_input:n}\" --out \"${_output:nn}\" \\\n --chr \"${_regions[0]}\" --from-bp \"${_regions[1]}\" --to-bp \"${_regions[2]}\" \\\n --r square0 --make-just-bim --threads ${numThreads}\n Rscript ${modular_script_dir}/data_preprocessing/genotype/genotype_formatting.R \\\n --step ld_by_region_plink_1 \\\n --output \"${_output}\""
+ "source": [
+ "sos run pipeline/genotype_formatting.ipynb -h"
+ ]
},
{
"cell_type": "markdown",
@@ -314,17 +555,90 @@
"kernel": "SoS"
},
"source": [
- "### ldstore2 based implementation\n",
+ "```\n",
+ "usage: sos run pipeline/genotype_formatting.ipynb\n",
+ " [workflow_name | -t targets] [options] [workflow_options]\n",
+ " workflow_name: Single or combined workflows defined in this script\n",
+ " targets: One or more targets to generate\n",
+ " options: Single-hyphen sos parameters (see \"sos run -h\" for details)\n",
+ " workflow_options: Double-hyphen workflow-specific parameters\n",
"\n",
- "This is good for larger sample sizes such as data from UK Biobank although we are not facing this challenge in the FunGen-xQTL project.\n",
+ "Workflows:\n",
+ " plink_to_vcf\n",
+ " vcf_to_plink\n",
+ " genotype_by_region\n",
+ " ld_by_region_plink\n",
+ " genotype_by_chrom\n",
+ " write_data_list\n",
+ " merge_plink\n",
+ " merge_vcf\n",
"\n",
- "**FIXME: we need to build ldstore2 into a container image**. According to Diana it should be \n",
+ "Global Workflow Options:\n",
+ " --modular-script-dir code/script (as path)\n",
+ " --cwd output (as path)\n",
+ " Work directory & output directory\n",
+ " --job-size 1 (as int)\n",
+ " For cluster jobs, number commands to run per job\n",
+ " --walltime 5h\n",
+ " Wall clock time expected\n",
+ " --mem 16G\n",
+ " Memory expected\n",
+ " --numThreads 20 (as int)\n",
+ " Number of threads\n",
+ " --genoFile paths\n",
"\n",
- "```\n",
- "pip3 install https://files.pythonhosted.org/packages/a8/fd/f98ab7dea176f42cb61b80450b795ef19b329e8eb715b87b0d13c2a0854d/ldstore-0.1.9.tar.gz \n",
- "```\n",
+ " the path to a bed file or VCF file, a vector of bed\n",
+ " files or VCF files, or a text file listing the bed files\n",
+ " or VCF files to process\n",
+ " --name VAL (as str, required)\n",
"\n",
- "**FIXME: Diana, what's the input for this workflow?**"
+ "Sections\n",
+ " plink_to_vcf_1:\n",
+ " vcf_to_plink:\n",
+ " Workflow Options:\n",
+ " --[no-]remove-duplicates (default to False)\n",
+ " --[no-]add-chr (default to True)\n",
+ " --[no-]keep-dosage (default to False)\n",
+ " --remove-samples . (as path)\n",
+ " The path to the file that contains the list of samples\n",
+ " to remove (format FID, IID)\n",
+ " --keep-samples . (as path)\n",
+ " The path to the file that contains the list of samples\n",
+ " to keep (format FID, IID)\n",
+ " genotype_by_region_1:\n",
+ " Workflow Options:\n",
+ " --window 0 (as int)\n",
+ " cis window size\n",
+ " --region-list VAL (as path, required)\n",
+ " Region definition\n",
+ " ld_by_region_plink_1:\n",
+ " Workflow Options:\n",
+ " --region-list VAL (as path, required)\n",
+ " Region definition\n",
+ " --float-type 16 (as int)\n",
+ " genotype_by_chrom_1:\n",
+ " Workflow Options:\n",
+ " --chrom VAL VAL ... (as type, required)\n",
+ " genotype_by_chrom_2:\n",
+ " plink_to_vcf_2:\n",
+ " genotype_by_region_2:\n",
+ " ld_by_region_*_2:\n",
+ " Workflow Options:\n",
+ " --region-list VAL (as path, required)\n",
+ " write_data_list:\n",
+ " Workflow Options:\n",
+ " --out VAL (as path, required)\n",
+ " --ext VAL (as str, required)\n",
+ " --data-files paths\n",
+ "\n",
+ " merge_plink:\n",
+ " Workflow Options:\n",
+ " --keep-samples . (as path)\n",
+ " The path to the file that contains the list of samples\n",
+ " to keep (format FID, IID)\n",
+ " --extra-plink-opts (as list)\n",
+ " merge_vcf:\n",
+ "```"
]
},
{
@@ -333,41 +647,206 @@
"kernel": "SoS"
},
"source": [
- "#### Create `master` file for `ldstore2`\n",
+ "## Workflow implementation"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "outputs": [],
+ "source": [
+ "[global]\n",
+ "parameter: modular_script_dir = path('code/script') # override with --modular-script-dir\n",
+ "# Work directory & output directory\n",
+ "parameter: cwd = path(\"output\")\n",
+ "import re\n",
+ "# For cluster jobs, number commands to run per job\n",
+ "parameter: job_size = 1\n",
+ "# Wall clock time expected\n",
+ "parameter: walltime = \"5h\"\n",
+ "# Memory expected\n",
+ "parameter: mem = \"16G\"\n",
+ "# Number of threads\n",
+ "parameter: numThreads = 20\n",
+ "# the path to a bed file or VCF file, a vector of bed files or VCF files, or a text file listing the bed files or VCF files to process\n",
+ "parameter: genoFile = paths\n",
+ "parameter: name = str\n",
+ "# use this function to edit memory string for PLINK input\n",
+ "from sos.utils import expand_size\n",
+ "cwd = f\"{cwd:a}\"\n",
"\n",
- "The master file is a semicolon-separated text file and contains no space. It contains the following mandatory column names and one dataset per line:\n",
+ "import os\n",
+ "def get_genotype_file(geno_file_paths):\n",
+ " def valid_geno_file(x):\n",
+ " suffixes = path(x).suffixes\n",
+ " if suffixes[-1] in ['.bed', '.pgen']:\n",
+ " return True\n",
+ " if len(suffixes) > 1 and ''.join(suffixes[-2:]) == \".vcf.gz\":\n",
+ " return True\n",
+ " return False\n",
+ " #\n",
+ " def complete_geno_path(x, geno_file):\n",
+ " if not valid_geno_file(x):\n",
+ " raise ValueError(f\"Genotype file {x} should be VCF (end with .vcf.gz) or PLINK bed file (end with .bed)\")\n",
+ " if not os.path.isfile(x):\n",
+ " # relative path\n",
+ " if not os.path.isfile(f'{geno_file:ad}/' + x):\n",
+ " raise ValueError(f\"Cannot find genotype file {x}\")\n",
+ " else:\n",
+ " x = f'{geno_file:ad}/' + x\n",
+ " return x\n",
+ " # \n",
+ " def format_chrom(chrom):\n",
+ " if chrom.startswith('chr'):\n",
+ " chrom = chrom[3:]\n",
+ " return chrom\n",
+ " # Inputs are either VCF or bed, or a vector of them \n",
+ " if len(geno_file_paths) > 1:\n",
+ " if all([valid_geno_file(x) for x in geno_file_paths]):\n",
+ " return paths(geno_file_paths)\n",
+ " else: \n",
+ " raise ValueError(f\"Invalid input {geno_file_paths}\")\n",
+ " # Input is one genotype file or text list of genotype files\n",
+ " geno_file = geno_file_paths[0]\n",
+ " if valid_geno_file(geno_file):\n",
+ " return paths(geno_file)\n",
+ " else: \n",
+ " units = [x.strip().split() for x in open(geno_file).readlines() if x.strip() and not x.strip().startswith('#')]\n",
+ " if all([len(x) == 1 for x in units]):\n",
+ " return paths([complete_geno_path(x[0], geno_file) for x in units])\n",
+ " elif all([len(x) == 2 for x in units]):\n",
+ " genos = dict([(format_chrom(x[0]), path(complete_geno_path(x[1], geno_file))) for x in units])\n",
+ " else:\n",
+ " raise ValueError(f\"{geno_file} should contain one column of file names, or two columns of chrom number and corresponding file name\")\n",
+ " return genos\n",
"\n",
- "**FIXME: Diana, this documentation is not clearly written. I cannot understand it. What are the mandatory column names? What does it mean by one data-set per line?**\n",
+ "# Determine if the file is in PLINK1 (BED/BIM/FAM) or PLINK2 (PGEN/PVAR/PSAM) format\n",
+ "def determine_plink_format(file_path):\n",
+ " \"\"\"\n",
+ " Determine the PLINK file format based on file extensions and companion files.\n",
+ " \n",
+ " Args:\n",
+ " file_path (str or Path): Path to the input file\n",
+ " \n",
+ " Returns:\n",
+ " str: 'plink1' or 'plink2'\n",
+ " \"\"\"\n",
+ " # Convert to string if it's a Path object\n",
+ " file_path = str(file_path)\n",
+ " \n",
+ " # Check direct file extensions\n",
+ " if file_path.endswith('.bed'):\n",
+ " return 'plink1'\n",
+ " elif file_path.endswith('.pgen'):\n",
+ " return 'plink2'\n",
+ " \n",
+ " # If the file doesn't have a standard extension, try to infer format\n",
+ " try:\n",
+ " # Remove the file extension if present\n",
+ " base_path = file_path.rsplit('.', 1)[0] if '.' in file_path else file_path\n",
+ " \n",
+ " # Check for PLINK1 companion files\n",
+ " plink1_companion_files = [\n",
+ " f\"{base_path}.bim\",\n",
+ " f\"{base_path}.fam\"\n",
+ " ]\n",
+ " \n",
+ " # Check for PLINK2 companion files\n",
+ " plink2_companion_files = [\n",
+ " f\"{base_path}.pvar\",\n",
+ " f\"{base_path}.psam\"\n",
+ " ]\n",
+ " \n",
+ " # Check PLINK1 format\n",
+ " if all(os.path.exists(f) for f in plink1_companion_files):\n",
+ " return 'plink1'\n",
+ " \n",
+ " # Check PLINK2 format\n",
+ " if all(os.path.exists(f) for f in plink2_companion_files):\n",
+ " return 'plink2'\n",
+ " \n",
+ " except Exception as e:\n",
+ " print(f\"Error determining PLINK format: {e}\")\n",
+ " \n",
+ " # Default to PLINK1 if can't determine\n",
+ " return 'plink1'\n",
"\n",
- "- For the Z file, the format should be `rsid:chrom:pos:a1:a2`. Formatting for chromosome should be `01,02,03` etc\n",
- "- List of samples\n",
+ "# Get the appropriate PLINK command prefix\n",
+ "def get_plink_command_prefix(file_path):\n",
+ " format_type = determine_plink_format(file_path)\n",
+ " if format_type == 'plink1':\n",
+ " return \"--bfile\"\n",
+ " else: # plink2\n",
+ " return \"--pfile\"\n",
"\n",
- "**The LDstore draft is currently availale [here](https://github.com/statgenetics/UKBB_GWAS_dev/blob/master/workflow/111722_LDstore.ipynb) with the code to prepare for the genotypic input [here](https://github.com/statgenetics/UKBB_GWAS_dev/blob/master/workflow/113022_bgenix_ldblocks.ipynb). A minimal working example can be found [here]**"
+ "# Get output file extension based on format\n",
+ "def get_output_extension(format_type):\n",
+ " if format_type == 'plink1':\n",
+ " return \"bed\"\n",
+ " else: # plink2\n",
+ " return \"pgen\" \n",
+ " \n",
+ "# Choose the make-bed or make-pgen command based on desired output format\n",
+ "def get_make_command(output_format):\n",
+ " if output_format == 'plink1':\n",
+ " return '--make-bed'\n",
+ " else: # plink2\n",
+ " return '--make-pgen'\n",
+ "genoFile = get_genotype_file(genoFile)"
]
},
{
"cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS",
+ "tags": []
+ },
+ "source": [
+ "### PLINK to VCF"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
"metadata": {
"kernel": "SoS"
},
+ "outputs": [],
"source": [
- "## Split PLINK by Chromosome"
+ "[plink_to_vcf_1]\n",
+ "if isinstance(genoFile, dict):\n",
+ " genoFile = genoFile.values()\n",
+ "\n",
+ "input: genoFile, group_by = 1\n",
+ "output: f'{cwd}/{_input:bn}.vcf.gz'\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output[0]:bn}'\n",
+ "bash: expand= \"${ }\", stderr = f'{_output:nn}.stderr', stdout = f'{_output:nn}.stdout'\n",
+ " bash ${modular_script_dir}/data_preprocessing/genotype/genotype_formatting.sh plink_to_vcf_1 \\\n",
+ " --genoFile \"${_input}\" \\\n",
+ " --output \"${_output}\" \\\n",
+ " --mem \"${mem}\" \\\n",
+ " --numThreads ${numThreads}\n",
+ "\n",
+ "bash: expand= \"${ }\", stderr = f'{_output[0]:n}.stderr', stdout = f'{_output[0]:n}.stdout'\n",
+ " Rscript ${modular_script_dir}/data_preprocessing/genotype/genotype_formatting.R \\\n",
+ " --step vcf_gz_summary \\\n",
+ " --data-files \"${_output}\""
]
},
{
"cell_type": "markdown",
"metadata": {
- "kernel": "SoS",
- "vscode": {
- "languageId": "plaintext"
- }
+ "kernel": "SoS"
},
"source": [
- "## Pls note that .pvar file have meta_file headers before the variant info dataframe\n",
- "sos run /home/rl3328/xqtl-protocol/code/data_preprocessing/genotype/genotype_formatting.ipynb genotype_by_chrom \\\n",
- " --genoFile /home/rl3328/motor_data/geno_data/final/LD_pruning/chrAll_GRCh38_biallelic.motor_eQTL.plink_qc.pgen \\\n",
- " --cwd /home/rl3328/motor_data/geno_data/final/genotype_by_chrom \\\n",
- " --chrom `grep -v '^#' /home/rl3328/motor_data/geno_data/final/LD_pruning/chrAll_GRCh38_biallelic.motor_eQTL.plink_qc.pvar | cut -f 1 | uniq | sed \"s/chr//g\"`"
+ "### VCF to PLINK\n",
+ "\n",
+ "Export VCF files to PLINK 1.0 format, **without keeping allele orders by default**. The resulting PLINK will lose ref/alt allele information but will go by major/minor allele, as conventionally used in standard PLINK format. Notice that PLINK 1.0 format does not allow for dosages. PLINK 2.0 format support it, but it is generally not supported by downstreams data analysis. \n",
+ "\n",
+ "In the following code block the option `--vcf-half-call m` treat half-call as missing."
]
},
{
@@ -378,7 +857,39 @@
},
"outputs": [],
"source": [
- "[genotype_by_chrom_1]\nstop_if(len(paths(genoFile))>1, msg = \"This workflow expects one input genotype file.\")\nparameter: chrom = list\nchrom = list(set(chrom))\ninput: genoFile, for_each = \"chrom\"\nplink_command = get_plink_command_prefix(_input)\noutput_format = determine_plink_format(_input)\noutput_ext = get_output_extension(output_format)\nmake_command = get_make_command(output_format)\noutput: f'{cwd}/{_input:bn}.{_chrom}.{output_ext}'\n# look up for genotype file\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'\nbash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout'\n bash ${modular_script_dir}/data_preprocessing/genotype/genotype_formatting.sh genotype_by_chrom \\\n --cwd \"${cwd}\" \\\n --genoFile \"${_input}\" \\\n --name \"${name}\" \\\n --output \"${_output}\" \\\n --plink-command \"${plink_command}\" \\\n --make-command \"${make_command}\" \\\n --output-format \"${output_format}\" \\\n --chrom ${_chrom} \\\n --mem \"${mem}\" \\\n --numThreads ${numThreads}"
+ "[vcf_to_plink]\n",
+ "parameter: remove_duplicates = False\n",
+ "parameter: add_chr = True\n",
+ "parameter: keep_dosage = False\n",
+ "# The path to the file that contains the list of samples to remove (format FID, IID)\n",
+ "parameter: remove_samples = path('.')\n",
+ "# The path to the file that contains the list of samples to keep (format FID, IID)\n",
+ "parameter: keep_samples = path('.')\n",
+ "fail_if(not (keep_samples.is_file() or keep_samples == path('.')), msg = f'Cannot find ``{keep_samples}``')\n",
+ "fail_if(not (remove_samples.is_file() or remove_samples == path('.')), msg = f'Cannot find ``{remove_samples}``')\n",
+ "\n",
+ "if isinstance(genoFile, dict):\n",
+ " genoFile = genoFile.values()\n",
+ "\n",
+ "input: genoFile, group_by = 1\n",
+ "output: f'{cwd}/{_input:bnn}.{\"pgen\" if keep_dosage else \"bed\"}'\n",
+ "\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'\n",
+ "bash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout'\n",
+ " bash ${modular_script_dir}/data_preprocessing/genotype/genotype_formatting.sh vcf_to_plink \\\n",
+ " --cwd \"${cwd}\" \\\n",
+ " --genoFile \"${_input}\" \\\n",
+ " --numThreads ${numThreads}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS",
+ "tags": []
+ },
+ "source": [
+ "### Split PLINK genotypes into specified regions"
]
},
{
@@ -389,7 +900,40 @@
},
"outputs": [],
"source": [
- "[genotype_by_chrom_2]\ninput: group_by = \"all\"\noutput_format = determine_plink_format(_input)\noutput_ext = get_output_extension(output_format)\noutput: f'{_input[0]:nn}.{step_name[:-2]}_files.txt'\nsos_run(\"write_data_list\", data_files = _input, out = _output, ext = f\"{output_ext}\")"
+ "[genotype_by_region_1]\n",
+ "# cis window size\n",
+ "parameter: window = 0\n",
+ "# Region definition\n",
+ "parameter: region_list = path\n",
+ "regions = [x.strip().split() for x in open(region_list).readlines() if x.strip() and not x.strip().startswith('#')]\n",
+ "input: genoFile, for_each = 'regions'\n",
+ "output: f'{cwd}/{region_list:bn}_genotype_by_region/{_input:bn}.{_regions[3]}.bed'\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'\n",
+ "bash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout'\n",
+ " bash ${modular_script_dir}/data_preprocessing/genotype/genotype_formatting.sh genotype_by_region_1 \\\n",
+ " --genoFile \"${_input}\" \\\n",
+ " --output \"${_output}\" \\\n",
+ " --region-chrom \"${_regions[0]}\" \\\n",
+ " --region-start \"${_regions[1]}\" \\\n",
+ " --region-end \"${_regions[2]}\" \\\n",
+ " --window ${window} \\\n",
+ " --numThreads ${numThreads}\n",
+ "\n",
+ "bash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout'\n",
+ " Rscript ${modular_script_dir}/data_preprocessing/genotype/genotype_formatting.R \\\n",
+ " --step file_size_summary \\\n",
+ " --data-files \"${_output}\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### Compute LD matrices for given input region\n",
+ "\n",
+ "#### PLINK based implementation"
]
},
{
@@ -400,7 +944,64 @@
},
"outputs": [],
"source": [
- "[plink_to_vcf_2]\ninput: group_by = \"all\"\noutput: f'{_input[0]:nnn}.{step_name[:-2]}_files.txt'\nsos_run(\"write_data_list\", data_files = _input, out = _output, ext = \"vcf.gz\")"
+ "[ld_by_region_plink_1]\n",
+ "# Region definition\n",
+ "parameter: region_list = path\n",
+ "parameter: float_type = 16\n",
+ "regions = [x.strip().split() for x in open(region_list).readlines() if x.strip() and not x.strip().startswith('#')]\n",
+ "input: genoFile, for_each = 'regions'\n",
+ "output: f'{cwd}/{region_list:bn}_LD/{_input:bn}.{_regions[0]}_{_regions[1]}_{_regions[2]}.float{float_type}.rds'\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'\n",
+ "bash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout'\n",
+ " plink --bfile \"${_input:n}\" --out \"${_output:nn}\" \\\n",
+ " --chr \"${_regions[0]}\" --from-bp \"${_regions[1]}\" --to-bp \"${_regions[2]}\" \\\n",
+ " --r square0 --make-just-bim --threads ${numThreads}\n",
+ " Rscript ${modular_script_dir}/data_preprocessing/genotype/genotype_formatting.R \\\n",
+ " --step ld_by_region_plink_1 \\\n",
+ " --output \"${_output}\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "#### ldstore2 based implementation\n",
+ "\n",
+ "**Not implemented.** No `ldstore2` section exists in this notebook and the tool is not in the\n",
+ "container, so nothing here is runnable; `ld_by_region_plink` above is the only LD workflow. ldstore2\n",
+ "would suit larger cohorts such as UK Biobank, which is not a constraint in the FunGen-xQTL project.\n",
+ "\n",
+ "For whoever picks this up, the draft lives outside this repository:\n",
+ "[LDstore workflow](https://github.com/statgenetics/UKBB_GWAS_dev/blob/master/workflow/111722_LDstore.ipynb)\n",
+ "and [genotype preparation](https://github.com/statgenetics/UKBB_GWAS_dev/blob/master/workflow/113022_bgenix_ldblocks.ipynb).\n",
+ "It needs a semicolon-separated master file with one dataset per line, Z-file IDs formatted as\n",
+ "`rsid:chrom:pos:a1:a2` with zero-padded chromosomes, plus a sample list."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### Split PLINK by Chromosome\n",
+ "\n",
+ "One workflow covers both formats: `genotype_by_chrom` writes `{_chrom}.{output_ext}`, so\n",
+ "`--output-ext` chooses a PLINK fileset or a VCF per chromosome."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS",
+ "vscode": {
+ "languageId": "plaintext"
+ }
+ },
+ "source": [
+ "Note that .pvar file have meta_file headers before the variant info dataframe"
]
},
{
@@ -411,7 +1012,30 @@
},
"outputs": [],
"source": [
- "[genotype_by_region_2]\ninput: group_by = \"all\"\noutput: f'{_input[0]:nn}.{step_name[:-2]}_files.txt'\nsos_run(\"write_data_list\", data_files = _input, out = _output, ext = \"bed\")"
+ "[genotype_by_chrom_1]\n",
+ "stop_if(len(paths(genoFile))>1, msg = \"This workflow expects one input genotype file.\")\n",
+ "parameter: chrom = list\n",
+ "chrom = list(set(chrom))\n",
+ "input: genoFile, for_each = \"chrom\"\n",
+ "plink_command = get_plink_command_prefix(_input)\n",
+ "output_format = determine_plink_format(_input)\n",
+ "output_ext = get_output_extension(output_format)\n",
+ "make_command = get_make_command(output_format)\n",
+ "output: f'{cwd}/{_input:bn}.{_chrom}.{output_ext}'\n",
+ "# look up for genotype file\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'\n",
+ "bash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout'\n",
+ " bash ${modular_script_dir}/data_preprocessing/genotype/genotype_formatting.sh genotype_by_chrom \\\n",
+ " --cwd \"${cwd}\" \\\n",
+ " --genoFile \"${_input}\" \\\n",
+ " --name \"${name}\" \\\n",
+ " --output \"${_output}\" \\\n",
+ " --plink-command \"${plink_command}\" \\\n",
+ " --make-command \"${make_command}\" \\\n",
+ " --output-format \"${output_format}\" \\\n",
+ " --chrom ${_chrom} \\\n",
+ " --mem \"${mem}\" \\\n",
+ " --numThreads ${numThreads}"
]
},
{
@@ -422,7 +1046,12 @@
},
"outputs": [],
"source": [
- "[ld_by_region_*_2]\nparameter: region_list = path\ninput: group_by = \"all\"\noutput: f'{cwd}/{region_list:bn}_LD/{genoFile:bn}.ld.list'\nsos_run(\"write_data_list\", data_files = _input, out = _output, ext = \"npy.gz\")"
+ "[genotype_by_chrom_2]\n",
+ "input: group_by = \"all\"\n",
+ "output_format = determine_plink_format(_input)\n",
+ "output_ext = get_output_extension(output_format)\n",
+ "output: f'{_input[0]:nn}.{step_name[:-2]}_files.txt'\n",
+ "sos_run(\"write_data_list\", data_files = _input, out = _output, ext = f\"{output_ext}\")"
]
},
{
@@ -433,27 +1062,39 @@
},
"outputs": [],
"source": [
- "[write_data_list]\nparameter: out = path\nparameter: ext = str\nparameter: data_files = paths\ninput: data_files\noutput: out\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'\nbash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout'\n Rscript ${modular_script_dir}/data_preprocessing/genotype/genotype_formatting.R \\\n --step write_data_list \\\n --data-files \"${\"::\".join([str(x) for x in _input])}\" \\\n --ext \"${ext}\" \\\n --output \"${_output}\" \\\n --numThreads ${numThreads}"
+ "[plink_to_vcf_2]\n",
+ "input: group_by = \"all\"\n",
+ "output: f'{_input[0]:nnn}.{step_name[:-2]}_files.txt'\n",
+ "sos_run(\"write_data_list\", data_files = _input, out = _output, ext = \"vcf.gz\")"
]
},
{
- "cell_type": "markdown",
+ "cell_type": "code",
+ "execution_count": null,
"metadata": {
"kernel": "SoS"
},
+ "outputs": [],
"source": [
- "## Split VCF by Chromosome\n",
- "\n",
- "**FIXME: add this as needed**"
+ "[genotype_by_region_2]\n",
+ "input: group_by = \"all\"\n",
+ "output: f'{_input[0]:nn}.{step_name[:-2]}_files.txt'\n",
+ "sos_run(\"write_data_list\", data_files = _input, out = _output, ext = \"bed\")"
]
},
{
- "cell_type": "markdown",
+ "cell_type": "code",
+ "execution_count": null,
"metadata": {
"kernel": "SoS"
},
+ "outputs": [],
"source": [
- "## Merge PLINK files"
+ "[ld_by_region_*_2]\n",
+ "parameter: region_list = path\n",
+ "input: group_by = \"all\"\n",
+ "output: f'{cwd}/{region_list:bn}_LD/{genoFile:bn}.ld.list'\n",
+ "sos_run(\"write_data_list\", data_files = _input, out = _output, ext = \"npy.gz\")"
]
},
{
@@ -464,7 +1105,20 @@
},
"outputs": [],
"source": [
- "[merge_plink]\nskip_if(len(genoFile) == 1)\n# File prefix for the analysis output\nparameter: name = str\n# The path to the file that contains the list of samples to keep (format FID, IID)\nparameter: keep_samples = path('.')\nparameter: extra_plink_opts = []\nparameter: mem = '300G'\ninput: genoFile, group_by = 'all'\noutput: f\"{cwd}/{name}{_input[0]:x}\"\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'\nbash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout'\n bash ${modular_script_dir}/data_preprocessing/genotype/genotype_formatting.sh merge_plink \\\n --cwd \"${cwd}\" \\\n --genoFile \"${_input}\" \\\n --name \"${name}\" \\\n --mem \"${mem}\" \\\n --numThreads ${numThreads}"
+ "[write_data_list]\n",
+ "parameter: out = path\n",
+ "parameter: ext = str\n",
+ "parameter: data_files = paths\n",
+ "input: data_files\n",
+ "output: out\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'\n",
+ "bash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout'\n",
+ " Rscript ${modular_script_dir}/data_preprocessing/genotype/genotype_formatting.R \\\n",
+ " --step write_data_list \\\n",
+ " --data-files \"${\"::\".join([str(x) for x in _input])}\" \\\n",
+ " --ext \"${ext}\" \\\n",
+ " --output \"${_output}\" \\\n",
+ " --numThreads ${numThreads}"
]
},
{
@@ -473,7 +1127,35 @@
"kernel": "SoS"
},
"source": [
- "## Merge VCF files"
+ "### Merge PLINK files"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "outputs": [],
+ "source": [
+ "[merge_plink]\n",
+ "skip_if(len(genoFile) == 1)\n",
+ "# File prefix for the analysis output\n",
+ "parameter: name = str\n",
+ "# The path to the file that contains the list of samples to keep (format FID, IID)\n",
+ "parameter: keep_samples = path('.')\n",
+ "parameter: extra_plink_opts = []\n",
+ "parameter: mem = '300G'\n",
+ "input: genoFile, group_by = 'all'\n",
+ "output: f\"{cwd}/{name}{_input[0]:x}\"\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'\n",
+ "bash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout'\n",
+ " bash ${modular_script_dir}/data_preprocessing/genotype/genotype_formatting.sh merge_plink \\\n",
+ " --cwd \"${cwd}\" \\\n",
+ " --genoFile \"${_input}\" \\\n",
+ " --name \"${name}\" \\\n",
+ " --mem \"${mem}\" \\\n",
+ " --numThreads ${numThreads}"
]
},
{
@@ -482,9 +1164,7 @@
"kernel": "SoS"
},
"source": [
- "## Anticipated Results\n",
- "\n",
- "The pipeline produces output files in the `output/` subdirectory named after the workflow step. Verify success by checking that output files exist and are non-empty. See the **Output** section above for the expected file names and formats."
+ "### Merge VCF files"
]
},
{
@@ -495,7 +1175,22 @@
},
"outputs": [],
"source": [
- "[merge_vcf]\nskip_if(len(genoFile) == 1)\n# File prefix for the analysis output\nparameter: name = str\ninput: genoFile, group_by = 'all'\noutput: f\"{cwd}/{name}.vcf.gz\"\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'\nbash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout'\n bash ${modular_script_dir}/data_preprocessing/genotype/genotype_formatting.sh merge_vcf \\\n --genoFile \"${_input}\" \\\n --output \"${_output}\"\n\nbash: expand= \"${ }\", stderr = f'{_output[0]:n}.stderr', stdout = f'{_output[0]:n}.stdout'\n Rscript ${modular_script_dir}/data_preprocessing/genotype/genotype_formatting.R \\\n --step vcf_gz_summary \\\n --data-files \"${_output}\""
+ "[merge_vcf]\n",
+ "skip_if(len(genoFile) == 1)\n",
+ "# File prefix for the analysis output\n",
+ "parameter: name = str\n",
+ "input: genoFile, group_by = 'all'\n",
+ "output: f\"{cwd}/{name}.vcf.gz\"\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'\n",
+ "bash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout'\n",
+ " bash ${modular_script_dir}/data_preprocessing/genotype/genotype_formatting.sh merge_vcf \\\n",
+ " --genoFile \"${_input}\" \\\n",
+ " --output \"${_output}\"\n",
+ "\n",
+ "bash: expand= \"${ }\", stderr = f'{_output[0]:n}.stderr', stdout = f'{_output[0]:n}.stdout'\n",
+ " Rscript ${modular_script_dir}/data_preprocessing/genotype/genotype_formatting.R \\\n",
+ " --step vcf_gz_summary \\\n",
+ " --data-files \"${_output}\""
]
}
],
diff --git a/code/SoS/data_preprocessing/genotype_preprocessing.ipynb b/code/SoS/data_preprocessing/genotype_preprocessing.ipynb
index 741c519da..b7172a5cd 100644
--- a/code/SoS/data_preprocessing/genotype_preprocessing.ipynb
+++ b/code/SoS/data_preprocessing/genotype_preprocessing.ipynb
@@ -6,9 +6,9 @@
"kernel": "SoS"
},
"source": [
- "# Genotype Preprocessing\n",
+ "# Genotype preprocessing\n",
"\n",
- "This notebook is a minimal working example (MWE) of the genotype preprocessing section of the xQTL protocol. It chains the genotype worker pipelines on the bundled toy dataset (60 samples, `protocol_example.*`) so the whole section runs end-to-end without editing."
+ "Convert raw genotype VCFs into quality-controlled, sample-matched PLINK datasets and genetic principal components for downstream xQTL analysis."
]
},
{
@@ -17,16 +17,11 @@
"kernel": "SoS"
},
"source": [
- "## Description\n",
+ "#### Miniprotocol Timing\n",
"\n",
- "The genotype preprocessing section takes raw genotype data (VCF) through quality control, format conversion, and produces clean PLINK files plus genetic principal components for use as covariates downstream. It chains four worker pipelines:\n",
+ "This is the total duration for all phases; module-specific timings appear on their respective pages.\n",
"\n",
- "- `VCF_QC.ipynb` — quality control on VCF files\n",
- "- `genotype_formatting.ipynb` — VCF→PLINK conversion, merging, and partition by chromosome\n",
- "- `GWAS_QC.ipynb` — QC on PLINK files, sample matching, kinship, and preparing unrelated individuals\n",
- "- `PCA.ipynb` — principal component analysis on the unrelated samples\n",
- "\n",
- "**Timing:** ~3–5 min for the toy dataset on a single node."
+ "Timing: ~3-5 min (on the toy dataset)"
]
},
{
@@ -35,32 +30,11 @@
"kernel": "SoS"
},
"source": [
- "## Input Files\n",
+ "## Overview\n",
"\n",
- "| File | Description |\n",
- "| --- | --- |\n",
- "| `input/genotype/protocol_example.genotype.chr*.vcf.gz` | Per-chromosome toy genotype VCFs (60 samples) |\n",
- "| `input/reference_data/00-All.add_chr.variants.gz` | dbSNP variant reference (for annotation) |\n",
- "| `input/reference_data/GRCh38_full_analysis_set_plus_decoy_hla.noALT_noHLA_noDecoy_ERCC.fasta` | GRCh38 reference genome |\n",
- "| `input/rnaseq/protocol_example.rnaseq.bed.gz` | Molecular phenotype, used for sample matching |"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "## Output Files\n",
+ "This mini-protocol walks through how raw genotype VCFs are normalized, converted to PLINK, quality-controlled, matched to molecular-phenotype samples, and prepared for population-structure adjustment. Each step calls a workflow from [`VCF_QC.ipynb`](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/genotype/VCF_QC.html), [`genotype_formatting.ipynb`](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/genotype/genotype_formatting.html), [`GWAS_QC.ipynb`](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/genotype/GWAS_QC.html), or [`PCA.ipynb`](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/genotype/PCA.html).\n",
"\n",
- "| File | Description |\n",
- "| --- | --- |\n",
- "| `output/vcf_qc/*.leftnorm.*.vcf.gz` | QC-passed, left-normalized VCF |\n",
- "| `output/genotype_formatting/plink/protocol_example.genotype.merged.bed` | Merged genome-wide PLINK dataset |\n",
- "| `output/gwas_qc/plink/protocol_example.genotype.merged.plink_qc.bed` | QC-passed PLINK dataset |\n",
- "| `output/genotype_formatting/genotype_by_chrom/*.bed` | Per-chromosome PLINK files |\n",
- "| `output/gwas_qc/kinship/*.unrelated.bed` | Unrelated-individuals subset |\n",
- "| `output/pca_uf/*.pca.rds` / `*.pca.scree.png` | Genetic principal components and scree plot |"
+ "The commands are organized as selectable routes rather than one mandatory 12-step chain. The standard route estimates principal components in unrelated individuals. Use the conditional extension only when related individuals will remain in the analysis and must be projected into the same PCA space before the datasets are recombined.\n"
]
},
{
@@ -71,7 +45,15 @@
"source": [
"## Steps\n",
"\n",
- "Run the commands below in order from the toy example root. Each one invokes the corresponding worker pipeline through the `pipeline/` symlinks. Outputs from earlier commands feed the later ones."
+ "Choose a route before running commands; the 12 commands are not one mandatory chain.\n",
+ "\n",
+ "| **Analysis goal** | **Commands to run, in order** | **Inputs** |\n",
+ "| --- | --- | --- |\n",
+ "| Produce a QC-passed, chromosome-partitioned PLINK dataset | 1 \u2192 2 \u2192 3 \u2192 4 | `input/genotype/protocol_example.genotype.chr22.vcf.gz` `input/reference_data/00-All.add_chr.variants.gz` `input/reference_data/GRCh38_full_analysis_set_plus_decoy_hla.noALT_noHLA_noDecoy_ERCC.fasta` |\n",
+ "| Match samples and estimate PCA in unrelated individuals | 1 \u2192 2 \u2192 3 \u2192 4 \u2192 5 \u2192 6 \u2192 7 \u2192 8 | Basic-route inputs plus `input/rnaseq/protocol_example.rnaseq.bed.gz` |\n",
+ "| Retain related individuals by PCA projection | 1 \u2192 2 \u2192 3 \u2192 4 \u2192 5 \u2192 6 \u2192 7 \u2192 8 \u2192 9 \u2192 10 \u2192 11 \u2192 12 | Standard-PCA inputs plus `input/covariate/protocol_example.pca_pheno.txt` |\n",
+ "\n",
+ "Run only the row matching the intended analysis goal, following its commands in numerical order.\n"
]
},
{
@@ -80,9 +62,9 @@
"kernel": "SoS"
},
"source": [
- "### Quality control on VCF files\n",
+ "### 1. [Quality-control the input VCF](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/genotype/VCF_QC.html)\n",
"\n",
- "Apply variant-level QC to a genotype VCF (here chromosome 22), annotating against dbSNP and the GRCh38 reference. `--skip_vcf_header_filtering True` is used for the toy VCFs."
+ "**What it does:** Normalize and filter the toy VCF against dbSNP and GRCh38.\n"
]
},
{
@@ -98,7 +80,7 @@
" --dbsnp-variants input/reference_data/00-All.add_chr.variants.gz \\\n",
" --reference-genome input/reference_data/GRCh38_full_analysis_set_plus_decoy_hla.noALT_noHLA_noDecoy_ERCC.fasta \\\n",
" --cwd output/vcf_qc \\\n",
- " --skip_vcf_header_filtering True"
+ " --skip-vcf-header-filtering True"
]
},
{
@@ -107,9 +89,9 @@
"kernel": "SoS"
},
"source": [
- "### Convert VCF to PLINK and merge\n",
+ "### 2. [Convert the QC-passed VCF to PLINK and merge chromosomes](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/genotype/genotype_formatting.html)\n",
"\n",
- "Convert each per-chromosome VCF to PLINK format, then merge the per-chromosome PLINK files into a single genome-wide dataset."
+ "**What it does:** Convert the output of step 1 to PLINK. The merge command generalizes to multiple chromosome-level files.\n"
]
},
{
@@ -121,8 +103,9 @@
"outputs": [],
"source": [
"sos run pipeline/genotype_formatting.ipynb vcf_to_plink \\\n",
- " --genoFile `ls input/genotype/protocol_example.genotype.chr*.vcf.gz | grep -vE \"rawchr|withfmt|add_chr\"` \\\n",
+ " --genoFile output/vcf_qc/protocol_example.genotype.chr22.leftnorm.vcf.gz \\\n",
" --cwd output/genotype_formatting/plink \\\n",
+ " --name protocol_example \\\n",
" -j 4\n",
"\n",
"sos run pipeline/genotype_formatting.ipynb merge_plink \\\n",
@@ -138,9 +121,9 @@
"kernel": "SoS"
},
"source": [
- "### Quality control on PLINK files\n",
+ "### 3. [Apply PLINK-level quality control](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/genotype/GWAS_QC.html)\n",
"\n",
- "Apply genotype-, sample-, and HWE-level filters to the merged PLINK dataset."
+ "**What it does:** Apply genotype-, sample- and Hardy-Weinberg-equilibrium filters to the merged PLINK dataset.\n"
]
},
{
@@ -166,9 +149,9 @@
"kernel": "SoS"
},
"source": [
- "### Partition genotype data by chromosome\n",
+ "### 4. [Partition the QC-passed genotype data by chromosome](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/genotype/genotype_formatting.html)\n",
"\n",
- "Split the QC-passed PLINK dataset back into per-chromosome files, which several downstream xQTL steps expect."
+ "**What it does:** Create chromosome-specific PLINK files required by chromosome-oriented downstream workflows.\n"
]
},
{
@@ -192,9 +175,9 @@
"kernel": "SoS"
},
"source": [
- "### Match samples with the molecular phenotype\n",
+ "### 5. [Match genotype and molecular-phenotype samples](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/genotype/GWAS_QC.html)\n",
"\n",
- "Identify the samples that have both genotype and molecular phenotype data, producing the overlap sample list."
+ "**What it does:** Retain the sample intersection between the QC-passed genotype data and molecular phenotype.\n"
]
},
{
@@ -217,9 +200,9 @@
"kernel": "SoS"
},
"source": [
- "### Estimate kinship\n",
+ "### 6. [Estimate kinship and separate related individuals](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/genotype/GWAS_QC.html)\n",
"\n",
- "Run KING to estimate relatedness among the matched samples and flag related pairs."
+ "**What it does:** Use KING to identify related pairs and produce related and unrelated subsets.\n"
]
},
{
@@ -243,9 +226,9 @@
"kernel": "SoS"
},
"source": [
- "### Prepare unrelated individuals for PCA\n",
+ "### 7. [Prepare the unrelated, LD-pruned PCA subset](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/genotype/GWAS_QC.html)\n",
"\n",
- "Apply a minor-allele-count filter to the unrelated subset produced by the kinship step, yielding the pruned dataset used for PCA."
+ "**What it does:** Apply the minor-allele-count filter and LD pruning used to estimate ancestry axes.\n"
]
},
{
@@ -268,9 +251,23 @@
"kernel": "SoS"
},
"source": [
- "### Principal component analysis on genotype\n",
+ "### 8. [Estimate principal components in unrelated individuals](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/genotype/PCA.html)\n",
"\n",
- "Run flashPCA on the unrelated, pruned dataset to compute genetic principal components for use as covariates."
+ "**What it does:** Estimate the PCA model and scores in unrelated individuals.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "kernel": "Bash"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/PCA.ipynb flashpca \\\n",
+ " --cwd output/pca_uf \\\n",
+ " --genoFile output/gwas_qc/genotype/protocol_example.genotype.merged.plink_qc.protocol_example.king.unrelated.plink_qc.prune.bed \\\n",
+ " --name protocol_example"
]
},
{
@@ -279,65 +276,172 @@
"kernel": "SoS"
},
"source": [
- "## Anticipated Results\n",
+ "### 9. [Extract the related samples at the PCA variants](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/genotype/GWAS_QC.html)\n",
"\n",
- "The pipeline produces output files in the `output/` subdirectory named after the workflow step. Verify success by checking that output files exist and are non-empty. See the **Output** section above for the expected file names and formats."
+ "**What it does:** Restrict the related subset to the variants used by the unrelated-sample PCA model.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
- "kernel": "Bash"
+ "kernel": "Bash",
+ "tags": []
},
"outputs": [],
"source": [
- "sos run pipeline/PCA.ipynb flashpca \\\n",
+ "sos run pipeline/GWAS_QC.ipynb qc_no_prune \\\n",
+ " --cwd output/pca_related \\\n",
+ " --genoFile output/gwas_qc/kinship/protocol_example.genotype.merged.plink_qc.protocol_example.king.related.bed \\\n",
+ " --geno-filter 0 --mind-filter 0.1 --maf-filter 0 \\\n",
+ " --keep-variants output/gwas_qc/genotype/protocol_example.genotype.merged.plink_qc.protocol_example.king.unrelated.plink_qc.prune.in \\\n",
+ " --name for_pca"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### 10. [Project related samples and detect PCA outliers](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/genotype/PCA.html)\n",
+ "\n",
+ "**What it does:** Project related individuals into the PCA space and identify ancestry-space outliers.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "kernel": "Bash",
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/PCA.ipynb project_samples \\\n",
" --cwd output/pca_uf \\\n",
- " --genoFile output/gwas_qc/genotype/protocol_example.genotype.merged.plink_qc.protocol_example.king.unrelated.plink_qc.prune.bed \\\n",
- " --name protocol_example"
+ " --genoFile output/pca_related/protocol_example.genotype.merged.plink_qc.protocol_example.king.related.for_pca.plink_qc.extracted.bed \\\n",
+ " --phenoFile input/covariate/protocol_example.pca_pheno.txt \\\n",
+ " --pca-model output/pca_uf/protocol_example.genotype.merged.plink_qc.protocol_example.king.unrelated.plink_qc.prune.protocol_example.pca.rds \\\n",
+ " --label-col race --pop-col race --name protocol_example --maha-k 2"
]
},
{
"cell_type": "markdown",
- "id": "genotype-pca-for-covariates",
- "metadata": {},
+ "metadata": {
+ "kernel": "SoS"
+ },
"source": [
- "Compute a genotype PCA directly on the basic QC-passed merged dataset ",
- "(MAC-filtered and LD-pruned) and write results to `output/genotype/genotype_pca/`. ",
- "This `.rds` model is used by the covariate formatting step."
+ "### 11. [Remove projected PCA outliers](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/genotype/GWAS_QC.html)\n",
+ "\n",
+ "**What it does:** Remove projected outliers before recombining samples.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "genotype-pca-qc",
"metadata": {
"kernel": "Bash",
"tags": []
},
"outputs": [],
"source": [
- "sos run pipeline/GWAS_QC.ipynb qc \\\n",
- " --cwd output/genotype/genotype_pca \\\n",
- " --genoFile output/gwas_qc/plink/protocol_example.genotype.merged.plink_qc.bed \\\n",
- " --mac-filter 5"
+ "sos run pipeline/GWAS_QC.ipynb qc_no_prune \\\n",
+ " --cwd output/pca_related \\\n",
+ " --genoFile output/pca_related/protocol_example.genotype.merged.plink_qc.protocol_example.king.related.for_pca.plink_qc.extracted.bed \\\n",
+ " --remove-samples output/pca_uf/protocol_example.pca_pheno.pca.projected.outliers \\\n",
+ " --name no_outlier"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### 12. [Recombine unrelated and projected related samples](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/genotype/genotype_formatting.html)\n",
+ "\n",
+ "**What it does:** Merge the unrelated PCA subset with the retained projected related samples for downstream analysis.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "genotype-pca-flashpca",
"metadata": {
"kernel": "Bash",
"tags": []
},
"outputs": [],
"source": [
- "sos run pipeline/PCA.ipynb flashpca \\\n",
- " --cwd output/genotype/genotype_pca \\\n",
- " --genoFile output/genotype/genotype_pca/protocol_example.genotype.merged.plink_qc.plink_qc.prune.bed"
+ "sos run pipeline/genotype_formatting.ipynb merge_plink \\\n",
+ " --genoFile output/gwas_qc/genotype/protocol_example.genotype.merged.plink_qc.protocol_example.king.unrelated.plink_qc.prune.bed \\\n",
+ " output/pca_related/protocol_example.genotype.merged.plink_qc.protocol_example.king.related.for_pca.plink_qc.extracted.no_outlier.plink_qc.bed \\\n",
+ " --cwd output/genotype_final \\\n",
+ " --name protocol_example.qced"
]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "## Output Files\n",
+ "\n",
+ "| Route or step | Products |\n",
+ "| --- | --- |\n",
+ "| Step 1: VCF quality control | `output/vcf_qc/*.leftnorm.vcf.gz` and its index |\n",
+ "| Step 2: PLINK conversion and merge | `output/genotype_formatting/plink/protocol_example.genotype.merged.{bed,bim,fam}` |\n",
+ "| Step 3: PLINK quality control | `output/gwas_qc/plink/*.plink_qc.{bed,bim,fam}` |\n",
+ "| Step 4: Chromosome partitioning | `output/genotype_by_chrom/*.{bed,bim,fam}` |\n",
+ "| Step 5: Sample matching | `output/gwas_qc/genotype/*.sample_genotypes.txt` |\n",
+ "| Step 6: Kinship | KING results and related/unrelated PLINK subsets under `output/gwas_qc/kinship/` |\n",
+ "| Steps 7\u20138: Unrelated-sample PCA | LD-pruned PLINK data under `output/gwas_qc/genotype/`, plus the PCA model, scores and plots under `output/pca_uf/` |\n",
+ "| Steps 9\u201311: Related-sample projection | Variant-matched related PLINK data under `output/pca_related/`, projected PCA scores and the outlier list under `output/pca_uf/`, and an outlier-filtered related subset |\n",
+ "| Step 12: Recombined analysis dataset | `output/genotype_final/protocol_example.qced.{bed,bim,fam}` |\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "## Anticipated Results\n",
+ "\n",
+ "The QC route produces normalized genotype data in PLINK format, including chromosome-partitioned files for downstream xQTL workflows. The standard PCA route additionally produces sample-matched related and unrelated subsets, an LD-pruned unrelated dataset, and principal-component scores estimated without close relatives.\n",
+ "\n",
+ "When related individuals are retained, steps 9\u201312 project them using the unrelated-sample PCA loadings, remove designated projection outliers, and recombine the retained samples into the final analysis dataset.\n",
+ "\n",
+ "Proceed to phenotype and covariate preprocessing before association testing.\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "## Command interface\n",
+ "\n",
+ "List the workflows and parameters available in each module used by this mini-protocol.\n"
+ ],
+ "id": "d48efec1"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "kernel": "Bash"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/VCF_QC.ipynb -h\n",
+ "sos run pipeline/genotype_formatting.ipynb -h\n",
+ "sos run pipeline/GWAS_QC.ipynb -h\n",
+ "sos run pipeline/PCA.ipynb -h\n"
+ ],
+ "id": "56a944bc"
}
],
"metadata": {
@@ -375,5 +479,5 @@
}
},
"nbformat": 4,
- "nbformat_minor": 4
-}
+ "nbformat_minor": 5
+}
\ No newline at end of file
diff --git a/code/SoS/data_preprocessing/phenotype/gene_annotation.ipynb b/code/SoS/data_preprocessing/phenotype/gene_annotation.ipynb
index e6f1c375f..9631ed322 100644
--- a/code/SoS/data_preprocessing/phenotype/gene_annotation.ipynb
+++ b/code/SoS/data_preprocessing/phenotype/gene_annotation.ipynb
@@ -7,25 +7,9 @@
"tags": []
},
"source": [
- "# Gene Coordinate Annotation\n"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "## Description"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "We use a gene coordinate annotation pipeline based on [`pyqtl`, as demonstrated here](https://github.com/broadinstitute/gtex-pipeline/blob/master/qtl/src/eqtl_prepare_expression.py). This adds genomic coordinate annotations to gene-level molecular phenotype files generated in `gct` format and converts them to `bed` format for downstreams analysis.\n"
+ "# Gene Coordinate Annotation\n",
+ "\n",
+ "Attaches genomic coordinates to a molecular phenotype matrix, and emits the region and gene lists that downstream steps split work by."
]
},
{
@@ -34,16 +18,17 @@
"kernel": "SoS"
},
"source": [
- "### Alternative implementation\n",
+ "## Overview\n",
"\n",
- "Previously we use `biomaRt` package in R instead of code from `pyqtl`. The core function calls are:\n",
+ "Molecular phenotype matrices arrive keyed by feature ID alone -- an ENSEMBL gene ID, a UniProt accession, a leafcutter intron-cluster label -- but every downstream QTL step needs a genomic position to define a cis window. This module attaches those coordinates, turning a plain matrix into a coordinate-sorted, bgzipped and tabix-indexed `bed.gz` file.\n",
"\n",
- "```r\n",
- " ensembl = useEnsembl(biomart = \"ensembl\", dataset = \"hsapiens_gene_ensembl\", version = \"$[ensembl_version]\")\n",
- " ensembl_df <- getBM(attributes=c(\"ensembl_gene_id\",\"chromosome_name\", \"start_position\", \"end_position\"),mart=ensembl)\n",
- "```\n",
+ "Coordinates come from a collapsed gene-model GTF, following the [GTEx pipeline](https://github.com/broadinstitute/gtex-pipeline/blob/master/qtl/src/eqtl_prepare_expression.py) convention, so the annotation used here matches the one used to build the GTF in the first place. Feature IDs that the GTF does not contain are dropped rather than guessed at, which is why the row count of the output can be lower than that of the input.\n",
+ "\n",
+ "Which route applies depends on how the features are named. Gene-level expression matrices match the GTF directly. Protein matrices carry `gene_id|UniProt` composite IDs and need an auxiliary mapping to reach a gene. Leafcutter intron clusters are not genes at all: they are genomic intervals that must first be mapped onto the genes they overlap, which also produces the phenotype-group file that grouped multiple-testing correction later consumes. Psichomics PSI output is already grouped by gene symbol, so it only needs that symbol translated to an ENSEMBL ID.\n",
+ "\n",
+ "An older `biomaRt` route is retained for reference. It places a single-base TSS interval rather than the full gene body and is obsolete; new analyses should use the GTF-based route.\n",
"\n",
- "We require ENSEMBL version to be specified explicitly in this pipeline. As of 2021 for the Brain xQTL project, we use ENSEMBL version 103."
+ "**When to run it.** After a molecular phenotype matrix has been assembled and normalised, and before any QTL association scan, since the association modules read the coordinates from this `bed.gz`."
]
},
{
@@ -53,18 +38,32 @@
"tags": []
},
"source": [
- "## Input Files\n",
+ "## Input\n",
+ "\n",
+ "- `--phenoFile` (required): the molecular phenotype matrix to annotate. Feature IDs live in the column named by `--phenotype-id-column`; all remaining columns are samples.\n",
+ "- `--coordinate-annotation` (required): the collapsed gene-model GTF (or, for the `biomaRt` route, the reference the annotation is drawn from) supplying chromosome, start and end per feature.\n",
+ "- `--phenotype-id-column` (default `gene_id`): name of the feature-ID column in `--phenoFile`.\n",
+ "- `--molecular-trait-type` (default `gene`): `gene` for expression, `protein` for proteomics; selects how the feature ID is parsed before the GTF lookup.\n",
+ "- `--auxiliary-id-mapping` (default `.`): two-column map used for the `protein` trait type to reach a gene from a UniProt accession.\n",
+ "- `--sample-participant-lookup` (default `.`): optional two-column sample-to-participant map; supply it to rename matrix columns to participant IDs in the output.\n",
+ "- `--ensembl-version`: ENSEMBL release passed to `biomaRt`; only the `annotate_coord_biomart` workflow reads it, and it must be set explicitly there.\n",
+ "- `--intron-count`: leafcutter intron-count table, read by `map_leafcutter_cluster_to_gene` to enumerate the clusters that need a gene assignment.\n",
+ "- `--map-stra` / `--cwd` / `--container` / `--entrypoint`: intron-to-gene matching mode for cluster mapping (default `site`), output directory (default `output`), and the software environment.\n",
+ "\n",
+ "Example inputs shipped with the protocol:\n",
+ "\n",
+ "- `input/rnaseq/protocol_example.rnaseq.bed.gz` -- toy gene-level bulk RNA-seq matrix.\n",
+ "- `input/rnaseq/protocol_example.rnaseq.gene_ID.tsv` -- the same matrix with a `gene_ID` column, for the `biomaRt` example.\n",
+ "- `input/proteomics/protocol_example.protein.no_coord.tsv` -- toy protein matrix, `gene_id|UniProt` IDs, no coordinates.\n",
+ "- `input/reference_data/Homo_sapiens.GRCh38.103.chr.reformatted.collapse_only.gene` -- the collapsed gene model.\n",
+ "\n",
+ "First rows of the expression matrix:\n",
"\n",
- "| File | Description |\n",
- "|------|-------------|\n",
- "| `input/rnaseq/protocol_example.rnaseq.bed.gz` | Toy bulk RNA-seq expression matrix (gene-level). |\n",
- "| `input/proteomics/protocol_example.protein.no_coord.tsv` | Toy protein matrix with `gene_id\\|UniProt` IDs, no coordinates. |\n",
- "| `input/rnaseq/protocol_example.rnaseq.gene_ID.tsv` | Toy expression matrix with a `gene_ID` column, for the biomaRt example. |\n",
- "| `input/rnaseq/protocol_example.leafcutter.intron_count.tsv` | Toy leafcutter intron-count table (IDs `chr:start:end:clu_N_strand`). |\n",
- "| `input/rnaseq/protocol_example.leafcutter.phenotype.bed.gz` | Toy leafcutter intron-excision phenotype matrix. |\n",
- "| `input/rnaseq/protocol_example.psichomics.phenotype.tsv` | Toy psichomics phenotype matrix (IDs end in `_`). |\n",
- "| `input/reference_data/Homo_sapiens.GRCh38.103.chr.reformatted.collapse_only.gene.ERCC.gtf` | Collapsed gene-model GTF (gene/protein annotation). |\n",
- "| `input/reference_data/Homo_sapiens.GRCh38.103.chr.gtf` | Full gene-model GTF with exons (leafcutter/psichomics). |\n"
+ "```\n",
+ "#chr\tstart\tend\tID\tSAMPLE_001\n",
+ "chr22\t10939387\t10961337\tENSG00000283047\t37.077\n",
+ "chr22\t15528191\t15529138\tENSG00000130538\t0.0\n",
+ "```"
]
},
{
@@ -74,13 +73,29 @@
"tags": []
},
"source": [
- "## Output Files\n",
+ "## Output\n",
+ "\n",
+ "- `.bed.gz` plus its `.tbi` index -- the annotated matrix, coordinate-sorted, with `#chr start end ID` prepended to the sample columns. Written by every annotation workflow.\n",
+ "- `.region_list.txt` -- one row per retained feature giving its coordinates, ID and the path of the `bed.gz` it came from; QTL modules read this to enumerate cis regions.\n",
+ "- `.gene_list.tsv` -- the feature IDs that were successfully matched to the GTF.\n",
+ "- `*.leafcutter.clusters_to_genes.txt` and `*.exon_list` -- from `map_leafcutter_cluster_to_gene`: the cluster-to-gene assignment and the exon table it was derived from.\n",
+ "- `*.phenotype_group.txt` -- from the leafcutter and psichomics isoform workflows: the isoform-to-gene grouping used by grouped multiple-testing correction.\n",
+ "\n",
+ "`output/gene_annotation/protocol_example.rnaseq.bed.bed.gz`:\n",
+ "\n",
+ "```\n",
+ "#chr\tstart\tend\tID\tstrand\tSAMPLE_001\n",
+ "chr22\t10939387\t10961337\tENSG00000283047\t-\t37.0768403309204\n",
+ "chr22\t15528191\t15529138\tENSG00000130538\t+\t0.0\n",
+ "```\n",
"\n",
- "| Output | Description |\n",
- "|--------|-------------|\n",
- "| Coordinate-annotated phenotype (`*.bed.gz`) | The input molecular phenotype matrix with genomic coordinates (`#chr start end ID`) prepended, ready for downstream QTL analysis. |\n",
- "| Cluster-to-gene map (`*.leafcutter.clusters_to_genes.txt`) | (leafcutter) mapping of each intron cluster to its gene(s). |\n",
- "| Phenotype group file (`*.phenotype_group.txt`) | (leafcutter/psichomics) mapping of each feature ID to its gene, for grouped QTL analysis. |\n"
+ "`output/gene_annotation/protocol_example.rnaseq.bed.region_list.txt` (the `path` column holds the absolute location of the `bed.gz`, abbreviated here):\n",
+ "\n",
+ "```\n",
+ "#chr\tstart\tend\tID\tstrand\tpath\n",
+ "chr22\t10939387\t10961337\tENSG00000283047\t-\t.../output/gene_annotation/protocol_example.rnaseq.bed.bed.gz\n",
+ "chr22\t15528191\t15529138\tENSG00000130538\t+\t.../output/gene_annotation/protocol_example.rnaseq.bed.bed.gz\n",
+ "```"
]
},
{
@@ -89,9 +104,9 @@
"kernel": "SoS"
},
"source": [
- "## Steps\n",
+ "## Minimal Working Example\n",
"\n",
- "The example below adds genomic coordinates to a molecular phenotype matrix using the collapsed gene-model GTF. We show two cases: gene expression and proteins.\n"
+ "Each block below is a self-contained route: pick the one that matches how the features in the matrix are named. All of them read from `input/` and write to `output/gene_annotation/`, and none depends on the others having been run."
]
},
{
@@ -109,16 +124,17 @@
"kernel": "SoS"
},
"source": [
- "Timing: <1 min\n"
+ "Annotate a gene-level expression matrix. Each feature ID is an ENSEMBL gene ID that is matched directly against the GTF to obtain its chromosome, start and end coordinates."
]
},
{
"cell_type": "markdown",
"metadata": {
- "kernel": "SoS"
+ "kernel": "SoS",
+ "tags": []
},
"source": [
- "Annotate a gene-level expression matrix. Each feature ID is an ENSEMBL gene ID that is matched directly against the GTF to obtain its chromosome, start and end coordinates."
+ "**Timing**: ~1 min (on toy dataset)"
]
},
{
@@ -160,7 +176,7 @@
"kernel": "SoS"
},
"source": [
- "Timing: <1 min\n"
+ "Annotate a protein matrix. Here each feature ID has the form `gene_id\\|UniProt`; the gene ID portion is matched against the GTF for coordinates and the UniProt portion is retained in the output feature ID. Use `--molecular-trait-type protein` to select this behaviour."
]
},
{
@@ -169,7 +185,7 @@
"kernel": "SoS"
},
"source": [
- "Annotate a protein matrix. Here each feature ID has the form `gene_id\\|UniProt`; the gene ID portion is matched against the GTF for coordinates and the UniProt portion is retained in the output feature ID. Use `--molecular-trait-type protein` to select this behaviour."
+ "**Timing**: ~1 min (on toy dataset)"
]
},
{
@@ -203,7 +219,7 @@
"kernel": "SoS"
},
"source": [
- "Timing: <2 min\n"
+ "For leafcutter splicing data, intron clusters first need to be assigned to genes. The input is a leafcutter intron-count table whose feature IDs have the form `chr:start:end:clu_N_strand`; `--coordinate-annotation` is a full gene-model GTF (with exon records). The default `--map-stra site` maps introns to genes by matching splice-site coordinates to exon boundaries."
]
},
{
@@ -212,7 +228,7 @@
"kernel": "SoS"
},
"source": [
- "For leafcutter splicing data, intron clusters first need to be assigned to genes. The input is a leafcutter intron-count table whose feature IDs have the form `chr:start:end:clu_N_strand`; `--coordinate-annotation` is a full gene-model GTF (with exon records). The default `--map-stra site` maps introns to genes by matching splice-site coordinates to exon boundaries."
+ "**Timing**: ~2 min (on toy dataset)"
]
},
{
@@ -240,15 +256,6 @@
"### Leafcutter isoforms"
]
},
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "Timing: <2 min\n"
- ]
- },
{
"cell_type": "markdown",
"metadata": {
@@ -281,6 +288,15 @@
"| 22:19149916:19150025:clu_14_-:ENSG00000063515 | ENSG00000063515 |"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: ~2 min (on toy dataset)"
+ ]
+ },
{
"cell_type": "code",
"execution_count": null,
@@ -312,7 +328,7 @@
"kernel": "SoS"
},
"source": [
- "Timing: <2 min\n"
+ "For psichomics splicing quantifications, each event ID ends in `_`; the gene ID is matched against the GTF to obtain coordinates. The output is a coordinate-annotated phenotype BED plus a phenotype-group file."
]
},
{
@@ -321,7 +337,7 @@
"kernel": "SoS"
},
"source": [
- "For psichomics splicing quantifications, each event ID ends in `_`; the gene ID is matched against the GTF to obtain coordinates. The output is a coordinate-annotated phenotype BED plus a phenotype-group file."
+ "**Timing**: ~2 min (on toy dataset)"
]
},
{
@@ -353,7 +369,7 @@
"kernel": "SoS"
},
"source": [
- "Timing: <2 min (depends on network)\n"
+ "Instead of a local GTF, coordinates can be fetched from the Ensembl biomaRt web service. The phenotype matrix must have a `gene_ID` column of ENSEMBL gene IDs. `--ensembl-version` selects the Ensembl release; use a release whose server is reachable from your network (e.g. a current release). This step requires internet access to Ensembl."
]
},
{
@@ -362,7 +378,7 @@
"kernel": "SoS"
},
"source": [
- "Instead of a local GTF, coordinates can be fetched from the Ensembl biomaRt web service. The phenotype matrix must have a `gene_ID` column of ENSEMBL gene IDs. `--ensembl-version` selects the Ensembl release; use a release whose server is reachable from your network (e.g. a current release). This step requires internet access to Ensembl."
+ "**Timing**: ~2 min (on toy dataset)"
]
},
{
@@ -379,6 +395,48 @@
" --ensembl-version 115\n"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "Markdown",
+ "tags": []
+ },
+ "source": [
+ "chr | start | end | strand | gene_id | gene_name\n",
+ "------|---------|---------|----------|----------|--------------\n",
+ "chr1 | 29554 | 30039 | + | ENSG00000243485 | MIR1302-2HG\n",
+ "chr1 | 30564 | 30667 | + | ENSG00000243485 | MIR1302-2HG\n",
+ "chr1 | 30976 | 31097 | + | ENSG00000243485 | MIR1302-2HG\n",
+ "chr1 | 35721 | 36081 | - | ENSG00000237613 | FAM138A\n",
+ "chr1 | 35277 | 35481 | - | ENSG00000237613 | FAM138A\n",
+ "chr1 | 34554 | 35174 | - | ENSG00000237613 | FAM138A\n",
+ "chr1 | 65419 | 65433 | + | ENSG00000186092 | OR4F5\n",
+ "chr1 | 65520 | 65573 | + | ENSG00000186092 | OR4F5\n",
+ "chr1 | 69037 | 71585 | + | ENSG00000186092 | OR4F5"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "phenotype_group\n",
+ "\n",
+ "|X1|X2|\n",
+ "|-------------|---|\n",
+ "| 7:102476270:102478811:clu_309_-:ENSG00000005075 | ENSG00000005075 | \n",
+ "| 7:102476270:102478808:clu_309_-:ENSG00000005075 | ENSG00000005075 |\n",
+ "| X:47572961:47574002:clu_349_-:ENSG00000008056 | ENSG00000008056 |\n",
+ "| X:47572999:47574002:clu_349_-:ENSG00000008056 | ENSG00000008056 |\n",
+ "| 8:27236905:27239971:clu_322_-:ENSG00000015592 | ENSG00000015592 |\n",
+ "| 8:27239279:27239971:clu_322_-:ENSG00000015592 | ENSG00000015592 |\n",
+ "| 8:27241262:27241677:clu_323_-:ENSG00000015592 | ENSG00000015592 |\n",
+ "| 8:27241262:27242397:clu_323_-:ENSG00000015592 | ENSG00000015592 |\n",
+ "| 8:27241757:27242397:clu_323_-:ENSG00000015592 | ENSG00000015592 |\n",
+ "| 1:35558223:35559107:clu_4_+:ENSG00000020129 | ENSG00000020129 |"
+ ]
+ },
{
"cell_type": "markdown",
"metadata": {
@@ -390,98 +448,109 @@
},
{
"cell_type": "code",
- "execution_count": 2,
+ "execution_count": null,
"metadata": {
"kernel": "Bash"
},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "usage: sos run gene_annotation.ipynb\n",
- " [workflow_name | -t targets] [options] [workflow_options]\n",
- " workflow_name: Single or combined workflows defined in this script\n",
- " targets: One or more targets to generate\n",
- " options: Single-hyphen sos parameters (see \"sos run -h\" for details)\n",
- " workflow_options: Double-hyphen workflow-specific parameters\n",
- "\n",
- "Workflows:\n",
- " region_list_generation\n",
- " annotate_coord\n",
- " annotate_coord_biomart\n",
- " map_leafcutter_cluster_to_gene\n",
- " annotate_leafcutter_isoforms\n",
- " annotate_psichomics_isoforms\n",
- "\n",
- "Global Workflow Options:\n",
- " --cwd output (as path)\n",
- " Work directory & output directory\n",
- " --phenoFile VAL (as path, required)\n",
- " Molecular phenotype matrix\n",
- " --job-size 1 (as int)\n",
- " For cluster jobs, number commands to run per job\n",
- " --walltime 5h\n",
- " Wall clock time expected\n",
- " --mem 16G\n",
- " Memory expected\n",
- " --numThreads 1 (as int)\n",
- " Number of threads\n",
- " --container ''\n",
- "\n",
- "Sections\n",
- " region_list_generation:\n",
- " annotate_coord:\n",
- " Workflow Options:\n",
- " --sample-participant-lookup . (as path)\n",
- " A file to map sample ID from expression to genotype,\n",
- " must contain two columns, sample_id and participant_id,\n",
- " mapping IDs in the expression files to IDs in the\n",
- " genotype (these can be the same).\n",
- " --molecular-trait-type gene\n",
- " Options: gene, protein, atac\n",
- " --coordinate-annotation VAL (as path, required)\n",
- " gtf annotation for RNA-seq data; other types of\n",
- " annotation index for protein and atac-seq\n",
- " --phenotype-id-column 'gene_id'\n",
- " gene_id or gene_name for RNA-seq data, SOMAseqID for\n",
- " proteins for example\n",
- " --auxiliary-id-mapping . (as path)\n",
- " Optional mapping file (protein_name_index for protein,\n",
- " etc.)\n",
- " --[no-]strip-id (default to False)\n",
- " Optional processing flag\n",
- " annotate_coord_biomart:\n",
- " Workflow Options:\n",
- " --ensembl-version VAL (as int, required)\n",
- " map_leafcutter_cluster_to_gene:\n",
- " Workflow Options:\n",
- " --intron-count VAL (as path, required)\n",
- " Extract the code in case psichromatic needs to be\n",
- " processed the same way PheoFile in this step is the\n",
- " intron_count file\n",
- " --map-stra site\n",
- " Defines the mapping strategy options: 'site' or\n",
- " 'region', with 'site' as the default. The 'site'\n",
- " strategy maps introns to the start and end of each exon.\n",
- " The 'region' strategy, to be recommended in leafcutter2,\n",
- " maps each intron based on it overlapping more than\n",
- " overlap_ratio of a gene's region.\n",
- " --overlap-ratio 0.8 (as float)\n",
- " Define the overlap ratio as the proportion of the\n",
- " cluster length that intersects with a gene, used to\n",
- " determine mapping to the gene.\n",
- " annotate_leafcutter_isoforms:\n",
- " Workflow Options:\n",
- " --sample-participant-lookup . (as path)\n",
- " annotate_psichomics_isoforms:\n",
- " Workflow Options:\n",
- " --sample-participant-lookup . (as path)\n"
- ]
- }
- ],
+ "outputs": [],
+ "source": [
+ "sos run pipeline/gene_annotation.ipynb -h"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
"source": [
- "sos run gene_annotation.ipynb -h"
+ "```\n",
+ "usage: sos run pipeline/gene_annotation.ipynb\n",
+ " [workflow_name | -t targets] [options] [workflow_options]\n",
+ " workflow_name: Single or combined workflows defined in this script\n",
+ " targets: One or more targets to generate\n",
+ " options: Single-hyphen sos parameters (see \"sos run -h\" for details)\n",
+ " workflow_options: Double-hyphen workflow-specific parameters\n",
+ "\n",
+ "Workflows:\n",
+ " annotate_coord\n",
+ " annotate_coord_biomart\n",
+ " map_leafcutter_cluster_to_gene\n",
+ " annotate_leafcutter_isoforms\n",
+ " annotate_psichomics_isoforms\n",
+ "\n",
+ "Global Workflow Options:\n",
+ " --modular-script-dir code/script (as path)\n",
+ " --cwd output (as path)\n",
+ " Work directory & output directory\n",
+ " --phenoFile VAL (as path, required)\n",
+ " Molecular phenotype matrix\n",
+ " --job-size 1 (as int)\n",
+ " For cluster jobs, number commands to run per job\n",
+ " --walltime 5h\n",
+ " Wall clock time expected\n",
+ " --mem 16G\n",
+ " Memory expected\n",
+ " --numThreads 1 (as int)\n",
+ " Number of threads\n",
+ " --container ''\n",
+ " --entrypoint ''\n",
+ "\n",
+ "Sections\n",
+ " annotate_coord:\n",
+ " Workflow Options:\n",
+ " --sample-participant-lookup . (as path)\n",
+ " A file to map sample ID from expression to genotype,\n",
+ " must contain two columns, sample_id and participant_id,\n",
+ " mapping IDs in the expression files to IDs in the\n",
+ " genotype (these can be the same).\n",
+ " --molecular-trait-type gene\n",
+ " Options: gene, protein, atac\n",
+ " --coordinate-annotation VAL (as path, required)\n",
+ " gtf annotation for RNA-seq data; other types of\n",
+ " annotation index for protein and atac-seq\n",
+ " --phenotype-id-column 'gene_id'\n",
+ " gene_id or gene_name for RNA-seq data, SOMAseqID for\n",
+ " proteins for example\n",
+ " --auxiliary-id-mapping . (as path)\n",
+ " Optional mapping file (protein_name_index for protein,\n",
+ " etc.)\n",
+ " --[no-]strip-id (default to False)\n",
+ " Optional processing flag\n",
+ " --sep '\\t'\n",
+ " annotate_coord_biomart:\n",
+ " Workflow Options:\n",
+ " --ensembl-version VAL (as int, required)\n",
+ " map_leafcutter_cluster_to_gene:\n",
+ " Workflow Options:\n",
+ " --intron-count VAL (as path, required)\n",
+ " Extract the code in case psichromatic needs to be\n",
+ " processed the same way PheoFile in this step is the\n",
+ " intron_count file\n",
+ " --coordinate-annotation VAL (as path, required)\n",
+ " gtf annotation with exons (the full gene model), used to\n",
+ " map intron splice sites to genes\n",
+ " --map-stra site\n",
+ " Defines the mapping strategy options: 'site' or\n",
+ " 'region', with 'site' as the default. The 'site'\n",
+ " strategy maps introns to the start and end of each exon.\n",
+ " The 'region' strategy, to be recommended in leafcutter2,\n",
+ " maps each intron based on it overlapping more than\n",
+ " overlap_ratio of a gene's region.\n",
+ " --overlap-ratio 0.8 (as float)\n",
+ " Define the overlap ratio as the proportion of the\n",
+ " cluster length that intersects with a gene, used to\n",
+ " determine mapping to the gene.\n",
+ " annotate_leafcutter_isoforms:\n",
+ " Workflow Options:\n",
+ " --sample-participant-lookup . (as path)\n",
+ " --coordinate-annotation VAL (as path, required)\n",
+ " gtf annotation with exons (the full gene model)\n",
+ " annotate_psichomics_isoforms:\n",
+ " Workflow Options:\n",
+ " --sample-participant-lookup . (as path)\n",
+ " --coordinate-annotation VAL (as path, required)\n",
+ " gtf annotation with exons (the full gene model)\n",
+ "```"
]
},
{
@@ -490,7 +559,7 @@
"kernel": "Bash"
},
"source": [
- "## Setup and global parameters"
+ "## Workflow implementation"
]
},
{
@@ -525,7 +594,7 @@
"kernel": "SoS"
},
"source": [
- "## Implementation using `pyqtl`\n",
+ "### Implementation using `pyqtl`\n",
"\n",
"Implementation based on [GTEx pipeline](https://github.com/broadinstitute/gtex-pipeline/blob/master/qtl/src/eqtl_prepare_expression.py).\n",
"\n",
@@ -539,7 +608,40 @@
"kernel": "SoS"
},
"outputs": [],
- "source": "[annotate_coord]\n# A file to map sample ID from expression to genotype, must contain two columns, sample_id and participant_id, mapping IDs in the expression files to IDs in the genotype (these can be the same).\nparameter: sample_participant_lookup = path()\n# Options: gene, protein, atac\nparameter: molecular_trait_type = \"gene\" \n# gtf annotation for RNA-seq data; other types of annotation index for protein and atac-seq\nparameter: coordinate_annotation = path\n# gene_id or gene_name for RNA-seq data, SOMAseqID for proteins for example\nparameter: phenotype_id_column = \"gene_id\"\n# Optional mapping file (protein_name_index for protein, etc.)\nparameter: auxiliary_id_mapping = path()\n# Optional processing flag\nparameter: strip_id = False\nparameter: sep = \"\\t\"\n\ninput: phenoFile, coordinate_annotation\noutput: f'{cwd:a}/{_input[0]:bn}.bed.gz', f'{cwd:a}/{_input[0]:bn}.region_list.txt'\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, tags = f'{step_name}_{_output[0]:bn}'\nbash: expand= \"${ }\", stderr = f'{_output[0]:n}.stderr', stdout = f'{_output[0]:n}.stdout', container = container, entrypoint = entrypoint\n Rscript ${modular_script_dir}/data_preprocessing/phenotype/gene_annotation.R \\\n --step annotate_coord \\\n --phenoFile \"${_input[0]}\" \\\n --coordinate-annotation \"${_input[1]}\" \\\n --sample-participant-lookup \"${sample_participant_lookup}\" \\\n --molecular-trait-type \"${molecular_trait_type}\" \\\n --phenotype-id-column \"${phenotype_id_column}\" \\\n --auxiliary-id-mapping \"${auxiliary_id_mapping}\" \\\n --sep \"${sep}\" \\\n ${\"--strip-id\" if strip_id else \"\"} \\\n --output-bed \"${_output[0]}\" \\\n --output-region-list \"${_output[1]}\" \\\n --gene-list-output \"${cwd:a}/${_input[0]:bn}.gene_list.tsv\""
+ "source": [
+ "[annotate_coord]\n",
+ "# A file to map sample ID from expression to genotype, must contain two columns, sample_id and participant_id, mapping IDs in the expression files to IDs in the genotype (these can be the same).\n",
+ "parameter: sample_participant_lookup = path()\n",
+ "# Options: gene, protein, atac\n",
+ "parameter: molecular_trait_type = \"gene\" \n",
+ "# gtf annotation for RNA-seq data; other types of annotation index for protein and atac-seq\n",
+ "parameter: coordinate_annotation = path\n",
+ "# gene_id or gene_name for RNA-seq data, SOMAseqID for proteins for example\n",
+ "parameter: phenotype_id_column = \"gene_id\"\n",
+ "# Optional mapping file (protein_name_index for protein, etc.)\n",
+ "parameter: auxiliary_id_mapping = path()\n",
+ "# Optional processing flag\n",
+ "parameter: strip_id = False\n",
+ "parameter: sep = \"\\t\"\n",
+ "\n",
+ "input: phenoFile, coordinate_annotation\n",
+ "output: f'{cwd:a}/{_input[0]:bn}.bed.gz', f'{cwd:a}/{_input[0]:bn}.region_list.txt'\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, tags = f'{step_name}_{_output[0]:bn}'\n",
+ "bash: expand= \"${ }\", stderr = f'{_output[0]:n}.stderr', stdout = f'{_output[0]:n}.stdout', container = container, entrypoint = entrypoint\n",
+ " Rscript ${modular_script_dir}/data_preprocessing/phenotype/gene_annotation.R \\\n",
+ " --step annotate_coord \\\n",
+ " --phenoFile \"${_input[0]}\" \\\n",
+ " --coordinate-annotation \"${_input[1]}\" \\\n",
+ " --sample-participant-lookup \"${sample_participant_lookup}\" \\\n",
+ " --molecular-trait-type \"${molecular_trait_type}\" \\\n",
+ " --phenotype-id-column \"${phenotype_id_column}\" \\\n",
+ " --auxiliary-id-mapping \"${auxiliary_id_mapping}\" \\\n",
+ " --sep \"${sep}\" \\\n",
+ " ${\"--strip-id\" if strip_id else \"\"} \\\n",
+ " --output-bed \"${_output[0]}\" \\\n",
+ " --output-region-list \"${_output[1]}\" \\\n",
+ " --gene-list-output \"${cwd:a}/${_input[0]:bn}.gene_list.tsv\""
+ ]
},
{
"cell_type": "markdown",
@@ -548,7 +650,7 @@
"tags": []
},
"source": [
- "## Implementation using biomaRt\n",
+ "### Implementation using biomaRt\n",
"This workflow adds the annotations of chr pos(TSS where start = end -1) and gene_ID to the `bed` file. **This workflow is obsolete**."
]
},
@@ -581,7 +683,7 @@
"kernel": "SoS"
},
"source": [
- "## Annotation of leafcutter isoform"
+ "### Annotation of leafcutter isoform"
]
},
{
@@ -589,7 +691,9 @@
"metadata": {
"kernel": "SoS"
},
- "source": "The gtf used here should be the collapsed gtf, i.e. the final output of reference_data gtf processing and the one used to called rnaseq."
+ "source": [
+ "The gtf used here should be the collapsed gtf, i.e. the final output of reference_data gtf processing and the one used to called rnaseq."
+ ]
},
{
"cell_type": "code",
@@ -598,7 +702,32 @@
"kernel": "SoS"
},
"outputs": [],
- "source": "[map_leafcutter_cluster_to_gene]\n## Extract the code in case psichromatic needs to be processed the same way\n## PheoFile in this step is the intron_count file\nparameter: intron_count = path\n# gtf annotation with exons (the full gene model), used to map intron splice sites to genes\nparameter: coordinate_annotation = path\n# Defines the mapping strategy options: 'site' or 'region', with 'site' as the default. \n# The 'site' strategy maps introns to the start and end of each exon. \n# The 'region' strategy, to be recommended in leafcutter2, maps each intron based on it overlapping more than overlap_ratio of a gene's region.\nparameter: map_stra = \"site\" \n# Define the overlap ratio as the proportion of the cluster length that intersects with a gene, used to determine mapping to the gene.\nparameter: overlap_ratio = 0.8\ninput: intron_count, coordinate_annotation\noutput: f'{cwd}/{_input[0]:b}.exon_list', f'{cwd}/{_input[0]:b}.leafcutter.clusters_to_genes.txt'\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, tags = f'{step_name}_{_output[0]:bn}' \nbash: expand= \"${ }\", stderr = f'{_output[0]:n}.stderr', stdout = f'{_output[0]:n}.stdout', container = container, entrypoint = entrypoint\n Rscript ${modular_script_dir}/data_preprocessing/phenotype/gene_annotation.R \\\n --step map_leafcutter_cluster_to_gene \\\n --intron-count \"${_input[0]}\" \\\n --annotation-gtf \"${_input[1]}\" \\\n --map-stra \"${map_stra}\" \\\n --overlap-ratio ${overlap_ratio} \\\n --output-exon-list \"${_output[0]}\" \\\n --output-cluster-map \"${_output[1]}\""
+ "source": [
+ "[map_leafcutter_cluster_to_gene]\n",
+ "## Extract the code in case psichromatic needs to be processed the same way\n",
+ "## PheoFile in this step is the intron_count file\n",
+ "parameter: intron_count = path\n",
+ "# gtf annotation with exons (the full gene model), used to map intron splice sites to genes\n",
+ "parameter: coordinate_annotation = path\n",
+ "# Defines the mapping strategy options: 'site' or 'region', with 'site' as the default. \n",
+ "# The 'site' strategy maps introns to the start and end of each exon. \n",
+ "# The 'region' strategy, to be recommended in leafcutter2, maps each intron based on it overlapping more than overlap_ratio of a gene's region.\n",
+ "parameter: map_stra = \"site\" \n",
+ "# Define the overlap ratio as the proportion of the cluster length that intersects with a gene, used to determine mapping to the gene.\n",
+ "parameter: overlap_ratio = 0.8\n",
+ "input: intron_count, coordinate_annotation\n",
+ "output: f'{cwd}/{_input[0]:b}.exon_list', f'{cwd}/{_input[0]:b}.leafcutter.clusters_to_genes.txt'\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, tags = f'{step_name}_{_output[0]:bn}' \n",
+ "bash: expand= \"${ }\", stderr = f'{_output[0]:n}.stderr', stdout = f'{_output[0]:n}.stdout', container = container, entrypoint = entrypoint\n",
+ " Rscript ${modular_script_dir}/data_preprocessing/phenotype/gene_annotation.R \\\n",
+ " --step map_leafcutter_cluster_to_gene \\\n",
+ " --intron-count \"${_input[0]}\" \\\n",
+ " --annotation-gtf \"${_input[1]}\" \\\n",
+ " --map-stra \"${map_stra}\" \\\n",
+ " --overlap-ratio ${overlap_ratio} \\\n",
+ " --output-exon-list \"${_output[0]}\" \\\n",
+ " --output-cluster-map \"${_output[1]}\""
+ ]
},
{
"cell_type": "code",
@@ -607,16 +736,23 @@
"kernel": "SoS"
},
"outputs": [],
- "source": "[annotate_leafcutter_isoforms]\nparameter: sample_participant_lookup = path()\n# gtf annotation with exons (the full gene model)\nparameter: coordinate_annotation = path\ninput: phenoFile, coordinate_annotation, output_from(\"map_leafcutter_cluster_to_gene\")\noutput: f'{cwd:a}/{_input[0]:bn}.formated.bed.gz', f'{cwd:a}/{_input[0]:bn}.phenotype_group.txt'\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, tags = f'{step_name}_{_output[0]:bn}' \nbash: expand= \"${ }\", stderr = f'{_output[0]:n}.stderr', stdout = f'{_output[0]:n}.stdout', container = container, entrypoint = entrypoint\n Rscript ${modular_script_dir}/data_preprocessing/phenotype/gene_annotation.R \\\n --step annotate_leafcutter_isoforms \\\n --phenoFile \"${_input[0]}\" \\\n --annotation-gtf \"${_input[1]}\" \\\n --cluster-map \"${_input[3]}\" \\\n --sample-participant-lookup \"${sample_participant_lookup}\" \\\n --output-bed \"${_output[0]}\" \\\n --output-phenotype-group \"${_output[1]}\""
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
"source": [
- "## Processing of psichomics output\n",
- "It occurs that the psichomatic by default grouped the isoforms by gene name, so only thing needs to be done is to extract this information and potentially renamed the gene symbol into ENSG ID"
+ "[annotate_leafcutter_isoforms]\n",
+ "parameter: sample_participant_lookup = path()\n",
+ "# gtf annotation with exons (the full gene model)\n",
+ "parameter: coordinate_annotation = path\n",
+ "input: phenoFile, coordinate_annotation, output_from(\"map_leafcutter_cluster_to_gene\")\n",
+ "output: f'{cwd:a}/{_input[0]:bn}.formated.bed.gz', f'{cwd:a}/{_input[0]:bn}.phenotype_group.txt'\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, tags = f'{step_name}_{_output[0]:bn}' \n",
+ "bash: expand= \"${ }\", stderr = f'{_output[0]:n}.stderr', stdout = f'{_output[0]:n}.stdout', container = container, entrypoint = entrypoint\n",
+ " Rscript ${modular_script_dir}/data_preprocessing/phenotype/gene_annotation.R \\\n",
+ " --step annotate_leafcutter_isoforms \\\n",
+ " --phenoFile \"${_input[0]}\" \\\n",
+ " --annotation-gtf \"${_input[1]}\" \\\n",
+ " --cluster-map \"${_input[3]}\" \\\n",
+ " --sample-participant-lookup \"${sample_participant_lookup}\" \\\n",
+ " --output-bed \"${_output[0]}\" \\\n",
+ " --output-phenotype-group \"${_output[1]}\""
]
},
{
@@ -625,9 +761,8 @@
"kernel": "SoS"
},
"source": [
- "## Anticipated Results\n",
- "\n",
- "The pipeline produces output files in the `output/` subdirectory named after the workflow step. Verify success by checking that output files exist and are non-empty. See the **Output** section above for the expected file names and formats."
+ "### Processing of psichomics output\n",
+ "It occurs that the psichomatic by default grouped the isoforms by gene name, so only thing needs to be done is to extract this information and potentially renamed the gene symbol into ENSG ID"
]
},
{
@@ -637,7 +772,23 @@
"kernel": "SoS"
},
"outputs": [],
- "source": "[annotate_psichomics_isoforms]\nparameter: sample_participant_lookup = path()\n# gtf annotation with exons (the full gene model)\nparameter: coordinate_annotation = path\ninput: phenoFile, coordinate_annotation\noutput: f'{cwd:a}/{_input[0]:bn}.formated.bed.gz', f'{cwd:a}/{_input[0]:bn}.phenotype_group.txt'\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, tags = f'{step_name}_{_output[0]:bn}' \nbash: expand= \"${ }\", stderr = f'{_output[0]:n}.stderr', stdout = f'{_output[0]:n}.stdout', container = container, entrypoint = entrypoint\n Rscript ${modular_script_dir}/data_preprocessing/phenotype/gene_annotation.R \\\n --step annotate_psichomics_isoforms \\\n --phenoFile \"${_input[0]}\" \\\n --annotation-gtf \"${_input[1]}\" \\\n --sample-participant-lookup \"${sample_participant_lookup}\" \\\n --output-bed \"${_output[0]}\" \\\n --output-phenotype-group \"${_output[1]}\""
+ "source": [
+ "[annotate_psichomics_isoforms]\n",
+ "parameter: sample_participant_lookup = path()\n",
+ "# gtf annotation with exons (the full gene model)\n",
+ "parameter: coordinate_annotation = path\n",
+ "input: phenoFile, coordinate_annotation\n",
+ "output: f'{cwd:a}/{_input[0]:bn}.formated.bed.gz', f'{cwd:a}/{_input[0]:bn}.phenotype_group.txt'\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, tags = f'{step_name}_{_output[0]:bn}' \n",
+ "bash: expand= \"${ }\", stderr = f'{_output[0]:n}.stderr', stdout = f'{_output[0]:n}.stdout', container = container, entrypoint = entrypoint\n",
+ " Rscript ${modular_script_dir}/data_preprocessing/phenotype/gene_annotation.R \\\n",
+ " --step annotate_psichomics_isoforms \\\n",
+ " --phenoFile \"${_input[0]}\" \\\n",
+ " --annotation-gtf \"${_input[1]}\" \\\n",
+ " --sample-participant-lookup \"${sample_participant_lookup}\" \\\n",
+ " --output-bed \"${_output[0]}\" \\\n",
+ " --output-phenotype-group \"${_output[1]}\""
+ ]
}
],
"metadata": {
@@ -676,4 +827,4 @@
},
"nbformat": 4,
"nbformat_minor": 4
-}
\ No newline at end of file
+}
diff --git a/code/SoS/data_preprocessing/phenotype/phenotype_formatting.ipynb b/code/SoS/data_preprocessing/phenotype/phenotype_formatting.ipynb
index 59bdffa41..236d7a610 100644
--- a/code/SoS/data_preprocessing/phenotype/phenotype_formatting.ipynb
+++ b/code/SoS/data_preprocessing/phenotype/phenotype_formatting.ipynb
@@ -9,7 +9,7 @@
"source": [
"# Phenotype Data Formatting\n",
"\n",
- "**FIXME: this entire pipeline needs to be improved**"
+ "Splits a molecular phenotype matrix by chromosome or by region so the association scan can be parallelised."
]
},
{
@@ -18,7 +18,13 @@
"kernel": "SoS"
},
"source": [
- "## Description"
+ "## Overview\n",
+ "\n",
+ "Association testing and fine-mapping run per chromosome or per region, so the phenotype matrix has to be partitioned the same way before that work can be parallelised. These workflows split a phenotype BED by chromosome or by region, annotate features against TAD boundaries when region-based analysis is wanted, and accept GCT-format input as well as BED. Two further workflows trim inputs rather than split them: one subsets BAM files by coordinate, the other drops samples from a GCT matrix.\n",
+ "\n",
+ "The pipeline's author has flagged it as needing improvement, so treat the interface as unstable and re-check `-h` before relying on any option.\n",
+ "\n",
+ "**When to run it.** After phenotype QC, normalisation and imputation, and before covariate preprocessing and the association scan."
]
},
{
@@ -27,7 +33,36 @@
"kernel": "SoS"
},
"source": [
- "We include a collection of workflows to format molecular phenotype data. These include workflows to separate phenotypes by chromosome, by user-provided regions, a workflow to subset bam files and a workflow to extract samples from phenotype files.\n"
+ "## Input\n",
+ "\n",
+ "- `--phenoFile`: the molecular phenotype data, required by every workflow. A tabix-indexed UCSC `bed.gz` for the BED workflows, a `gct.gz` for the GCT ones, and a list of BAM paths for `bam_subsetting`. Example `output/rnaseq/protocol_example.rnaseq.bed.bed.gz`:\n",
+ "\n",
+ " ```\n",
+ " #chr start end ID SAMPLE_001 SAMPLE_002\n",
+ " chr22 10939387 10961337 ENSG00000283047 37.0768403309204 0.0\n",
+ " ```\n",
+ "\n",
+ "- `--chrom`: the chromosomes to extract, taking one or more values. Used by `phenotype_by_chrom` and `phenotype_by_chrom_gct`.\n",
+ "- `--region-list`: a 4-column index of the regions to analyse, giving chr, start, end and region name. Used by `phenotype_by_region`. Example `input/reference_data/TAD/protocol_example_protein.enhanced_cis_chr22.bed`:\n",
+ "\n",
+ " ```\n",
+ " #chr start end gene_id\n",
+ " chr22 0 18960000 ENSG00000131100_P36543\n",
+ " ```\n",
+ "\n",
+ "- `--TAD-list`: the TAD boundary file for `phenotype_annotate_by_tad`, in the same 4-column shape. Example `input/reference_data/TAD/TADB_enhanced_cis.bed`.\n",
+ "- `--phenotype-per-tad`: the minimum number of features a TAD must contain to be kept, `2` by default.\n",
+ "- `--region`: coordinates passed through to `samtools view` by `bam_subsetting`, for example `--region chr21 chr22`.\n",
+ "- `--keep-samples`: for `gct_extract_samples`, a file listing the sample IDs to retain, one per line. Example `input/colocboost/keep_samples.txt`:\n",
+ "\n",
+ " ```\n",
+ " SAMPLE_001\n",
+ " SAMPLE_002\n",
+ " SAMPLE_003\n",
+ " ```\n",
+ "\n",
+ "- `--name`: the stem of the output files, derived from `--phenoFile` by default.\n",
+ "- `--cwd`: the directory outputs are written to."
]
},
{
@@ -36,12 +71,21 @@
"kernel": "SoS"
},
"source": [
- "## Input Files\n",
+ "## Output\n",
+ "\n",
+ "- `{cwd}/{name}.{chrom}.bed.gz` - one phenotype BED per requested chromosome, with a tabix index, from `phenotype_by_chrom`.\n",
+ "- `{cwd}/{name}.phenotype_by_chrom_files.txt` and `{cwd}/{name}.phenotype_by_chrom_files.region_list.txt` - the manifest of those per-chromosome files and the matching region list, both written by the collecting substep. Example `output/phenotype_protein/protocol_example_protein.phenotype_by_chrom_files.region_list.txt`:\n",
+ "\n",
+ " ```\n",
+ " #chr start end ID path\n",
+ " chr22 17592135 17628748 ENSG00000131100_P36543 output/phenotype_protein/protocol_example_protein.chr22.bed.gz\n",
+ " ```\n",
"\n",
- "| File | Description |\n",
- "|------|-------------|\n",
- "| `output/rnaseq/protocol_example.rnaseq.bed.bed.gz` | Residualized molecular phenotype data as a bgzipped, tabix-indexed BED (`#chr`/`start`/`end`/`ID` + one column per sample). Output of upstream `covariate_preprocessing` / `gene_annotation`. |\n",
- "| `output/rnaseq/protocol_example.rnaseq.bed.bed.gz.tbi` | Tabix index for the phenotype BED, required to extract records per chromosome. |"
+ "- `{cwd}/{region_list}_phenotype_by_region/{name}.{region}.bed.gz` and `{cwd}/{name}.phenotype_by_region_files.txt` - the per-region equivalents from `phenotype_by_region`.\n",
+ "- `{cwd}/{phenoFile}.{TAD_list}.{n}_pheno_per_region.region_list` - the TAD-grouped region list from `phenotype_annotate_by_tad`.\n",
+ "- `{cwd}/{name}.{chrom}.gct` - one GCT per chromosome, from `phenotype_by_chrom_gct`.\n",
+ "- `{cwd}/{bam}.subsetted.bam` - the coordinate-restricted BAM from `bam_subsetting`.\n",
+ "- `{phenoFile}.sample_matched.gct.gz` - the sample-filtered GCT from `gct_extract_samples`, written beside its input rather than under `--cwd`."
]
},
{
@@ -50,13 +94,46 @@
"kernel": "SoS"
},
"source": [
- "## Output Files\n",
+ "## Minimal Working Example"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### Partition a phenotype BED by chromosome\n",
"\n",
- "| File | Description |\n",
- "|------|-------------|\n",
- "| `output/phenotype_uf/protocol_example..bed.gz` (+`.tbi`) | Per-chromosome phenotype BED (bed+index) annotated with genomic coordinates, suitable for TensorQTL analysis. |\n",
- "| `output/phenotype_uf/protocol_example.phenotype_by_chrom_files.txt` | Two-column list mapping each chromosome to its per-chromosome phenotype BED. |\n",
- "| `output/phenotype_uf/protocol_example.phenotype_by_chrom_files.region_list.txt` | Region list (`#chr`/`start`/`end`/`ID`/`path`) for downstream cis-QTL / fine-mapping. |"
+ "`phenotype_by_chrom` runs two substeps in sequence: `_1` pulls the records for each requested chromosome out of the tabix-indexed BED and writes a per-chromosome `bed.gz` with its index, and `_2` collects those into the `*_files.txt` and `*.region_list.txt` lists. This is the partitioning cis TensorQTL analysis needs. Only `chr22` is present in the toy phenotype data."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS",
+ "tags": []
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "kernel": "Bash",
+ "vscode": {
+ "languageId": "plaintext"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/phenotype_formatting.ipynb phenotype_by_chrom \\\n",
+ " --cwd output/phenotype_uf \\\n",
+ " --phenoFile output/rnaseq/protocol_example.rnaseq.bed.bed.gz \\\n",
+ " --name protocol_example \\\n",
+ " --chrom chr22"
]
},
{
@@ -65,8 +142,9 @@
"kernel": "SoS"
},
"source": [
- "## Minimal Working Example Steps\n",
- "The data and singularity image used are available on [Synapse](https://www.synapse.org/#!Synapse:syn69670658).\n"
+ "### Partition a GCT matrix by chromosome\n",
+ "\n",
+ "`phenotype_by_chrom_gct` does the same split for RNASeQC-style GCT input rather than BED, writing one `.gct` per chromosome."
]
},
{
@@ -75,7 +153,21 @@
"kernel": "SoS"
},
"source": [
- "### **Step 1.** Partition phenotype by chromosome"
+ "**Timing**: TBD (on toy dataset)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/phenotype_formatting.ipynb phenotype_by_chrom_gct \\\n",
+ " --cwd output/phenotype_gct \\\n",
+ " --phenoFile input/rnaseq/protocol_example.rnaseq.gene_tpm.gct.gz \\\n",
+ " --chrom chr21 chr22"
]
},
{
@@ -84,7 +176,9 @@
"kernel": "SoS"
},
"source": [
- "This is necessary for cis TensorQTL analysis."
+ "### Partition a phenotype BED by region\n",
+ "\n",
+ "`phenotype_by_region` splits on an explicit region list instead of whole chromosomes, giving one `bed.gz` per region under a directory named after the list. Like `phenotype_by_chrom` it runs two substeps in sequence: `_1` writes the per-region files and `_2` collects them into a `_files.txt` manifest."
]
},
{
@@ -93,25 +187,21 @@
"kernel": "SoS"
},
"source": [
- "Timing < 1 min"
+ "**Timing**: TBD (on toy dataset)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
- "kernel": "Bash",
- "vscode": {
- "languageId": "plaintext"
- }
+ "kernel": "SoS"
},
"outputs": [],
"source": [
- "sos run pipeline/phenotype_formatting.ipynb phenotype_by_chrom \\\n",
- " --cwd output/phenotype_uf \\\n",
+ "sos run pipeline/phenotype_formatting.ipynb phenotype_by_region \\\n",
+ " --cwd output/phenotype_by_region \\\n",
" --phenoFile output/rnaseq/protocol_example.rnaseq.bed.bed.gz \\\n",
- " --name protocol_example \\\n",
- " --chrom chr22"
+ " --region-list input/reference_data/TAD/protocol_example_protein.enhanced_cis_chr22.bed"
]
},
{
@@ -120,7 +210,9 @@
"kernel": "SoS"
},
"source": [
- "This runs the two `phenotype_by_chrom` substeps in sequence: substep `_1` extracts the records for each requested chromosome from the tabix-indexed BED and writes a per-chromosome `bed.gz` (+ index); substep `_2` collects those into the `*_files.txt` and `*.region_list.txt` lists. For this minimal example only `chr22` is present in the toy phenotype data."
+ "### Group features into TAD windows\n",
+ "\n",
+ "`phenotype_annotate_by_tad` assigns each feature to a TAD and emits a region list, so that region-based analysis works on TAD windows rather than fixed cis-windows. `--phenotype-per-tad` sets the minimum features a TAD must hold to be kept."
]
},
{
@@ -129,7 +221,22 @@
"kernel": "SoS"
},
"source": [
- "## Troubleshooting"
+ "**Timing**: TBD (on toy dataset)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/phenotype_formatting.ipynb phenotype_annotate_by_tad \\\n",
+ " --cwd output/phenotype_by_region \\\n",
+ " --phenoFile output/rnaseq/protocol_example.rnaseq.bed.bed.gz \\\n",
+ " --TAD-list input/reference_data/TAD/TADB_enhanced_cis.bed \\\n",
+ " --phenotype-per-tad 2"
]
},
{
@@ -138,11 +245,66 @@
"kernel": "SoS"
},
"source": [
- "| Step | Substep | Problem | Possible Reason | Solution |\n",
- "|------|---------|---------|------------------|---------|\n",
- "| | | | | |\n",
+ "### Extract samples from a GCT matrix\n",
"\n",
- "\n"
+ "`gct_extract_samples` keeps only the samples named in `--keep-samples`, which is how a GCT from RNASeQC is cut down to the cohort actually being analysed."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/phenotype_formatting.ipynb gct_extract_samples \\\n",
+ " --cwd output/phenotype_gct \\\n",
+ " --phenoFile input/rnaseq/protocol_example.rnaseq.gene_tpm.gct.gz \\\n",
+ " --keep-samples input/colocboost/keep_samples.txt"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### Subset BAM files by region\n",
+ "\n",
+ "`bam_subsetting` runs `samtools view` over each BAM in `--phenoFile` and keeps only the given coordinates. No BAM ships with this repository, so the command below is a template rather than a runnable example."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/phenotype_formatting.ipynb bam_subsetting \\\n",
+ " --cwd output/bam_subset \\\n",
+ " --phenoFile /bam_file_list.txt \\\n",
+ " --region chr21 chr22"
]
},
{
@@ -158,14 +320,86 @@
"cell_type": "code",
"execution_count": null,
"metadata": {
- "kernel": "Bash",
- "vscode": {
- "languageId": "plaintext"
- }
+ "kernel": "SoS"
},
"outputs": [],
"source": [
- "!sos run phenotype_formatting.ipynb -h"
+ "sos run pipeline/phenotype_formatting.ipynb -h"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "```\n",
+ "usage: sos run pipeline/phenotype_formatting.ipynb\n",
+ " [workflow_name | -t targets] [options] [workflow_options]\n",
+ " workflow_name: Single or combined workflows defined in this script\n",
+ " targets: One or more targets to generate\n",
+ " options: Single-hyphen sos parameters (see \"sos run -h\" for details)\n",
+ " workflow_options: Double-hyphen workflow-specific parameters\n",
+ "\n",
+ "Workflows:\n",
+ " phenotype_by_chrom\n",
+ " phenotype_annotate_by_tad\n",
+ " phenotype_by_chrom_gct\n",
+ " phenotype_by_region\n",
+ " bam_subsetting\n",
+ " gct_extract_samples\n",
+ "\n",
+ "Global Workflow Options:\n",
+ " --modular-script-dir code/script (as path)\n",
+ " --cwd output (as path)\n",
+ " Work directory & output directory\n",
+ " --container ''\n",
+ " The filename namefor output data\n",
+ " --entrypoint ''\n",
+ " --job-size 1 (as int)\n",
+ " For cluster jobs, number commands to run per job\n",
+ " --walltime 5h\n",
+ " Wall clock time expected\n",
+ " --mem 16G\n",
+ " Memory expected\n",
+ " --numThreads 20 (as int)\n",
+ " Number of threads\n",
+ " --phenoFile VAL (as path, required)\n",
+ " Path to the input molecular phenotype data.\n",
+ " --name str(phenoFile).removesuffix('.gz').removesuffix('.bed')\n",
+ "\n",
+ " name for the analysis output\n",
+ "\n",
+ "Sections\n",
+ " phenotype_by_chrom_1:\n",
+ " Workflow Options:\n",
+ " --chrom VAL VAL ... (as type, required)\n",
+ " list of chroms to extract\n",
+ " phenotype_by_chrom_2:\n",
+ " phenotype_annotate_by_tad:\n",
+ " Workflow Options:\n",
+ " --TAD-list VAL (as path, required)\n",
+ " --phenotype-per-tad 2 (as int)\n",
+ " phenotype_by_chrom_gct_1:\n",
+ " Workflow Options:\n",
+ " --chrom VAL VAL ... (as type, required)\n",
+ " list of chroms to extract\n",
+ " phenotype_by_region_1:\n",
+ " Workflow Options:\n",
+ " --region-list VAL (as path, required)\n",
+ " An index text file with 4 columns specifying the chr,\n",
+ " start, end and name of regions to analyze\n",
+ " phenotype_by_region_2:\n",
+ " bam_subsetting:\n",
+ " Workflow Options:\n",
+ " --region VAL VAL ... (as type, required)\n",
+ " Input to `samtools view` coordinates, for example,\n",
+ " --region chr21 chr22\n",
+ " gct_extract_samples: Extract samples from expression data generated by\n",
+ " RNASeQC\n",
+ " Workflow Options:\n",
+ " --keep-samples VAL (as path, required)\n",
+ "```"
]
},
{
@@ -174,7 +408,7 @@
"kernel": "SoS"
},
"source": [
- "## Setup and global parameters"
+ "## Workflow implementation"
]
},
{
@@ -208,28 +442,6 @@
"parameter: name = str(phenoFile).removesuffix('.gz').removesuffix('.bed')"
]
},
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS",
- "tags": []
- },
- "source": [
- "## Process of molecular phenotype file\n",
- "This workflow produce a bed+tabix file for all the molecular pheno data that are included in the region list to feed into downstream analysis"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "## Anticipated Results\n",
- "\n",
- "The pipeline produces output files in the `output/` subdirectory named after the workflow step. Verify success by checking that output files exist and are non-empty. See the **Output** section above for the expected file names and formats."
- ]
- },
{
"cell_type": "code",
"execution_count": null,
@@ -237,7 +449,24 @@
"kernel": "SoS"
},
"outputs": [],
- "source": "[phenotype_by_chrom_1]\n# list of chroms to extract \nparameter: chrom = list\nchrom = list(set(chrom))\n# Path to the input molecular phenotype data.\ninput: phenoFile, for_each = \"chrom\"\noutput: f'{cwd}/{name}.{_chrom}.bed.gz'\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, tags = f'{step_name}_{_output:bn}' \nbash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout', container = container, entrypoint = entrypoint\n Rscript ${modular_script_dir}/data_preprocessing/phenotype/phenotype_formatting.R --step phenotype_by_chrom_1 \\\n --cwd \"${cwd}\" \\\n --phenoFile \"${_input}\" \\\n --name \"${name}\" \\\n --chrom \"${_chrom}\" \\\n --output \"${_output}\" \\\n --numThreads ${numThreads}"
+ "source": [
+ "[phenotype_by_chrom_1]\n",
+ "# list of chroms to extract \n",
+ "parameter: chrom = list\n",
+ "chrom = list(set(chrom))\n",
+ "# Path to the input molecular phenotype data.\n",
+ "input: phenoFile, for_each = \"chrom\"\n",
+ "output: f'{cwd}/{name}.{_chrom}.bed.gz'\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, tags = f'{step_name}_{_output:bn}' \n",
+ "bash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout', container = container, entrypoint = entrypoint\n",
+ " Rscript ${modular_script_dir}/data_preprocessing/phenotype/phenotype_formatting.R --step phenotype_by_chrom_1 \\\n",
+ " --cwd \"${cwd}\" \\\n",
+ " --phenoFile \"${_input}\" \\\n",
+ " --name \"${name}\" \\\n",
+ " --chrom \"${_chrom}\" \\\n",
+ " --output \"${_output}\" \\\n",
+ " --numThreads ${numThreads}"
+ ]
},
{
"cell_type": "code",
@@ -246,7 +475,19 @@
"kernel": "SoS"
},
"outputs": [],
- "source": "[phenotype_by_chrom_2]\n# Path to the input molecular phenotype data.\ninput: group_by = \"all\"\noutput: f'{cwd}/{name}.{step_name[:-2]}_files.txt',f'{cwd}/{name}.{step_name[:-2]}_files.region_list.txt'\nbash: expand= \"${ }\", stderr = f'{_output[0]:n}.stderr', stdout = f'{_output[0]:n}.stdout', container = container, entrypoint = entrypoint\n Rscript ${modular_script_dir}/data_preprocessing/phenotype/phenotype_formatting.R --step phenotype_by_chrom_2 \\\n --cwd \"${cwd}\" \\\n --phenoFile \"${phenoFile}\" \\\n --inputs ${_input} \\\n --output-files \"${_output[0]}\" \\\n --output-region-list \"${_output[1]}\""
+ "source": [
+ "[phenotype_by_chrom_2]\n",
+ "# Path to the input molecular phenotype data.\n",
+ "input: group_by = \"all\"\n",
+ "output: f'{cwd}/{name}.{step_name[:-2]}_files.txt',f'{cwd}/{name}.{step_name[:-2]}_files.region_list.txt'\n",
+ "bash: expand= \"${ }\", stderr = f'{_output[0]:n}.stderr', stdout = f'{_output[0]:n}.stdout', container = container, entrypoint = entrypoint\n",
+ " Rscript ${modular_script_dir}/data_preprocessing/phenotype/phenotype_formatting.R --step phenotype_by_chrom_2 \\\n",
+ " --cwd \"${cwd}\" \\\n",
+ " --phenoFile \"${phenoFile}\" \\\n",
+ " --inputs ${_input} \\\n",
+ " --output-files \"${_output[0]}\" \\\n",
+ " --output-region-list \"${_output[1]}\""
+ ]
},
{
"cell_type": "code",
@@ -279,7 +520,24 @@
"kernel": "SoS"
},
"outputs": [],
- "source": "[phenotype_by_chrom_gct_1]\n# list of chroms to extract \nparameter: chrom = list\nchrom = list(set(chrom))\n# Path to the input molecular phenotype data.\ninput: phenoFile, for_each = \"chrom\"\noutput: f'{cwd:a}/{name}.{_chrom}.gct'\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, tags = f'{step_name}_{_output:bn}' \nbash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout', container = container, entrypoint = entrypoint\n Rscript ${modular_script_dir}/data_preprocessing/phenotype/phenotype_formatting.R --step phenotype_by_chrom_gct_1 \\\n --cwd \"${cwd}\" \\\n --phenoFile \"${_input}\" \\\n --name \"${name}\" \\\n --chrom \"${_chrom}\" \\\n --output \"${_output}\" \\\n --numThreads ${numThreads}"
+ "source": [
+ "[phenotype_by_chrom_gct_1]\n",
+ "# list of chroms to extract \n",
+ "parameter: chrom = list\n",
+ "chrom = list(set(chrom))\n",
+ "# Path to the input molecular phenotype data.\n",
+ "input: phenoFile, for_each = \"chrom\"\n",
+ "output: f'{cwd:a}/{name}.{_chrom}.gct'\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, tags = f'{step_name}_{_output:bn}' \n",
+ "bash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout', container = container, entrypoint = entrypoint\n",
+ " Rscript ${modular_script_dir}/data_preprocessing/phenotype/phenotype_formatting.R --step phenotype_by_chrom_gct_1 \\\n",
+ " --cwd \"${cwd}\" \\\n",
+ " --phenoFile \"${_input}\" \\\n",
+ " --name \"${name}\" \\\n",
+ " --chrom \"${_chrom}\" \\\n",
+ " --output \"${_output}\" \\\n",
+ " --numThreads ${numThreads}"
+ ]
},
{
"cell_type": "code",
@@ -288,7 +546,24 @@
"kernel": "SoS"
},
"outputs": [],
- "source": "[phenotype_by_region_1]\n# An index text file with 4 columns specifying the chr, start, end and name of regions to analyze\nparameter: region_list = path\nregions = [x.strip().split() for x in open(region_list).readlines() if x.strip() and not x.strip().startswith('#')]\n# Path to the input molecular phenotype data.\ninput: phenoFile, for_each = \"regions\"\noutput: f'{cwd}/{region_list:bn}_phenotype_by_region/{name}.{_regions[3]}.bed.gz'\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, tags = f'{step_name}_{_output:bn}' \nbash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout', container = container, entrypoint = entrypoint\n Rscript ${modular_script_dir}/data_preprocessing/phenotype/phenotype_formatting.R --step phenotype_by_region_1 \\\n --cwd \"${cwd}\" \\\n --phenoFile \"${_input}\" \\\n --name \"${name}\" \\\n --region \"${_regions[0]}\" \"${_regions[1]}\" \"${_regions[2]}\" \"${_regions[3]}\" \\\n --output \"${_output}\" \\\n --numThreads ${numThreads}"
+ "source": [
+ "[phenotype_by_region_1]\n",
+ "# An index text file with 4 columns specifying the chr, start, end and name of regions to analyze\n",
+ "parameter: region_list = path\n",
+ "regions = [x.strip().split() for x in open(region_list).readlines() if x.strip() and not x.strip().startswith('#')]\n",
+ "# Path to the input molecular phenotype data.\n",
+ "input: phenoFile, for_each = \"regions\"\n",
+ "output: f'{cwd}/{region_list:bn}_phenotype_by_region/{name}.{_regions[3]}.bed.gz'\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, tags = f'{step_name}_{_output:bn}' \n",
+ "bash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout', container = container, entrypoint = entrypoint\n",
+ " Rscript ${modular_script_dir}/data_preprocessing/phenotype/phenotype_formatting.R --step phenotype_by_region_1 \\\n",
+ " --cwd \"${cwd}\" \\\n",
+ " --phenoFile \"${_input}\" \\\n",
+ " --name \"${name}\" \\\n",
+ " --region \"${_regions[0]}\" \"${_regions[1]}\" \"${_regions[2]}\" \"${_regions[3]}\" \\\n",
+ " --output \"${_output}\" \\\n",
+ " --numThreads ${numThreads}"
+ ]
},
{
"cell_type": "code",
@@ -297,7 +572,16 @@
"kernel": "SoS"
},
"outputs": [],
- "source": "[phenotype_by_region_2]\ninput: group_by = \"all\"\noutput: f'{cwd}/{name}.{step_name[:-2]}_files.txt'\nbash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout', container = container, entrypoint = entrypoint\n Rscript ${modular_script_dir}/data_preprocessing/phenotype/phenotype_formatting.R --step phenotype_by_region_2 \\\n --cwd \"${cwd}\" \\\n --inputs ${_input} \\\n --output \"${_output}\""
+ "source": [
+ "[phenotype_by_region_2]\n",
+ "input: group_by = \"all\"\n",
+ "output: f'{cwd}/{name}.{step_name[:-2]}_files.txt'\n",
+ "bash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout', container = container, entrypoint = entrypoint\n",
+ " Rscript ${modular_script_dir}/data_preprocessing/phenotype/phenotype_formatting.R --step phenotype_by_region_2 \\\n",
+ " --cwd \"${cwd}\" \\\n",
+ " --inputs ${_input} \\\n",
+ " --output \"${_output}\""
+ ]
},
{
"cell_type": "code",
@@ -306,7 +590,21 @@
"kernel": "SoS"
},
"outputs": [],
- "source": "[bam_subsetting]\n# Input to `samtools view` coordinates, for example, --region chr21 chr22\nparameter: region = list\n# Path to the input molecular phenotype data.\nparameter: phenoFile = paths\ninput: phenoFile , group_by = 1\noutput: f'{cwd}/{_input:bn}.subsetted.bam'\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads\nbash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout', container=container, entrypoint=entrypoint\n Rscript ${modular_script_dir}/data_preprocessing/phenotype/phenotype_formatting.R --step bam_subsetting \\\n --phenoFile \"${_input}\" \\\n --region ${' '.join(region)} \\\n --output \"${_output}\""
+ "source": [
+ "[bam_subsetting]\n",
+ "# Input to `samtools view` coordinates, for example, --region chr21 chr22\n",
+ "parameter: region = list\n",
+ "# Path to the input molecular phenotype data.\n",
+ "parameter: phenoFile = paths\n",
+ "input: phenoFile , group_by = 1\n",
+ "output: f'{cwd}/{_input:bn}.subsetted.bam'\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads\n",
+ "bash: expand= \"${ }\", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout', container=container, entrypoint=entrypoint\n",
+ " Rscript ${modular_script_dir}/data_preprocessing/phenotype/phenotype_formatting.R --step bam_subsetting \\\n",
+ " --phenoFile \"${_input}\" \\\n",
+ " --region ${' '.join(region)} \\\n",
+ " --output \"${_output}\""
+ ]
},
{
"cell_type": "code",
@@ -364,4 +662,4 @@
},
"nbformat": 4,
"nbformat_minor": 4
-}
\ No newline at end of file
+}
diff --git a/code/SoS/data_preprocessing/phenotype/phenotype_imputation.ipynb b/code/SoS/data_preprocessing/phenotype/phenotype_imputation.ipynb
index 391a633a0..8aa073f9f 100644
--- a/code/SoS/data_preprocessing/phenotype/phenotype_imputation.ipynb
+++ b/code/SoS/data_preprocessing/phenotype/phenotype_imputation.ipynb
@@ -8,16 +8,8 @@
},
"source": [
"# Phenotype data imputation\n",
- "\n"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "## Description"
+ "\n",
+ "Imputes missing values in a molecular phenotype matrix, so downstream models see a complete matrix."
]
},
{
@@ -26,7 +18,13 @@
"kernel": "SoS"
},
"source": [
- "Empirical Bayes Matrix Factorization (EBMF) is the primary imputation method we use to impute molecular phenotype data [[cf. Qi et al., medRxiv, 2023](https://doi.org/10.1101/2023.11.29.23299181)]. We also provide a collection of other methods to impute missing omics data values including missing forest, XGboost, k-nearest neighbors, soft impute, mean imputation, and limit of detection."
+ "## Overview\n",
+ "\n",
+ "Molecular phenotype matrices are rarely complete: a feature can be unmeasured in a sample for technical reasons, and the models downstream generally require a full matrix. Dropping every feature with a missing entry throws away real signal, so this step imputes the gaps instead.\n",
+ "\n",
+ "Empirical Bayes matrix factorisation is the primary method here, `EBMF` for a single matrix and `gEBMF` when the data are grouped, following [Qi et al., medRxiv 2023](https://doi.org/10.1101/2023.11.29.23299181). Seven alternatives are also provided - missForest, missXGBoost, k-nearest neighbours, SoftImpute, mean imputation and limit-of-detection imputation - so a method can be matched to the assay.\n",
+ "\n",
+ "**When to run it.** After phenotype QC and normalisation, before covariate preprocessing and the association scan."
]
},
{
@@ -35,7 +33,20 @@
"kernel": "SoS"
},
"source": [
- "**Timing:** ~varies on typical compute infrastructure."
+ "## Input\n",
+ "\n",
+ "- `--phenoFile`: the molecular phenotype matrix in BED layout, the first four columns being `#chr`, `start`, `end` and `ID` and the rest one column per sample, with missing entries written as `NA`. Example `input/proteomics/protocol_example.protein.missing.bed.gz`, a toy matrix with roughly 10% of values set to missing so the methods have gaps to fill:\n",
+ "\n",
+ " ```\n",
+ " #chr start end ID SAMPLE_001 SAMPLE_002\n",
+ " chr12 752578 752579 chr12_ENSG00000060237_Q9H4A3 0.238966360190167 -0.611171227886468\n",
+ " chr12 990508 990509 chr12_ENSG00000082805_Q8IUD2 NA -1.86313205860919\n",
+ " ```\n",
+ "\n",
+ "- `--num_factor`: the number of factors for the `EBMF` and `gEBMF` models. It must be smaller than the number of samples; the toy set has 60, so the examples use `30`.\n",
+ "- `--cwd`: the directory outputs are written to.\n",
+ "- `--save_flash`: off by default. When set, `gEBMF` also writes the fitted flashier factor object.\n",
+ "- `--no-qc-prior-to-impute`: leaves quality control on by default. Passing it skips QC, which also means the QC matrix is not available to the output."
]
},
{
@@ -44,11 +55,16 @@
"kernel": "SoS"
},
"source": [
- "## Input Files\n",
+ "## Output\n",
+ "\n",
+ "- `{cwd}/{phenoFile}.imputed.bed.gz` - the imputed matrix in the same BED layout as the input, bgzipped and tabix-indexed, with a `.tbi` beside it. Example `output/phenotype_imputation_uf/protocol_example.protein.missing.bed.imputed.bed.gz`, where the gaps shown in the input preview above are filled:\n",
"\n",
- "| File | Description |\n",
- "|------|-------------|\n",
- "| `input/proteomics/protocol_example.protein.missing.bed.gz` | Toy molecular phenotype matrix in BED format (first four columns `#chr`/`start`/`end`/`ID`, remaining columns one per sample) with ~10% values set to missing (`NA`) so the imputation methods have gaps to fill. |\n"
+ " ```\n",
+ " #chr\tstart\tend\tID\tSAMPLE_001\tSAMPLE_002\tSAMPLE_003\tSAMPLE_004\tSAMPLE_005\tSAMPLE_006\tSAMP\n",
+ " chr12\t752578\t752579\tchr12_ENSG00000060237_Q9H4A3\t0.238966360190167\t-0.611171227886468\t1.\n",
+ " chr12\t990508\t990509\tchr12_ENSG00000082805_Q8IUD2\t-0.762712503460405\t-1.86313205860919\t1.\n",
+ " ```\n",
+ "- `{cwd}/{phenoFile}.gEBMF_factors.rds` - the saved flashier factor object from a `gEBMF` fit, written only when `--save_flash True` is given. It is a binary R object, so it has no text preview; load it with `readRDS`. Example `output/phenotype_imputation_uf/protocol_example.protein.missing.bed.gEBMF_factors.rds`."
]
},
{
@@ -57,12 +73,9 @@
"kernel": "SoS"
},
"source": [
- "## Output Files\n",
+ "## Minimal Working Example\n",
"\n",
- "| File | Description |\n",
- "|------|-------------|\n",
- "| `output/phenotype_imputation_uf/protocol_example.protein.missing.bed.imputed.bed.gz` (+ `.tbi`) | Imputed molecular phenotype matrix in the same BED layout, bgzipped and tabix-indexed. |\n",
- "| `output/phenotype_imputation_uf/protocol_example.protein.missing.bed.gEBMF_factors.rds` | Saved flashier factor object from the gEBMF fit (written only when `--save_flash True`). |\n"
+ "Eight imputation methods plus a filter that drops features too sparse to impute. Pick one method. Two practical notes for the toy data: `--num_factor` for the EBMF and gEBMF factor models must be smaller than the number of samples, which is 60 here, so the examples use `--num_factor 30`; and leave quality control enabled, that is do not pass `--no-qc-prior-to-impute`, so the QC matrix used for the output is available to every method."
]
},
{
@@ -71,23 +84,19 @@
"kernel": "SoS"
},
"source": [
- "## Steps\n",
+ "### Grouped Empirical Bayes Matrix Factorization (gEBMF)\n",
"\n",
- "The pipeline offers several imputation methods. Any one of them can be run on the toy file below; each writes an imputed BED next to the `--cwd` output directory. Two practical notes for the toy data:\n",
- "\n",
- "- `--num_factor` for the EBMF / gEBMF factor models must be smaller than the number of samples (the toy set has 60 samples), so the examples use `--num_factor 30`.\n",
- "- Leave quality control enabled (do not pass `--no-qc-prior-to-impute`) so that the QC matrix used for the output is available to every method.\n"
+ "Grouped Empirical Bayes Matrix Factorization. Extends EBMF by fitting factors within row groups (here, by chromosome), so structure shared across molecular features in the same group is borrowed when filling in missing values. This is the recommended default for the protocol.\n"
]
},
{
"cell_type": "markdown",
"metadata": {
- "kernel": "SoS"
+ "kernel": "SoS",
+ "tags": []
},
"source": [
- "### gEBMF (grouped Empirical Bayes Matrix Factorization)\n",
- "\n",
- "Grouped Empirical Bayes Matrix Factorization. Extends EBMF by fitting factors within row groups (here, by chromosome), so structure shared across molecular features in the same group is borrowed when filling in missing values. This is the recommended default for the protocol.\n"
+ "**Timing**: TBD (on toy dataset)"
]
},
{
@@ -110,11 +119,21 @@
"kernel": "SoS"
},
"source": [
- "### EBMF (Empirical Bayes Matrix Factorization)\n",
+ "### Empirical Bayes Matrix Factorization (EBMF)\n",
"\n",
"Empirical Bayes Matrix Factorization. Decomposes the phenotype matrix into a small number of latent factors with adaptive shrinkage priors, then reconstructs the matrix to impute missing entries. Captures global low-rank structure across all samples and features.\n"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS",
+ "tags": []
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ]
+ },
{
"cell_type": "code",
"execution_count": null,
@@ -140,6 +159,16 @@
"A non-parametric, iterative random-forest imputation. Each feature with missing values is predicted in turn from the other features using a random forest, repeating until the imputed values stabilize. Handles non-linear relationships but is computationally heavier.\n"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS",
+ "tags": []
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ]
+ },
{
"cell_type": "code",
"execution_count": null,
@@ -164,6 +193,16 @@
"Iterative imputation using gradient-boosted trees (XGBoost) as the per-feature predictor instead of random forests. Often faster and more accurate than missForest on large matrices while still modeling non-linear dependencies between features.\n"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS",
+ "tags": []
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ]
+ },
{
"cell_type": "code",
"execution_count": null,
@@ -188,6 +227,16 @@
"k-Nearest-Neighbours imputation. A missing entry is filled with a (distance-weighted) average of the corresponding values from the k most similar samples. Simple and fast, and works well when samples cluster into similar profiles.\n"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS",
+ "tags": []
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ]
+ },
{
"cell_type": "code",
"execution_count": null,
@@ -212,6 +261,16 @@
"Matrix-completion via iterative soft-thresholded singular value decomposition. Assumes the phenotype matrix is approximately low-rank and recovers missing entries by repeatedly shrinking the singular values. A good linear baseline for structured data.\n"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS",
+ "tags": []
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ]
+ },
{
"cell_type": "code",
"execution_count": null,
@@ -236,6 +295,16 @@
"Mean imputation. Each missing value is replaced by the mean of the observed values for that feature. The simplest baseline; fast but ignores correlations between samples and features.\n"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS",
+ "tags": []
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ]
+ },
{
"cell_type": "code",
"execution_count": null,
@@ -260,6 +329,16 @@
"Limit-of-detection imputation. Missing values are replaced with a low constant derived from the smallest observed values, appropriate when missingness is driven by measurements falling below an assay's detection threshold (e.g., low-abundance proteins/metabolites).\n"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS",
+ "tags": []
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ]
+ },
{
"cell_type": "code",
"execution_count": null,
@@ -273,6 +352,39 @@
" --cwd output/phenotype_imputation_uf\n"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### Drop features that are too sparse to impute\n",
+ "\n",
+ "`bed_filter_na` removes rows whose missing fraction is above the threshold, so that the imputation methods are not asked to invent values for features with almost no data. Run it before a method rather than instead of one."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/phenotype_imputation.ipynb bed_filter_na \\\n",
+ " --cwd output/phenotype_imputation_uf \\\n",
+ " --phenoFile input/proteomics/protocol_example.protein.missing.bed.gz"
+ ]
+ },
{
"cell_type": "markdown",
"metadata": {
@@ -286,11 +398,11 @@
"cell_type": "code",
"execution_count": null,
"metadata": {
- "kernel": "Bash"
+ "kernel": "SoS"
},
"outputs": [],
"source": [
- "sos run phenotype_imputation.ipynb -h"
+ "sos run pipeline/phenotype_imputation.ipynb -h"
]
},
{
@@ -300,7 +412,7 @@
},
"source": [
"```\n",
- "usage: sos run phenotype_imputation.ipynb\n",
+ "usage: sos run pipeline/phenotype_imputation.ipynb\n",
" [workflow_name | -t targets] [options] [workflow_options]\n",
" workflow_name: Single or combined workflows defined in this script\n",
" targets: One or more targets to generate\n",
@@ -319,6 +431,7 @@
" bed_filter_na\n",
"\n",
"Global Workflow Options:\n",
+ " --modular-script-dir code/script (as path)\n",
" --cwd output (as path)\n",
" Work directory & output directory\n",
" --phenoFile VAL (as path, required)\n",
@@ -334,6 +447,7 @@
" --numThreads 20 (as int)\n",
" Number of threads\n",
" --container ''\n",
+ " --entrypoint ''\n",
"\n",
"Sections\n",
" EBMF:\n",
@@ -361,8 +475,11 @@
" --lambda-hyp 30 (as int)\n",
" --impute-method soft\n",
" --tol-missing 0.05 (as float)\n",
- " Tolerance of missingness rows with missing rate larger than tol_missing will be removed, with missing rate smaller than tol_missing will be mean_imputed. Say if\n",
- " we want to keep rows with less than 5% missing, then we use 0.05 as tol_missing.\n",
+ " Tolerance of missingness rows with missing rate larger\n",
+ " than tol_missing will be removed, with missing rate\n",
+ " smaller than tol_missing will be mean_imputed. Say if we\n",
+ " want to keep rows with less than 5% missing, then we use\n",
+ " 0.05 as tol_missing.\n",
"```"
]
},
@@ -372,7 +489,7 @@
"kernel": "SoS"
},
"source": [
- "## Setup and global parameters"
+ "## Workflow implementation"
]
},
{
@@ -629,40 +746,6 @@
" --tol-missing ${tol_missing} \\\n",
" --numThreads ${numThreads}\n"
]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS",
- "tags": [],
- "vscode": {
- "languageId": "plaintext"
- }
- },
- "source": [
- "The resource usage for softimputing 450K methylation data are as followed:\n",
- "\n",
- "``` \n",
- "time elapsed: 880.90s\n",
- "peak first occurred: 152.11s\n",
- "peak last occurred: 175.41s\n",
- "max vms_memory: 38.95GB\n",
- "max rss_memory: 34.35GB\n",
- "memory check interval: 1s\n",
- "return code: 0\n",
- "```"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "## Anticipated Results\n",
- "\n",
- "The pipeline produces output files in the `output/` subdirectory named after the workflow step. Verify success by checking that output files exist and are non-empty. See the **Output** section above for the expected file names and formats."
- ]
}
],
"metadata": {
@@ -693,4 +776,4 @@
},
"nbformat": 4,
"nbformat_minor": 4
-}
\ No newline at end of file
+}
diff --git a/code/SoS/data_preprocessing/phenotype_preprocessing.ipynb b/code/SoS/data_preprocessing/phenotype_preprocessing.ipynb
index f76da6c36..a26996452 100644
--- a/code/SoS/data_preprocessing/phenotype_preprocessing.ipynb
+++ b/code/SoS/data_preprocessing/phenotype_preprocessing.ipynb
@@ -6,10 +6,11 @@
"kernel": "SoS"
},
"source": [
- "# Phenotype Preprocessing\n",
+ "# Phenotype preprocessing\n",
"\n",
- "This notebook is a minimal working example (MWE) of the phenotype preprocessing section of the xQTL protocol. It chains the phenotype worker pipelines on the bundled toy dataset (60 samples, `protocol_example.*`) so the whole section runs end-to-end without editing."
- ]
+ "Prepare molecular-phenotype matrices for xQTL analysis by imputing missing values when needed, adding genomic annotations, and formatting data for chromosome-, region-, TAD-, or sample-specific analyses.\n"
+ ],
+ "id": "4189c8e9"
},
{
"cell_type": "markdown",
@@ -17,16 +18,13 @@
"kernel": "SoS"
},
"source": [
- "## Description\n",
- "\n",
- "The phenotype preprocessing section takes a molecular phenotype matrix through genomic-coordinate annotation, missing-value imputation, and partition by chromosome, producing a BED-formatted phenotype ready for QTL association testing. It chains three worker pipelines:\n",
+ "#### Miniprotocol Timing\n",
"\n",
- "- `gene_annotation.ipynb` — add genomic coordinate annotation and convert to `.bed`\n",
- "- `phenotype_imputation.ipynb` — impute missing values (gEBMF and other methods)\n",
- "- `phenotype_formatting.ipynb` — partition the phenotype by chromosome\n",
+ "This is the total duration for the standard toy-data route; module-specific timings appear on their respective pages.\n",
"\n",
- "**Timing:** < 12 min for the toy dataset on a single node."
- ]
+ "Timing: <12 min (on the toy dataset)\n"
+ ],
+ "id": "9baa6527"
},
{
"cell_type": "markdown",
@@ -34,14 +32,13 @@
"kernel": "SoS"
},
"source": [
- "## Input Files\n",
+ "## Overview\n",
"\n",
- "| File | Description |\n",
- "| --- | --- |\n",
- "| `input/rnaseq/protocol_example.rnaseq.bed.gz` | Gene-level molecular phenotype matrix (60 samples) |\n",
- "| `input/proteomics/protocol_example.protein.missing.bed.gz` | Protein phenotype with missing values (for imputation) |\n",
- "| `reference_data/Homo_sapiens.GRCh38.103.chr.reformatted.collapse_only.gene.ERCC.gtf` | GENCODE/Ensembl gene coordinate annotation |"
- ]
+ "This mini-protocol walks through how molecular-phenotype matrices are prepared for downstream xQTL analysis. Each step calls a workflow from [`phenotype_imputation.ipynb`](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/phenotype/phenotype_imputation.html), [`gene_annotation.ipynb`](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/phenotype/gene_annotation.html), or [`phenotype_formatting.ipynb`](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/phenotype/phenotype_formatting.html).\n",
+ "\n",
+ "The commands are selectable routes rather than one mandatory chain. Impute only matrices containing missing values. Choose the annotation workflow matching gene, protein, LeafCutter, or psichomics identifiers, then choose the formatting workflow required by the downstream analysis unit. GCT sample extraction and BAM subsetting are independent utilities.\n"
+ ],
+ "id": "416d15c6"
},
{
"cell_type": "markdown",
@@ -49,14 +46,27 @@
"kernel": "SoS"
},
"source": [
- "## Output Files\n",
+ "## Steps\n",
"\n",
- "| File | Description |\n",
- "| --- | --- |\n",
- "| `output/rnaseq/protocol_example.rnaseq.bed.bed.gz` | Coordinate-annotated phenotype in BED format |\n",
- "| `output/phenotype_imputation_uf/*.imputed.*` | Imputed phenotype matrix |\n",
- "| `output/phenotype_uf/*.chr22.bed.gz` | Per-chromosome phenotype, ready for TensorQTL |"
- ]
+ "Choose a route before running commands; the 12 commands are not one mandatory chain.\n",
+ "\n",
+ "| **Analysis goal** | **Commands to run, in order** | **Inputs** |\n",
+ "| --- | --- | --- |\n",
+ "| Gene-expression or protein phenotype by chromosome | 2 \u2192 7 | `input/rnaseq/protocol_example.rnaseq.bed.gz`; `input/reference_data/Homo_sapiens.GRCh38.103.chr.reformatted.collapse_only.gene.ERCC.gtf` |\n",
+ "| Phenotype with missing values by chromosome | 1 \u2192 2 \u2192 7 | `input/proteomics/protocol_example.protein.missing.bed.gz`; gene-coordinate GTF above |\n",
+ "| Retrieve gene coordinates from Ensembl BioMart | 3 | `input/rnaseq/protocol_example.rnaseq.gene_ID.tsv`; internet access |\n",
+ "| LeafCutter clusters mapped to genes | 4 | `input/rnaseq/protocol_example.leafcutter.phenotype.bed.gz`; `input/rnaseq/protocol_example.leafcutter.intron_count.tsv`; `input/reference_data/Homo_sapiens.GRCh38.103.chr.gtf` |\n",
+ "| LeafCutter isoforms annotated for QTL analysis | 5 | `input/rnaseq/protocol_example.leafcutter.phenotype.bed.gz`; `input/rnaseq/protocol_example.leafcutter.intron_count.tsv`; `input/reference_data/Homo_sapiens.GRCh38.103.chr.gtf` |\n",
+ "| psichomics isoforms annotated for QTL analysis | 6 | `input/rnaseq/protocol_example.psichomics.phenotype.tsv`; `input/reference_data/Homo_sapiens.GRCh38.103.chr.gtf` |\n",
+ "| Partition a GCT matrix by chromosome | 8 | `input/rnaseq/protocol_example.rnaseq.gene_tpm.gct.gz` |\n",
+ "| Partition a BED phenotype by predefined regions | 2 \u2192 9 | `input/rnaseq/protocol_example.rnaseq.bed.gz`; gene-coordinate GTF above; `input/reference_data/TAD/protocol_example_protein.enhanced_cis_chr22.bed` |\n",
+ "| Define TAD-based phenotype regions | 2 \u2192 10 | `input/rnaseq/protocol_example.rnaseq.bed.gz`; gene-coordinate GTF above; `input/reference_data/TAD/TADB_enhanced_cis.bed` |\n",
+ "| Extract selected samples from a GCT matrix | 11 | `input/rnaseq/protocol_example.rnaseq.gene_tpm.gct.gz`; `input/colocboost/keep_samples.txt` |\n",
+ "| Subset BAM files to selected genomic regions | 12 | `input/rnaseq/bam_file_list.txt`; referenced BAM files; no example BAM is currently bundled |\n",
+ "\n",
+ "Run only the row matching the intended analysis goal, following its commands in numerical order. Other imputation methods are available through `phenotype_imputation.ipynb` and its Command Interface."
+ ],
+ "id": "6628e5e0"
},
{
"cell_type": "markdown",
@@ -64,10 +74,26 @@
"kernel": "SoS"
},
"source": [
- "## Steps\n",
+ "### 1. [Impute missing phenotype values](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/phenotype/phenotype_imputation.html)\n",
"\n",
- "Run the commands below in order from the toy example root. Each one invokes the corresponding worker pipeline through the `pipeline/` symlinks."
- ]
+ "**What it does:** Use generalized empirical Bayes matrix factorization to complete the example protein matrix.\n"
+ ],
+ "id": "a7c33119"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "kernel": "Bash"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/phenotype_imputation.ipynb gEBMF \\\n",
+ " --phenoFile input/proteomics/protocol_example.protein.missing.bed.gz \\\n",
+ " --cwd output/phenotype_imputation_uf \\\n",
+ " --num_factor 30\n"
+ ],
+ "id": "c79f6386"
},
{
"cell_type": "markdown",
@@ -75,10 +101,11 @@
"kernel": "SoS"
},
"source": [
- "### Annotate phenotype with genomic coordinates\n",
+ "### 2. [Add genomic coordinates to gene or protein phenotypes](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/phenotype/gene_annotation.html)\n",
"\n",
- "Add gene-level genomic coordinates to the molecular phenotype matrix and convert it to BED format, using the gene annotation GTF."
- ]
+ "**What it does:** Join phenotype identifiers to a supplied coordinate annotation and write a coordinate-aware BED matrix.\n"
+ ],
+ "id": "6c5a94f3"
},
{
"cell_type": "code",
@@ -89,11 +116,12 @@
"outputs": [],
"source": [
"sos run pipeline/gene_annotation.ipynb annotate_coord \\\n",
- " --cwd output/rnaseq \\\n",
+ " --cwd output/gene_annotation \\\n",
" --phenoFile input/rnaseq/protocol_example.rnaseq.bed.gz \\\n",
" --coordinate-annotation input/reference_data/Homo_sapiens.GRCh38.103.chr.reformatted.collapse_only.gene.ERCC.gtf \\\n",
- " --phenotype-id-column gene_id"
- ]
+ " --phenotype-id-column gene_id\n"
+ ],
+ "id": "dc78fd7a"
},
{
"cell_type": "markdown",
@@ -101,10 +129,11 @@
"kernel": "SoS"
},
"source": [
- "### Impute missing values\n",
+ "### 3. [Retrieve gene coordinates from Ensembl BioMart](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/phenotype/gene_annotation.html)\n",
"\n",
- "Impute missing entries in the phenotype matrix. Here the gEBMF (greedy Empirical Bayes Matrix Factorization) method is used on the toy protein phenotype; other methods (EBMF, missForest, missXGBoost, knn, soft, mean, lod) are also available in the worker pipeline."
- ]
+ "**What it does:** Query the selected Ensembl release when a local coordinate annotation is unavailable.\n"
+ ],
+ "id": "4a64a987"
},
{
"cell_type": "code",
@@ -114,11 +143,12 @@
},
"outputs": [],
"source": [
- "sos run pipeline/phenotype_imputation.ipynb gEBMF \\\n",
- " --phenoFile input/proteomics/protocol_example.protein.missing.bed.gz \\\n",
- " --cwd output/phenotype_imputation_uf \\\n",
- " --num_factor 30"
- ]
+ "sos run pipeline/gene_annotation.ipynb annotate_coord_biomart \\\n",
+ " --cwd output/gene_annotation \\\n",
+ " --phenoFile input/rnaseq/protocol_example.rnaseq.gene_ID.tsv \\\n",
+ " --ensembl-version 115\n"
+ ],
+ "id": "7401792f"
},
{
"cell_type": "markdown",
@@ -126,10 +156,28 @@
"kernel": "SoS"
},
"source": [
- "### Partition phenotype by chromosome\n",
+ "### 4. [Map LeafCutter clusters to genes](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/phenotype/gene_annotation.html)\n",
"\n",
- "Split the annotated phenotype into per-chromosome files (here chromosome 22), the format expected by the QTL association steps."
- ]
+ "**What it does:** Assign LeafCutter clusters to genes using splice-site overlap with the gene annotation.\n"
+ ],
+ "id": "e7409492"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "kernel": "Bash"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/gene_annotation.ipynb map_leafcutter_cluster_to_gene \\\n",
+ " --cwd output/gene_annotation \\\n",
+ " --phenoFile input/rnaseq/protocol_example.leafcutter.phenotype.bed.gz \\\n",
+ " --intron-count input/rnaseq/protocol_example.leafcutter.intron_count.tsv \\\n",
+ " --coordinate-annotation input/reference_data/Homo_sapiens.GRCh38.103.chr.gtf \\\n",
+ " --map-stra site\n"
+ ],
+ "id": "355635fc"
},
{
"cell_type": "markdown",
@@ -137,10 +185,67 @@
"kernel": "SoS"
},
"source": [
- "## Anticipated Results\n",
+ "### 5. [Annotate LeafCutter isoforms](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/phenotype/gene_annotation.html)\n",
"\n",
- "The pipeline produces output files in the `output/` subdirectory named after the workflow step. Verify success by checking that output files exist and are non-empty. See the **Output** section above for the expected file names and formats."
- ]
+ "**What it does:** Convert LeafCutter intron-cluster phenotypes into annotated isoform features for QTL analysis.\n"
+ ],
+ "id": "16afacb2"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "kernel": "Bash"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/gene_annotation.ipynb annotate_leafcutter_isoforms \\\n",
+ " --cwd output/gene_annotation \\\n",
+ " --phenoFile input/rnaseq/protocol_example.leafcutter.phenotype.bed.gz \\\n",
+ " --intron-count input/rnaseq/protocol_example.leafcutter.intron_count.tsv \\\n",
+ " --coordinate-annotation input/reference_data/Homo_sapiens.GRCh38.103.chr.gtf \\\n",
+ " --map-stra site\n"
+ ],
+ "id": "72809d29"
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### 6. [Annotate psichomics isoforms](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/phenotype/gene_annotation.html)\n",
+ "\n",
+ "**What it does:** Add genomic and gene annotations to psichomics-derived splicing phenotypes.\n"
+ ],
+ "id": "acfd2814"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "kernel": "Bash"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/gene_annotation.ipynb annotate_psichomics_isoforms \\\n",
+ " --cwd output/gene_annotation \\\n",
+ " --phenoFile input/rnaseq/protocol_example.psichomics.phenotype.tsv \\\n",
+ " --coordinate-annotation input/reference_data/Homo_sapiens.GRCh38.103.chr.gtf\n"
+ ],
+ "id": "e6d20f6d"
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### 7. [Partition a BED phenotype by chromosome](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/phenotype/phenotype_formatting.html)\n",
+ "\n",
+ "**What it does:** Split a coordinate-annotated BED phenotype into chromosome-specific files.\n"
+ ],
+ "id": "557f48cf"
},
{
"cell_type": "code",
@@ -151,11 +256,213 @@
"outputs": [],
"source": [
"sos run pipeline/phenotype_formatting.ipynb phenotype_by_chrom \\\n",
- " --cwd output/phenotype/phenotype_by_chrom_for_cis \\\n",
- " --phenoFile output/rnaseq/protocol_example.rnaseq.bed.bed.gz \\\n",
- " --name bulk_rnaseq \\\n",
- " --chrom chr22"
- ]
+ " --cwd output/phenotype_uf \\\n",
+ " --phenoFile output/gene_annotation/protocol_example.rnaseq.bed.bed.gz \\\n",
+ " --name protocol_example \\\n",
+ " --chrom chr22\n"
+ ],
+ "id": "7c3143df"
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### 8. [Partition a GCT phenotype by chromosome](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/phenotype/phenotype_formatting.html)\n",
+ "\n",
+ "**What it does:** Split a coordinate-aware GCT matrix into chromosome-specific GCT files.\n"
+ ],
+ "id": "ff8a1cbe"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "kernel": "Bash"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/phenotype_formatting.ipynb phenotype_by_chrom_gct \\\n",
+ " --cwd output/phenotype_gct \\\n",
+ " --phenoFile input/rnaseq/protocol_example.rnaseq.gene_tpm.gct.gz \\\n",
+ " --chrom chr21 chr22\n"
+ ],
+ "id": "7f114e77"
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### 9. [Partition a phenotype by predefined regions](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/phenotype/phenotype_formatting.html)\n",
+ "\n",
+ "**What it does:** Extract phenotype features falling within each region in a supplied region list.\n"
+ ],
+ "id": "90267404"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "kernel": "Bash"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/phenotype_formatting.ipynb phenotype_by_region \\\n",
+ " --cwd output/phenotype_by_region \\\n",
+ " --phenoFile output/gene_annotation/protocol_example.rnaseq.bed.bed.gz \\\n",
+ " --region-list input/reference_data/TAD/protocol_example_protein.enhanced_cis_chr22.bed\n"
+ ],
+ "id": "cb9b0cde"
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### 10. [Define TAD-based phenotype regions](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/phenotype/phenotype_formatting.html)\n",
+ "\n",
+ "**What it does:** Assign phenotype features to TAD windows and generate a region list for downstream analysis.\n"
+ ],
+ "id": "cfce416b"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "kernel": "Bash"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/phenotype_formatting.ipynb phenotype_annotate_by_tad \\\n",
+ " --cwd output/phenotype_by_region \\\n",
+ " --phenoFile output/gene_annotation/protocol_example.rnaseq.bed.bed.gz \\\n",
+ " --TAD-list input/reference_data/TAD/TADB_enhanced_cis.bed \\\n",
+ " --phenotype-per-tad 2\n"
+ ],
+ "id": "107d5715"
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### 11. [Extract selected samples from a GCT matrix](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/phenotype/phenotype_formatting.html)\n",
+ "\n",
+ "**What it does:** Retain only samples listed in a supplied keep file.\n"
+ ],
+ "id": "03c6b4ce"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "kernel": "Bash"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/phenotype_formatting.ipynb gct_extract_samples \\\n",
+ " --cwd output/phenotype_gct \\\n",
+ " --phenoFile input/rnaseq/protocol_example.rnaseq.gene_tpm.gct.gz \\\n",
+ " --keep-samples input/colocboost/keep_samples.txt\n"
+ ],
+ "id": "addabe04"
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### 12. [Subset BAM files by genomic region](https://statfungen.github.io/xqtl-protocol/code/data_preprocessing/phenotype/phenotype_formatting.html)\n",
+ "\n",
+ "**What it does:** Extract selected chromosomes or regions from every BAM listed in the input manifest.\n"
+ ],
+ "id": "720706d3"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "kernel": "Bash"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/phenotype_formatting.ipynb bam_subsetting \\\n",
+ " --cwd output/bam_subset \\\n",
+ " --phenoFile input/rnaseq/bam_file_list.txt \\\n",
+ " --region chr21 chr22\n"
+ ],
+ "id": "d5c0f358"
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "## Output Files\n",
+ "\n",
+ "| Route or step | Products and relative paths |\n",
+ "| --- | --- |\n",
+ "| Step 1 | `output/phenotype_imputation_uf/protocol_example.protein.missing.bed.imputed.bed.gz` |\n",
+ "| Step 2 | `output/gene_annotation/protocol_example.rnaseq.bed.bed.gz`; `output/gene_annotation/protocol_example.rnaseq.bed.region_list.txt` |\n",
+ "| Step 3 | `output/gene_annotation/protocol_example.rnaseq.gene_ID.bed.gz` |\n",
+ "| Step 4 | `output/gene_annotation/protocol_example.leafcutter.phenotype.bed.gz.exon_list`; `output/gene_annotation/protocol_example.leafcutter.phenotype.bed.gz.leafcutter.clusters_to_genes.txt` |\n",
+ "| Step 5 | `output/gene_annotation/protocol_example.leafcutter.phenotype.bed.formated.bed.gz`; `output/gene_annotation/protocol_example.leafcutter.phenotype.phenotype_group.txt` |\n",
+ "| Step 6 | `output/gene_annotation/protocol_example.psichomics.phenotype.formated.bed.gz`; `output/gene_annotation/protocol_example.psichomics.phenotype.phenotype_group.txt` |\n",
+ "| Step 7 | `output/phenotype_uf/protocol_example.chr22.bed.gz`; `output/phenotype_uf/protocol_example.phenotype_by_chrom_files.txt`; its region list |\n",
+ "| Step 8 | `output/phenotype_gct/protocol_example.rnaseq.gene_tpm.chr21.gct`; `output/phenotype_gct/protocol_example.rnaseq.gene_tpm.chr22.gct` |\n",
+ "| Step 9 | `output/phenotype_by_region/protocol_example_protein.enhanced_cis_chr22_phenotype_by_region/*.bed.gz`; `output/phenotype_by_region/*.phenotype_by_region_files.txt` |\n",
+ "| Step 10 | `output/phenotype_by_region/*_pheno_per_region.region_list`; TAD-annotated region list under `output/phenotype_by_region/` |\n",
+ "| Step 11 | `output/phenotype_gct/protocol_example.rnaseq.gene_tpm.sample_matched.gct.gz` |\n",
+ "| Step 12 | `output/bam_subset/*.subsetted.bam` |"
+ ],
+ "id": "48f58267"
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "## Anticipated Results\n",
+ "\n",
+ "The selected route produces a molecular-phenotype matrix with the genomic annotation and layout required by its downstream xQTL analysis. Standard gene-expression routes typically end with chromosome-specific BED files; splicing routes end with gene- or isoform-annotated phenotypes; region and TAD routes end with per-region files and their manifest.\n",
+ "\n",
+ "Continue with covariate preprocessing and then the appropriate association-testing workflow.\n"
+ ],
+ "id": "c7bd9755"
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "## Command interface\n",
+ "\n",
+ "List the workflows and parameters available in each module used by this mini-protocol.\n"
+ ],
+ "id": "08582e7a"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "kernel": "Bash"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/phenotype_imputation.ipynb -h\n",
+ "sos run pipeline/gene_annotation.ipynb -h\n",
+ "sos run pipeline/phenotype_formatting.ipynb -h\n"
+ ],
+ "id": "a71cd0bd"
}
],
"metadata": {
@@ -193,5 +500,5 @@
}
},
"nbformat": 4,
- "nbformat_minor": 4
-}
+ "nbformat_minor": 5
+}
\ No newline at end of file
diff --git a/code/SoS/enrichment/enrichment_validation.ipynb b/code/SoS/enrichment/enrichment_validation.ipynb
new file mode 100644
index 000000000..4e03b858e
--- /dev/null
+++ b/code/SoS/enrichment/enrichment_validation.ipynb
@@ -0,0 +1,539 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "04a8e344",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "# Enrichment and functional validation mini-protocol\n",
+ "Evaluate prioritized genes and variants using pathway, genomic-annotation, and heritability-enrichment analyses."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3f291c0c",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "#### Miniprotocol Timing\n",
+ "This is the total duration for the selected route; module-specific timings appear on their respective pages.\n",
+ "Timing: TBD"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "88fb2296",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "## Overview\n",
+ "This mini-protocol provides alternative functional follow-up routes. Step 1 calls [`gsea.ipynb`](https://statfungen.github.io/xqtl-protocol/code/enrichment/gsea.html), step 2 calls [`eoo_enrichment.ipynb`](https://statfungen.github.io/xqtl-protocol/code/enrichment/eoo_enrichment.html), steps 3\u20135 call [`gregor.ipynb`](https://statfungen.github.io/xqtl-protocol/code/enrichment/gregor.html), and steps 6\u20139 call [`sldsc_enrichment.ipynb`](https://statfungen.github.io/xqtl-protocol/code/enrichment/sldsc_enrichment.html).\n",
+ "Pathway analysis, enrichment-over-odds, GREGOR, and S-LDSC use different inputs and null models. Select the route matching the scientific question; only the numbered commands within the GREGOR and S-LDSC routes form ordered chains."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "457c0bdf",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "## Steps\n",
+ "Choose a route before running commands; the commands are not one mandatory chain.\n",
+ "\n",
+ "| Analysis goal | Commands to run, in order | Inputs |\n",
+ "|---|---:|---|\n",
+ "| Test pathway and GO-term enrichment of gene groups | 1 | `input/enrichment/protocol_example.pathway_genes.tsv` |\n",
+ "| Test annotation enrichment using variant-level odds | 2 | `input/enrichment/protocol_example.eoo_significant_variants.tsv.gz`; `input/enrichment/protocol_example.eoo_baseline_annotation.tsv` |\n",
+ "| Test overlap of index SNPs with genomic annotations using matched controls | 3 \u2192 4 \u2192 5 | `input/ld/protocol_example.index.snps.txt`; `input/enrichment/protocol_example.bed.file.index`; `input/enrichment/protocol_example.gregor_ref` |\n",
+ "| Partition GWAS heritability across annotations | 6 \u2192 7 \u2192 8 | `input/enrichment/sldsc/colocboost_test_annotation_path.txt`; `input/enrichment/sldsc/reference_annotation0.txt`; `input/enrichment/sldsc/genome_reference_bfile.txt`; `input/enrichment/sldsc/sumstats_test_all.txt` |\n",
+ "| Re-meta-analyze a selected subset of S-LDSC traits | 8 \u2192 9 | `output/sldsc_postprocess/protocol_example.sldsc_postprocess.rds`; `input/enrichment/sldsc/sumstats_test_category1.txt` |\n",
+ "\n",
+ "Run only the route appropriate for the scientific question. GREGOR steps 3\u20135 and S-LDSC steps 6\u20138 are ordered workflows; step 9 is an optional follow-up."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "29ba5b87",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### 1. [Test pathway and GO enrichment](https://statfungen.github.io/xqtl-protocol/code/enrichment/gsea.html)\n",
+ "\n",
+ "**What it does:** Maps grouped genes to ENTREZ identifiers and tests KEGG and GO BP/CC/MF over-representation for each group."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6e5ecb92",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "3599443d",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/gsea.ipynb pathway_analysis \\\n",
+ " --genes_file input/enrichment/protocol_example.pathway_genes.tsv \\\n",
+ " --name protocol_example \\\n",
+ " --pvalue_cutoff 1 --organism hsa \\\n",
+ " --cwd output/gsea"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a14ba77d",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### 2. [Estimate enrichment over odds](https://statfungen.github.io/xqtl-protocol/code/enrichment/eoo_enrichment.html)\n",
+ "\n",
+ "**What it does:** Estimates annotation odds ratios and enrichment with chromosome block-jackknife uncertainty."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9aa362ed",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f7bfbd12",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/eoo_enrichment.ipynb enrichment \\\n",
+ " --significant_variants_path input/enrichment/protocol_example.eoo_significant_variants.tsv.gz \\\n",
+ " --baseline_anno_path input/enrichment/protocol_example.eoo_baseline_annotation.tsv \\\n",
+ " --trait protocol_example \\\n",
+ " --annotation-name baseline \\\n",
+ " --cwd output/eoo_enrichment"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "2792f907",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### 3. [Create a GREGOR configuration](https://statfungen.github.io/xqtl-protocol/code/enrichment/gregor.html)\n",
+ "\n",
+ "**What it does:** Writes the configuration connecting index SNPs, annotation BED files, population settings, and the GREGOR reference database."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ee641232",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "dbc59c74",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/gregor.ipynb gregor_conf \\\n",
+ " --gregor_db input/enrichment/protocol_example.gregor_ref \\\n",
+ " --index_snp_file input/ld/protocol_example.index.snps.txt \\\n",
+ " --bed_file_index input/enrichment/protocol_example.bed.file.index \\\n",
+ " --pop EUR \\\n",
+ " --cwd output/gregor"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "744b6b9e",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### 4. [Run GREGOR enrichment](https://statfungen.github.io/xqtl-protocol/code/enrichment/gregor.html)\n",
+ "\n",
+ "**What it does:** Compares annotation overlap for index SNPs with overlap among LD- and frequency-matched control variants."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "64a0fbe7",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d02841c6",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/gregor.ipynb gregor \\\n",
+ " --gregor_db input/enrichment/protocol_example.gregor_ref \\\n",
+ " --index_snp_file input/ld/protocol_example.index.snps.txt \\\n",
+ " --bed_file_index input/enrichment/protocol_example.bed.file.index \\\n",
+ " --pop EUR \\\n",
+ " --cwd output/gregor"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "75dd60d5",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### 5. [Plot GREGOR Fisher enrichment](https://statfungen.github.io/xqtl-protocol/code/enrichment/gregor.html)\n",
+ "\n",
+ "**What it does:** Compares annotation odds ratios from two GREGOR result sets."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "67ff5909",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d3140197",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/gregor.ipynb gregor_fisher_plot \\\n",
+ " --fisher1 input/enrichment/protocol_example.trait1_enrichment_results.txt \\\n",
+ " --fisher2 input/enrichment/protocol_example.trait2_enrichment_results.txt \\\n",
+ " --cwd output/gregor"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d3b0606c",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### 6. [Build annotation LD scores](https://statfungen.github.io/xqtl-protocol/code/enrichment/sldsc_enrichment.html)\n",
+ "\n",
+ "**What it does:** Converts genomic annotations into chromosome-level annotation and LD-score files for stratified LD-score regression."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d3a1b8e5",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "c9cbb86f",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/sldsc_enrichment.ipynb make_annotation_files_ldscore \\\n",
+ " --annotation_file input/enrichment/sldsc/colocboost_test_annotation_path.txt \\\n",
+ " --reference_anno_file input/enrichment/sldsc/reference_annotation0.txt \\\n",
+ " --genome_ref_file input/enrichment/sldsc/genome_reference_bfile.txt \\\n",
+ " --annotation_name protocol_example \\\n",
+ " --plink_name reference. --baseline_name annotations. --weight_name weights. \\\n",
+ " --cwd output/sldsc_ldscore -j 4\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6a54b97a",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### 7. [Estimate stratified SNP heritability](https://statfungen.github.io/xqtl-protocol/code/enrichment/sldsc_enrichment.html)\n",
+ "\n",
+ "**What it does:** Runs S-LDSC for each trait and annotation target to estimate annotation-specific heritability enrichment."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "be08e2ff",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "42145dab",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/sldsc_enrichment.ipynb get_heritability \\\n",
+ " --target_anno_dirs output/sldsc_ldscore/protocol_example_single_1 \\\n",
+ " --all_traits_file input/enrichment/sldsc/sumstats_test_all.txt \\\n",
+ " --sumstat_dir input/enrichment/sldsc \\\n",
+ " --baseline_ld_dir input/enrichment/sldsc \\\n",
+ " --weights_dir input/enrichment/sldsc \\\n",
+ " --plink_name reference. --baseline_name annotations. --weight_name weights. \\\n",
+ " --annotation_name protocol_example \\\n",
+ " --maf_cutoff 0 --cwd output/sldsc_heritability -j 4\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "302f7aaf",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### 8. [Postprocess and meta-analyze S-LDSC results](https://statfungen.github.io/xqtl-protocol/code/enrichment/sldsc_enrichment.html)\n",
+ "\n",
+ "**What it does:** Standardizes per-trait results and computes random-effects meta-analyses across traits."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "8118eb74",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "7a141410",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/sldsc_enrichment.ipynb postprocess \\\n",
+ " --traits_file input/enrichment/sldsc/sumstats_test_all.txt \\\n",
+ " --heritability_cwd output/sldsc_heritability \\\n",
+ " --target_categories ANNOT_0 --target_categories_label protocol_example_annotation \\\n",
+ " --target_anno_dir output/sldsc_ldscore/protocol_example_single_1 \\\n",
+ " --annotation_name protocol_example \\\n",
+ " --maf_cutoff 0 --cwd output/sldsc_postprocess -j 4\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "be7580d5",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### 9. [Re-meta-analyze a trait subset](https://statfungen.github.io/xqtl-protocol/code/enrichment/sldsc_enrichment.html)\n",
+ "\n",
+ "**What it does:** Reuses postprocessed S-LDSC results to estimate enrichment for a selected subset without rerunning regression."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c1e0a694",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "c9293044",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/sldsc_enrichment.ipynb meta_subset \\\n",
+ " --postprocess_rds output/sldsc_postprocess/protocol_example.sldsc_postprocess.rds \\\n",
+ " --subset_traits_file input/enrichment/sldsc/sumstats_test_category1.txt \\\n",
+ " --subset_name category1 --target_categories ANNOT_0 \\\n",
+ " --annotation_name protocol_example \\\n",
+ " --maf_cutoff 0 --cwd output/sldsc_postprocess -j 4"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f945efcd",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "## Output Files\n",
+ "\n",
+ "| Step | Relative path | Contents |\n",
+ "|---:|---|---|\n",
+ "| 1 | `output/gsea/pathway_analysis/protocol_example.combined_pathway_results.rds` | Standardized KEGG and GO enrichment results for all gene groups |\n",
+ "| 2 | `output/eoo_enrichment/enrichment/protocol_example.baseline.enrichment_results.rds` | Odds ratios, enrichment estimates, and block-jackknife uncertainty |\n",
+ "| 3 | `output/gregor/protocol_example.index.gregor.conf` | GREGOR configuration |\n",
+ "| 4 | `output/gregor/_gregor_output/StatisticSummaryFile.txt` | Raw GREGOR overlap statistics |\n",
+ "| 4 | `output/gregor/_variant_counts.txt` | Parsed annotation overlap counts |\n",
+ "| 4 | `output/gregor/_enrichment_results.txt` | Fisher-test enrichment estimates |\n",
+ "| 5 | `output/gregor/_vs__enrichment.pdf` | GREGOR odds-ratio comparison |\n",
+ "| 6 | `output/sldsc_ldscore//*.{annot.gz,l2.ldscore.parquet,l2.M}` | Annotation and LD-score products |\n",
+ "| 7 | `output/sldsc_heritability//.results` | Trait- and annotation-specific S-LDSC results |\n",
+ "| 8 | `output/sldsc_postprocess/.sldsc_postprocess.rds` | Per-trait results and cross-trait meta-analysis |\n",
+ "| 9 | `output/sldsc_postprocess/*` | Trait-subset meta-analysis tables |"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b30bbec7",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "## Anticipated Results\n",
+ "\n",
+ "Pathway analysis summarizes biological processes represented by prioritized genes. Enrichment-over-odds and GREGOR test whether prioritized variants overlap functional annotations more than expected under their respective background models. S-LDSC tests whether GWAS heritability is disproportionately concentrated in annotations while accounting for LD.\n",
+ "\n",
+ "Interpret enrichment in light of the selected background, annotation coverage, population-matched reference data, multiple testing, and uncertainty. Enrichment supports functional relevance but does not by itself validate a causal gene, variant, or mechanism."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9da187fe",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "## Command interface"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d0ad6a66",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/gsea.ipynb -h"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "ee662ec8",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/eoo_enrichment.ipynb -h"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "044f27f1",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/gregor.ipynb -h"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8859ff76",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/sldsc_enrichment.ipynb -h"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "SoS",
+ "language": "sos",
+ "name": "sos"
+ },
+ "language_info": {
+ "codemirror_mode": "sos",
+ "file_extension": ".sos",
+ "mimetype": "text/x-sos",
+ "name": "sos",
+ "nbconvert_exporter": "sos_notebook.converter.SoS_Exporter",
+ "pygments_lexer": "sos"
+ },
+ "sos": {
+ "kernels": [
+ [
+ "SoS",
+ "sos",
+ "sos",
+ "",
+ ""
+ ]
+ ],
+ "version": ""
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
\ No newline at end of file
diff --git a/code/SoS/enrichment/eoo_enrichment.ipynb b/code/SoS/enrichment/eoo_enrichment.ipynb
index dc7a9fc08..c5eec7bc2 100644
--- a/code/SoS/enrichment/eoo_enrichment.ipynb
+++ b/code/SoS/enrichment/eoo_enrichment.ipynb
@@ -6,16 +6,9 @@
"kernel": "SoS"
},
"source": [
- "# Chromosome-Specific Enrichment Analysis of Annotations Using Block Jackknife"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "## Description"
+ "# Chromosome-Specific Enrichment Analysis of Annotations Using Block Jackknife\n",
+ "\n",
+ "Tests how strongly a set of significant variants overlaps a genomic annotation relative to the genome, with block-jackknife standard errors."
]
},
{
@@ -24,7 +17,13 @@
"kernel": "SoS"
},
"source": [
- "Chromosome-specific enrichment analysis for genomic annotations using a block jackknife. For each annotation it computes an odds ratio (OR) and an enrichment statistic, leaving out one chromosome at a time to obtain block-jackknife standard errors. This quantifies how strongly significant variants overlap each annotation relative to the genome.\n"
+ "## Overview\n",
+ "\n",
+ "A variant set that overlaps an annotation more often than chance would predict is evidence that the annotation marks functional sequence. Counting the overlap is easy; putting an error bar on it is not, because variants are correlated along the genome and so cannot be resampled independently.\n",
+ "\n",
+ "This module computes an odds ratio and an enrichment statistic for each annotation, then leaves out one chromosome at a time and recomputes, using the spread across those 22 leave-one-out estimates as a block-jackknife standard error. Blocking by chromosome keeps correlated variants together rather than splitting them across resamples.\n",
+ "\n",
+ "**When to run it.** Run this module after chromosome-level enrichment inputs are available when a combined enrichment estimate and block-jackknife standard error are needed.\n"
]
},
{
@@ -36,7 +35,7 @@
}
},
"source": [
- "## Definitions and Test Statistics\n",
+ "### Definitions and test statistics\n",
"\n",
"### Odds Ratio (OR)\n",
"Strength of association between significant variants and an annotation:\n",
@@ -55,8 +54,7 @@
"### Standard Error (LOCO block jackknife)\n",
"Standard errors are estimated by removing one chromosome at a time and recomputing the statistic, capturing variability due to genomic structure. With $\\theta_i$ the statistic ($OR_i$ or $\\text{Enrichment}_i$) excluding chromosome $i$, $\\bar{\\theta}$ its mean, and $N$ the number of chromosomes (22 for autosomes):\n",
"\n",
- "$$\\text{SE}(\\theta) = \\sqrt{\\frac{\\sum_{i=1}^{N} (\\theta_i - \\bar{\\theta})^2}{N \\cdot (N-1)}}$$\n",
- ""
+ "$$\\text{SE}(\\theta) = \\sqrt{\\frac{\\sum_{i=1}^{N} (\\theta_i - \\bar{\\theta})^2}{N \\cdot (N-1)}}$$\n"
]
},
{
@@ -68,14 +66,14 @@
},
"outputs": [
{
- "output_type": "display_data",
- "metadata": {},
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAACEkAAAaHCAYAAACDgqBnAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAXEgAAFxIBZ5/SUgABAABJREFUeJzs3Qd4XGeZ9vF7inrvlmzZcu/djhM7vfeEJISWQGBZOsvC7sLusvXbZQsLy9JDCYQACYGEBKd3J7ZT3HsvsixZvXdpyne970iyyows2erz//k618ycc2bm6MxodDzvfZ7H4ff7/QIAAAAAAAAAAAAAAJjgnKO9AQAAAAAAAAAAAAAAACOBkAQAAAAAAAAAAAAAAAgLhCQAAAAAAAAAAAAAAEBYICQBAAAAAAAAAAAAAADCAiEJAAAAAAAAAAAAAAAQFghJAAAAAAAAAAAAAACAsEBIAgAAAAAAAAAAAAAAhAVCEgAAAAAAAAAAAAAAICwQkgAAAAAAAAAAAAAAAGGBkAQAAAAAAAAAAAAAAAgLhCQAAAAAAAAAAAAAAEBYICQBAAAAAAAAAAAAAADCAiEJAAAAAAAAAAAAAAAQFghJAAAAAAAAAAAAAACAsEBIAgAAAAAAAAAAAAAAhAVCEgAAAAAAAAAAAAAAICwQkgAAAAAAAAAAAAAAAGGBkAQAAAAAAAAAAAAAAAgLhCQAAAAAAAAAAAAAAEBYICQBAAAAAAAAAAAAAADCAiEJAAAAAAAAAAAAAAAQFghJAAAAAAAAAAAAAACAsEBIAgAAAAAAAAAAAAAAhAVCEgAAAAAAAAAAAAAAICwQkgAAAAAAAAAAAAAAAGGBkAQAAAAAAAAAAAAAAAgLhCQAAAAAAAAAAAAAAEBYICQBAAAAAAAAAAAAAADCAiEJAAAAAAAAAAAAAAAQFghJAAAAAAAAAAAAAACAsEBIAgAAAAAAAAAAAAAAhAVCEgAAAAAAAAAAAAAAICwQkgAAAAAAAAAAAAAAAGGBkAQAAAAAAAAAAAAAAAgLhCQAAAAAAAAAAAAAAEBYICQBAAAAAAAAAAAAAADCAiEJAAAAAAAAAAAAAAAQFghJAAAAAAAAAAAAAACAsEBIAgAAAAAAAAAAAAAAhAVCEgAAAAAAAAAAAAAAICwQkgAAAAAAAAAAAAAAAGGBkAQAAAAAAAAAAAAAAAgLhCQAAAAAAAAAAAAAAEBYICQBAAAAAAAAAAAAAADCAiEJAAAAAAAAAAAAAAAQFghJAAAAAAAAAAAAAACAsEBIAgAAAAAAAAAAAAAAhAVCEgAAAAAAAAAAAAAAICy4NYb5fD7VN7aorqFJza1t8np9dvKP9oYBAAAAAAAAAAAAAAA5HQ65XE65XS7Fx0YrMSFGMVGRcjgcY3LvjJmQhN/vV1Vtg04Wlqm8qk51Dc2qa2y28wEAAAAAAAAAAAAAwPgQ4XYpMT5WSQmxyslM0fTJmYqJjtRY4PCPcgqhsqZeJ06X6kRhmWrrm0ZzUwAAAAAAAAAAAAAAwBAzRSWyM1I0Izdr1AMToxaSaGhq0bu7jthwBAAAAAAAAAAAAAAAmPjcbpdWzJ+uxXOm2jYdEz4k4fX6tPdIgXYcOCGP1zeg+0RGuBUbHSWX2ymX0zlme5cAAAAAAAAAAAAAABA2/JLP75PX51d7u0eNzS3y+QYWQTCtONatmKspWWmasCEJUz3ihbd2qrquMeQ6JgwxZVKaUpLilRAXo/jYaEVFRozUJgIAAAAAAAAAAAAAgPPg8/vV3NKq+sYWNTQ2q6yqVmfKqmwxhVDmz5isS1fOG7FiCSMWkjABiWc3bFddQ3PQShHTp2QpNztdqUnxVIoAAAAAAAAAAAAAAGAC8Hi9Ki6rVkFxuQpLKoOuMycvW1esXjAiWYERCUn0F5CYmTtJi+dOo1oEAAAAAAAAAAAAAAATWGVNvXbsP66q2oZRC0oMe0iird2jp17ZotqGph7zTSuNi5fOUWpywnA+PQAAAAAAAAAAAAAAGCP8fr9OnC7VjgPH5fP1jCssmp2rtcvnDuvzO4f10SXtOHAyaEDiqjWLCUgAAAAAAAAAAAAAABBGHA6HZk6dpHXL58vp7Fk1Yt/R0yqrrB2/IYnqukbtPVIQNCAREx05nE8NAAAAAAAAAAAAAADGqJys1KBBic07DvepMDEuQhKmRMbmHYfsZdeTORy6dOV8AhIAAAAAAAAAAAAAAIS5nKxULZo9rce88uo6HTpZNP5CEqdLKnWmrLrHvLkzJisxPna4nhIAAAAAAAAAAAAAAIwjc6bn2I4U3W3de1xer298hSSOF5T0uB0bHaUFM3OH6+kAAAAAAAAAAAAAAMA443I6tXLhzB7zWtvaVVRaNX5CEibRkX+mvMe8udNz5Ha7huPpAAAAAAAAAAAAAADAOJWVnqz0lMQe846fLh0/IYnC0kq1t3t7zJuSnT4cTwUAAAAAAAAAAAAAAMa53ElpPW6fOlM+LC03hiUkcaJXosMkPky7DQAAAAAAAAAAAAAAgN56F15oa/fYAg3jIiRRWlnb43YuVSQAAAAAAAAAAAAAAEAIpvBC75YbpRU9swdjMiTh8/lU39jSY15acsJQPw0AAAAAAAAAAAAAAJhA0nplC2obmsZ+SMIEJPx+f495CXExQ/00AAAAAAAAAAAAAABgAomPi+5xu248hCR6JzkiI9x2AgAAAAAAAAAAAAAACCUhtmcBhtqG5j5FGsZcSKKuvmdIgioSAAAAAAAAAAAAAABgsJUkPB6vmlvaNKZDEo3NrT1ux8VEDfVTAAAAAAAAAAAAAACACSY2OkoOh6PfDMKYC0l4fb4et91u11A/BQAAAAAAAAAAAAAAmGAcDofcLme/GYQxF5LweHtuoMs55E8BAAAAAAAAAAAAAAAmIFevjIHH6x3Sxx/yBIPf7+9xu3cpDAAAAAAAAAAAAAAAgGAczp4ZA7+vZwbhQlHmAQAAAAAAAAAAAAAAhAVCEgAAAAAAAAAAAAAAICwQkgAAAAAAAAAAAAAAAGGBkAQAAAAAAAAAAAAAAAgLhCQAAAAAAAAAAAAAAEBYICQBAAAAAAAAAAAAAADCAiEJAAAAAAAAAAAAAAAQFghJAAAAAAAAAAAAAACAsEBIAgAAAAAAAAAAAAAAhAVCEgAAAAAAAAAAAAAAICwQkgAAAAAAAAAAAAAAAGGBkAQAAAAAAAAAAAAAAAgLhCQAAAAAAAAAAAAAAEBYICQBAAAAAAAAAAAAAADCAiEJAAAAAAAAAAAAAAAQFghJAAAAAAAAAAAAAACAsOAe7Q0AAAAAAACYCApOF+qOD9yv9vb2rnkx0dF68U9/UFpqikbbotWXBp3/iwe/p4tWrrjg9TExtLW16ab3fUClZeU95j/8kx9o1Yplo7ZdAAAAADBUCEkAAAAAmLA8Ho927dmnbTt2afuu3XbAp7a2TrV1dYpwuxUXF6fJOdmaNWO6Vq5YqnUXrxkTA5kDcf3t9+hMccmwPHZO9iS9vP6JYXlsjG1+v1/vbtmmNzZu1v4Dh3SmuFgNDY1qa29XVFSk4mJjlZGepsyMDE3Pm6YZedO0cME8+zvkdFKs8pvf+X6PgIRx7913Dvpz5Ze/flTf/t6PQi7Pm5qrZ598TBPJcL73Hvj0F+zfgWCmTM7RM088av8m9CdUYOTf/+nvdedtNw/6eXtzuVyKj49TUmKiZs+coaWLF+qm669R9qRJGmmRkZH6xEc/ov/81v/1mP9f3/6ufv/rh/hdBwAAADDuEZIAAAAAMOE0t7Toyaef0a9++zsVl5SGDFCY9SoqK7V77z49+adn7MDPukvW6BP3f1irVy4f8e0GfvjTh/Tjn/2yz44wZ2+bs7iH05btO/T//uN/lF9wOujypqZmO5VXVOrAoSPasHFz17K4uFj94de/0NTcKQrHfWeYwfDu+8Rwu9164L4PDvqxnnnhpX6Xm9fIBMCWLVmkiWA033uFRWfs34sP3vM+jSav1xsI8dXW2Yokr214S//3w5/ouquv1N/99V8qPS11RLfnnjtv04M/f1jVNTVd8w4dOapnnn9Jd9x604huCwAAAAAMNU7zAAAAADCh7N1/QHfce5894zVUQCIUn8+njZvf0a8f+/2wbR8wFv3+j0/rzz77pZCD1OfS2Nik1tY2hbMf/OTnfebdeN01ykhPH9TjHDp8VEeOHj/neuuff1ETwVh47/3koYfV0tKqscb8TXrp1dd15wfut++LkRQVFaX333V7n/k//vkvbcgQAAAAAMYzQhIAAAAAJoyn1j+nj/7554etDQUwEZkKCP/+3/9r2x3g/GzfuTtoW4XzqU4w0PDDi6+81qe1x3gzVt57pkLFo78fuy2Gampr9akvflkVFZUj+rz33nVnn9YapvLGcy+9MqLbAQAAAABDjXYbAAAAACaEtza/o3/5j2/akuWhJCcl6eKLViknO0sx0TF24KnoTLF27NqtuvoGjSemX3xDQ+htNgNZTzz9TNBl119zpRbMmxvyvvHx8UOyjRgf/uf/fmDPWA9mwbw5WrxwgdLT0uztuvp6FRUX27PaCSOd9cijj/fZd9OnTR10Owzz+fX8S68OaN26unrbdsK0YxivxtJ776FHfqv333WHEkbg8y8xId5+hncyGZGq6mptfneLTpzMD3qfquoafffHP9W//ePfaaRMysrUmtUr9c57W3vM/81jf9Adt9ByAwAAAMD4RUgCAAAAwLhXUFikv/n6P4cMSKSmpugvP/9p3XnrzX3OijXMIN27W7fr0cef6NHrfiw71xnqW7bvCBmSuHzdWt15283DtGUYb787+w8e6jM/KipS3//2f2vtmtUh71tcUqINb23W8y+/qp279ypclZaV6423NvWZf/MN1w36scwgeUXlwKsFrH/uxXEbkhhr773a2jr98teP6i8++ykNNxNE++QD9/eZbypq/PAnD+nBhx4Oer/nXnxFf/fXf6nYmBiNlFtvvL5PSOLg4SPatWffoENAAAAAADBWEJIAAAAAMO794MGf2b70wUzJydbPfvh/yp0yOeT9TXDCDMiZaev2nXr7vS1Dtm2LVl8adP4vHvyeLlq5QuNBu8ej48dPav+hwzp46LCOn8xXWXm5qqpq1NLaKr/Pp9i4WCXGx2v69DwtmDtH111zpebNmT0k+6ihodG2IHj1jTd1quC0Kquq5fF49O//9Pchwx6mjL9pv7J9126VV1TYyiHmrOgrLl2r991+i6ZMzrHrXX/7PUHPSu/vsbszZ7WbAXLzfIVnztjqJO3tHqUkJykne5LWrF5lK3fMmTUz5GP88KcP6cc/+2W/z2MeP9R+eulPf9DknGydj117gg8wmwH+/gapjexJk/She++207HjJ5SRkT6o5x7v+67Tcy++HLQawtVXXjboxzKhh2DWrFqp97Zt7zN/49vv2goEqSkpGm9G870Xym9+9wd95APvV1rq6OxPh8OhL3zmk3pny1bt3ru/z/K2tjb7fr583SVBAxbvbd1uf6cOHTmq04VFqm9oVGtrq6IiI5WQEG8/A3OnTNHcObO0eMF8LVm0QFFRUf1uk/nMdLlcfUKIzzz/IiEJAAAAAOMWIQkAAAAA45oZIHvxldeDLnO73fr2f/1bvwGJ3lavXG4nnPXRT35We/cfPGfpfzMVninWxs3v6Ce/+JVWLV+mf/2Hr2na1Nzz3p1mQPDv/vnfVFxSOqD1W1pa9a//+U098/xLPea3trbZQXgzeGhaI3z5i5/Vh++9+4Led9/8vx/o7Xe3hKwuYCZzlvtPHnpYN153tb72lS8pPS1VY0lFZVXQ+YM9U33WzBlht+86vfjKa33mZWVmaO7sWYN6nPqGBr3x1sagyz7zyQdUXVujI0eP95hvwkKmPcd9H3y/xpvReO+dS1NTs376i1/Zag2j6YrL1gUNSRjBQl3HT5y0n5MHDh0Jep/mlhY7lZVXaM++AzbYY0RGRurxX/1cs2eF3ofJyUlatGC+du/d12P+K69v0Ne/+pWg1ZkAAAAAYKzjfzIAAAAAxjUTkAjV095UDFg4f96Ib9NE4w2xf89l285dev/9n7DBhPOxd98BffZLfz3ggIQ5y/qLf/23fQISvZnBwv/4n+/YIMf5MCX+P/TAp0IO8gc7w/uFl1/Thz/+KR09dkJjiakCEur3qrDozJA/30Tad50D/QcP931/m4DQYL30yus2zNNbRnqaVi5fqhuuvTro/UyVlfFopN97wcwOUqXkD0+tt+08RlNqcnLIZXX19T1um8/Hj37q8yEDEuf6zGxt6/ue623Vir7v56rqGu0/eHjQzwkAAAAAYwEhCQAAAADjWn+tMT5w150jui0Ifmb2X/7N39uWHYP13R/91AYaBup7P/6Z3nlv64DX//6Pf2YrFgzGprff1d/9078Naru6nwH+mS/9lW2PMFakp6cFnV9ZVaX3ffCj+vq/fMNWShiKQeOJtu+MLdt32CBHb0sWLxz0Y4UKO1x/zVX2bP2brrsm6PIDBw/bSgLjzUi+90K55srLtWD+3D7BgR/+9BcaTZX9vM8TExJ63P7ej3+q2tq6Yd2epSHez1uCtIABAAAAgPGAdhsAAAAAxi0zmBXqTNakpETbdx1DJzoqSnPnzLbtSzIz0hQTHSOny6mmxiYVFZdo6/addoCzN9OCw/Svv+v2Wwf1fJ0VQhwOh22BsnjBfEVEROhMSYk2v9MzHGMGiU0bjf7OGL9s7RpFuCO0a88+vdcxuOf1ege8PTU1tfqbr/9LyPuYs/3nz52jqKgoHTl23IYCeg+gm1DGP/6//9QPv/PNrnmXXLRKUZGR9vq7W7bp3a19Bx6n5GTrnvfdHvR5ExN7DpoOxsplS0MuM2GGPz33gp2M1NQUW5ll2ZJF9vVYumihXC5X2O47Y3+INjT9tS8IxlROMO1Fgrnp+mvt5dTcKXYfHTzct2LA+udetC1kxpOReu/1x+GQvvTZT+nTf/FXPeabajSf+OhHNCNvmkbDhrc2hVyWkz2p67r5HXn9zeAtWmZOz9Oa1Sttuwyf12crUOQXnNbRY8dVXlE5qO2ZHaKlyb4Dhwb1OAAAAAAwVhCSAAAAADBumXLfoQZd586eZQfXceFuu/EGffUvv6ilSxYpwh36v5Ht7e36f//1LT21/rk+y157461BhySM5KQkfe9b/6EVvQZUPR6PGpuaum7/9vdPhmy78sB9H9JXvvhZezZ+90HQr//rN0LeJ5hf/Pq3qm9o6DM/Jjpa3/vWf+qSNat7zN+xa7c+/+Wv9bnPm5vetoOLixYEWsGYn63z5zOl74MN9E+alKVPPnC/htqUyTm69JI12vTOe+dct6qqWhs3v2MnIy01VbfeeJ0euP9DykhPD7t9ZxwNUcFhWm7uoKtIBKtIkT0pq8dZ/Dded3XQkMSzL76iL33+0z3e42PdSL33zmXdJWtsO4ltO3Z1zTN/V37w4M/0v//17xpJ5j3ww588pL0hwjcmJLZq+dnPwuqaGjU2nv0c7HTDtVfpW//x/0L+DTyRf8oGkZ598WUbFBnIa+V2u+3nbnfHjo+9FjgAAAAAMBDj53/PAAAAABDk7PRQkhIT2V9D5P4P32sHEfsLSHQO4H38/g8HXbZ77/7zeu7/+ca/9AlIGGbArvtr/Oobbwa9/4J5c/RXf/G5PoPHt918g+687eZBDV4+/czzQZd99ctf7DPIb5jt/uyffzzofR5/8imNFV//6lfOq6KCqRryq0cf1013fkCPP/l0WO67M2eK+8wzFQ7S01IH9TgmtBPMDdde3WOg29wOprSsTO8FCYiMdcP93huoL33u033mvfL6m9p/cHgqJTQ0NOjnD/+6a/rZL3+tb37n+7rzA/frwYceDnm/W264TrGxsV23g+RqrEUL5vcbEjQVMj764Q/o9488ZCt0nIt5rIwg7VFMVR8AAAAAGI+oJAEAAABg3GppbQ25LLKjBP9o27c1dNn08eZkfoHeeGujDTzknyqwlTyampvU2tp2zvvW1NaqtbXVtlMYqBXLlgQdQO+t4HShPdM8mLvvvD3kYOH733eH/vinZwe0LYeOHLU/b7D32c03XBfyfpdcFHz7t2zbobHCtE/51U9+qC999e/tvjyf38N/+69v2dfXDLyG076rDPK+S4iPH1QrCFM143RhUdBlN153TZ8z+s0A+L4DB4NWoxjI78tYMtzvvYFavnSxrrh0ra1U0j3c890f/VQ//f7/aqjV1Tfo/374k0HdJyU5WX/xuU/1mJeclKi4uNg+1SR+9stH7O/XRStXaGru5EF97vZX1ae4pLTHPPPZ39DQqPj4uAt+fAAAAAAYSYQkAAAAAIxb/VWLCFbaH+fHDIz917e/q9c2vHVBu7Cuvl4Zgxisu2ztJQNaL9QAs7Fk0YKQy+bPnW2rX5g2Iedy5NjxoPPb2tq05srrNVhFZ4pVVV2t1JQUjQWzZ83Q0797RI/+/kn97omnVFh0ZtCP8Z0fPKjrrr7StogIl33X3NzcZ95gB6TXP/diyABBZ1uR3q0UgoUkXn3jLf3j15p6VBoYD4bzvTcYJoDw1uZ3erQ9efvdLdq6fadWr1yu0ZSUlKiffP/byszo2VrEhHFMuOP5l17tE8L4z2/9n71uQmKTsjJt9YhZM6Zr2dIlumjlcvuYgxEdHfx93dzSTEgCAAAAwLhDuw0AAAAA41ZqSnLIZSWlPc94xfkxA5b3/dlnLzggYbS1nTuM0N30vKkDWq+2vj7ksrTU0G0PerfsON/WLuerOkh1hdFkzjx/4L4P6YWnHtdjD//UDhqvvfgiWxlhIEzY5I/rnw3LfddTiB4IIYIiL7/2RtBlN153dcj5waqjmMCGaRExHg3Xe28w5s6epZuu71m5wzDVJEaLaRNkwh8mRLJg3tyg63zhM3/eb+DBhD5M0G3zu1tsi5Ivf+3ruuLG2/WXX/26TuSfGvC2hGrtEWo+AAAAAIxlVJIAAAAAMG6ZXvamBHl1Td8B0+Mn8m0JclOKHOfPlLIvLSsblV040AFShxyhl4VeNKDlnVrbzt1SZLBq6+o0FpkB+MULF9jpUx//qHw+n46dOGnPqH/l9Q3atmNXyPvu3L03rPZdTExMn6o1A2k/0+n1Nzfas/6Dqaqq0c8f/nXQZSbcY1rYBKtKccetN2m8Gur33mB94dOf1MuvviGP19s1b9eevdqwcbOGm6kKYf5emdfWVNdYumihbrr+WuVkT+r3flNty5If6J+/8d+2FdJAeDwevfrGm7ZyxoPf/ZYuWrXinPcxLU2CiYmJHtBzAgAAAMBYQkgCAAAAwLgeULtkzao+pcYNr9drB9XuvO3mUdm2iVJFwpx9HMzM6Xn69Ccf0PIli5WenqYIt7vrPjfeee+QPL/D6RhwWCaUquoaZaT3LFHffaCwpnZgg+0DrTgxGN0HYscyczb7nFkz7fSRD9yjDW9t0l/8zd/bAezeyssrwmrfpaQk9wlJmNvm88cMep/L+udfCrnsyT89M+jt2bpjp60acCFtJybSe2+wpuZO0fvuuFV/+OOfesz/3o+HtpqECT68vP6JIXu8WTNn6Le/+In27Nuv1zZs1Padu3X8xMlztp0ylUy+/q/f0PN//J1tPdSfYKEcUwEkPi7ugrcfAAAAAEYaIQkAAAAA49qlay8OGpIwTGnxW266vmsAH4NjBtqCiY6K0i8e/L7SUlP6LAtW1WO45U6ZHHLZwUNHbBn9YA4dOWbL9A9EZoigRXJSkh6474M6H7mTczQeXXn5pVq7ZrU2vfNen2X+MNt3k3OyVXC6sMc8E5CoqKxSVmZGv/etrKrW20H24YUw4YFnX3hZf/7x+zURDfa9dz4++8mPa/1zL/SoCHLk6HGNB0sWLbRTJ/M+PF1YZNtqvLtlq1585XXbfqM7E6rZsWuP1qxe2e/7qryiss/87EmZQVu/AAAAAMBYxzeFAAAAAMa1G6+9Wt/70U9VUtq3JcTRY8f1gx//TF/+4mcH9ZhHjh23Zy4PhUWrLw06/xcPfk8XrTx3ifPRVFHZd1DMmDEjL2hAwtj49rsaaabcfFpqqiqrqvosW//8iyGriTzxVM+zxfuzYtkSe1Z77zPYGxoadOdttyg9LVVDIdTZ3M3NLRoOR4+dsNU/rrx83aAGO9PT0oLOT01ODpt9Z8yaMV3vvLe1z/xTp0+fMyTx/IuvDEtFDPOeHw8hiZF4752PzIx0fej9d+vh3zym8c78bplp+dLFuvuOW21gyYQHezt6/ES/IYnCM8W28k5vs2bMGPJtBgAAAICR4GQ3AwAAABjPTLnvz/zZAyGXP/TIb/Wd7/9Y7UEGeHo7ePiIPvm5L+kHD/58iLdyfArVLsAMbDa39B14NmcrP/LbvgNwI+G6q68IOn/Lth365a8f7TP/+Zde0R/XPzfgx09KSuxxhnYnM8j9n9/6jq0eMBCnCk7b9+OGjZuDLk8IUbo+v6BAdfX1GmqlZWX64l//re75yMf11Prn1NjYdM771NbVafO774UM0ITLvjMWLZgXMgBwLn967oVh2CLpZP4p7d1/QGPdSLz3ztcnP3bfuGkj8Zvf/cF+9g5EfHz8eQWJTOAwmEUL5g/oeQEAAABgrKGSBAAAAIBx7647brUDp6EGT01Q4tUNb+rD996jtWsuUk52lqKiouzAadGZYttW4tXX39S2nbvs+ldfcdkI/wRj05QQLQ3q6ur1la/9g77yxc9pxvRpdoDt1Tfe1Le/9yM1NDZqNHzo3rv1h6fWBx1wN9v18mtvaM3qVXK7XNq1Z6/e3bp90M/xyQfu0xe+8rU+81969Q0VnSnRRz/yAa1avkwZ6Wn2zPimpiaVlVfo6PGT2rt/vzZufteesW38e960oM+RN21q0PlmAPn+P/usrrxsnRISzg50zp87R+suWaMLdfjoMf3jv/2nvvHN/9Xll16iVSuWa+nihcrJnqSkxERbCaKiotLutwd//sugpfeNyy65OKz23UWrgleD2bPvgD7ygdD3MyGKQ0eOBl32zX//Z918w3XnfG5TmeOqm+4MXkHluRe1eOECjQfD/d47H8nJSfrYRz6oH/70IY11Tzy1Xv/17e92vZ+XLlpoP5czMtIVGxNjPxPNPnttw1v6xSO/DfoY56rmsmfv/qDz16we29WQAAAAACAUQhIAAAAAxj0ziGYGFj/6558POfB4qqBQ//mt/+tRJWGgZ7CHK1N+PToqSi2trUHbapipdxuFYG0VRsLM6Xn66Ic/ELRqhLF3/0E7dWcG491ut9rb2wf0HGaQ/YpL1+rNTW/3WbbvwEF99ev/0vW4Zj+cz/vLDGyH2ufHT+bbqbv333XHkIQkOpnnffm1DXbqZH4W41yvq2k9YQa5w2nfZaSn28FpU4Wmu+0dgavBVpGIiorUFZeuG9Bzm/10zVWX6/dPPt1n2Qsvv6avfuUvFOEeP1/7DNd773x97CMf0GN/eFJV1TUaD8x7sPf70Ow/v99vp1DM71x/rTaMzgBhd8lJSVSSAAAAADBu0W4DAAAAwIQQGxurX/7k+7r6yoFVgSAgcW4J8fG670P39rtO78HLP/vYRzRavviZT+rii1YNeP2//+u/tJULBtNq5H++8S92ULw/ZkDyfN9f8fFxuv3WmzSWmNf4XIPUMdHR+pevfy3kfpvI++6Ga6/qM6+ktEzHOipf9GZ+PtPuJRgT2oiLix3wc193VfA2MzW1tXorSCBlvBmq9975/k355AP3azwz+66/gIRxxy032aodoVTX1GjfgUN95l9/7VVdIRYAAAAAGG/43wwAAACACTWo/73/+U99/W++rPS04IPfA5EeYuA8HH3+03+mtRdfNKB1//zj9+ueO2/XaImMjNQPvv3fuu3mG/pdzwxCf+Ofv25bdDQ2NQVdJyU5KeTA6SM//5Hed/st572dsbExIcMZxl998XNasWzJeT/+oLYlLtbutwuRlZmhH37nm1q2ZFH/zzXB9l2nW268Puhg8Wtvbgy6/rtbttlWIsHccE3fwEV/Vq9cbs/oD8a03BjLRvK9d74+eM/7NCkrUxPV5Zeu1d9/9cv9rvPmxreDBpduu6n/z1kAAAAAGMvGT91FAAAAABggM/h995236U/PvqDnX35Ve/btV2trW7/3yZ6UpcvWXaI7b71JSxYtZF93MOX6f/Sdb+rBh36lXz/2uBob+4YK8qbm6nOf+oRuvuE6FZ0pHtV9Fx0dpf/813/U3Xfcpj+uf1Y7du5WeWWlYqJjlJOdpSsvv1R33X6rHfgsLStXbW1d0MfJyEjv98z1f/vHv9MH7n6ffv3o43pj4yY1NTX3u12pqSm6aOUK23LCtEiIjYnpN8Txix9/Ty+9+rpeeu0NHTp8VNXVNWpuadFQW7F0iTa98qw2v7tFb7+7Rbv27LNtKQbSMmV63jTdfvON+tD777JVHAZiIu277p8dpp3I671CEc+/+Io+/YmPDTi8YAIDV1526aCe27SLufLydXr6mef7LHtr8zuqqalVcojAz2gb6ffe+TCvyWf//BP653//L41VX//qV2wbm63bd+rIsePnbB9kAj1LFy/Uh++9Rzded7Vtt9GfZ198uc+8ubNnafnSxRe87QAAAAAwWhz+c9XdG6Q3tx7Q4ZNnum7PycvR8gUzhvIpAAAAAGBQzKCRKRduSuDX1tWprr5ebpdLcXFxypmUpZkzpvdbbhwBTU1N2rpjl04VnFZbW5ut1jFzRp4WL1wwLnfRt7/3I/3y14/2mW/OzH/r5WcGXErenGVtBiePn8i376+GhgY7uBpv3l/Zk+yA7nh6fzU1N6vgdKENvJRXVKq5udmGjKKiAj+TCQXMnTNLGemhgyQDNVH23bYdu/TAp7/QZ/5jD/903P5+TPT33kT9W3ci/5Tdf2Vl5bZSTnu7x4bH4uPjNXXKZM2ZNXPAwRnzN/P62+/pE1z593/6e915283D9FMAAAAAgLT+9S1qbjl7wtNNly1TbvbQ/V+QShIAAAAAJryIiAjOeh0Cpl2COZt/LPvnf/9v3XzDtVqxfKmtghFqIPGXv3lMD//msaDLr7ri0gEHJAyXy6X5c+fYaSIwlRrmzZltp+E2UfbdqhXLtGr5Mm3buavH/Mf+8EdCEmP0vTdR/9aZKg9mGgqPP/lUn4DElJxs3XLT9UPy+AAAAAAwWghJAAAAAAAmjKeffV5P/ukZxcbGaNGC+Zo+bapSkpMVERmhpsYmnTpdqC3bd6iurj7o/U0Vg8/82QMjvt0Y/z7/6T/Txz/zxR7zXnj5NX3li59TelrqqG0XcD5aW1v1xFPP9Jlv2o+ECqABAAAAwHjB/2oAAAAAABNOU1OztmzbYafB+OpffkGTc7KHbbswca1euVxXXrZOGzZu7lG15Fe//Z3+6i8+N6rbBgzWE08/o+qamh7zTIWP226+gZ0JAAAAYNwbeP1QAAAAAAAmKLfbra9++Yv64PvvGu1NwThm3kOm5UF3v/vDH1VZVT1q2wQMVltbm37xyG/7zP/bv/rSoFoRAQAAAMBYRSUJAAAAAMCEkZyUpMqqqgGv73A4tGb1Sv31lz5vz5IGLsTU3Cna+fYb7ESMa6bt0GvPPTXamwEAAAAAw4aQBAAAAABgwnj9+ae0Y9cebd2xU3v3H1RhYZHKKyrV0tIil9ul+Lh4JSclavasmVq0YJ6uufJy5U6ZPNqbDQAAAAAAgBFCSAIAAAAAMGG4XC6tXrncTgAAAAAAAEBvNBIEAAAAAAAAAAAAAABhgZAEAAAAAAAAAAAAAAAIC4QkAAAAAAAAAAAAAABAWCAkAQAAAAAAAAAAAAAAwgIhCQAAAAAAAAAAAAAAEBYISQAAAAAAAAAAAAAAgLBASAIAAAAAAAAAAAAAAIQFQhIAAAAAAAAAAAAAACAsEJIAAAAAAAAAAAAAAABhgZAEAAAAAAAAAAAAAAAIC4QkAAAAAAAAAAAAAABAWCAkAQAAAAAAAAAAAAAAwgIhCQAAAAAAAAAAAAAAEBYISQAAAAAAAAAAAAAAgLBASAIAAAAAAAAAAAAAAIQFQhIAAAAAAAAAAAAAACAsEJIAAAAAAAAAAAAAAABhgZAEAAAAAAAAAAAAAAAIC4QkAAAAAAAAAAAAAABAWCAkAQAAAAAAAAAAAAAAwgIhCQAAAAAAAAAAAAAAEBYISQAAAAAAAAAAAAAAgLBASAIAAAAAAAAAAAAAAIQFQhIAAAAAAAAAAAAAACAsEJIAAAAAAAAAAAAAAABhgZAEAAAAAAAAAAAAAAAIC4QkAAAAAAAAAAAAAABAWCAkAQAAAAAAAAAAAAAAwgIhCQAAAAAAAAAAAAAAEBYISQAAAAAAAAAAAAAAgLBASAIAAAAAAAAAAAAAAIQFQhIAAAAAAAAAAAAAACAsEJIAAAAAAAAAAAAAAABhgZAEAAAAAAAAAAAAAAAIC4QkAAAAAAAAAAAAAABAWCAkAQAAAAAAAAAAAAAAwgIhCQAAAAAAAAAAAAAAEBYISQAAAAAAAAAAAAAAgLBASAIAAAAAAAAAAAAAAIQFQhIAAAAAAAAAAAAAACAsEJIAAAAAAAAAAAAAAABhgZAEAAAAAAAAAAAAAAAIC4QkAAAAAAAAAAAAAABAWCAkAQAAAAAAAAAAAAAAwgIhCQAAAAAAAAAAAAAAEBYISQAAAAAAAAAAAAAAgLBASAIAAAAAAAAAAAAAAIQFQhIAAAAAAAAAAAAAACAsEJIAAAAAAAAAAAAAAABhgZAEAAAAAAAAAAAAAAAIC4QkAAAAAAAAAAAAAABAWCAkAQAAAAAAAAAAAAAAwgIhCQAAAAAAAAAAAAAAEBYISQAAAAAAAAAAAAAAgLBASAIAAAAAAAAAAAAAAIQFQhIAAAAAAAAAAAAAACAsEJIAAAAAAAAAAAAAAABhgZAEAAAAAAAAAAAAAAAIC4QkAAAAAAAAAAAAAABAWCAkAQAAAAAAAAAAAAAAwgIhCQAAAAAAAAAAAAAAEBYISQAAAAAAAAAAAAAAgLBASAIAAAAAAAAAAAAAAIQF92hvAPr6zo9+oNq6Wn3sQ/dp+rRp4/Y5AAAAAAAAAAAAAAAYSwhJTAAlZaU6ePiQjuefVH19vRqbmuR0OpUQH68pOZO1YN58zZ4x084DAAAAAAAAAAAAACBcEZIYx+rq6/XyG69p34H9PeZHR0Wrta1VlVVVdtq9b68y0tN1751320sAAAAAAAAAAAAAAMIRIYlxqqyiXI/87lE1NDTI7XZr6aLFWrF0mTLTMxQRESGf36/SslLtO3hAW7ZvU3lFhdo97aO92QAAAAAAAAAAAAAAjBpCEuNQZXWVHv7tb9TU3KTEhAR94K57NDk7p8c6TodD2VmT7HTRilV6Yv1TyszIHLVtBgAAAAAAAAAAAABgtBGSGGe8Xq+eXP+0DUhERkbqgQ/fr9SUlH7vk5SYqI9/5KM2OAEAAAAAAAAAAAAAQLhyjvYGYHBM64wzxcX2+q033HTOgEQnAhIAAAAAAAAAAAAAgHBHSGIc8Xg8envLu/b6lJzJWrJw0WhvEgAAAAAAAAAAAAAA4wYhiXHkyPFjqm9osNfXrFo92psDAAAAAAAAAAAAAMC4QkhiHDl24ri9dDqdmjtr9mhvDgAAAAAAAAAAAAAA4wohiXGkuKTEXmakpysyMnK0NwcAAAAAAAAAAAAAgHGFkMQ40tAYaLWRlJA42psCAAAAAAAAAAAAAMC4Q0hiHPF4vfbS5XKN9qYAAAAAAAAAAAAAADDuEJIYR+Lj4uxlXUP9aG8KAAAAAAAAAAAAAADjDiGJcSR38hR7WVxSovqGQOsNAAAAAAAAAAAAAAAwMIQkxpEVS5fZS5/Ppxdfe0V+v7/f9Wtqa/XCqy+P0NYBAAAAAAAAAAAAADC2uUd7AzBwU3Im66IVq7RlxzbtP3hATU1NunztpcqdMkVul8uu09jUpILC03b5gcOHNHfWbHYxAAAAAAAAAAAAAACEJMafG6+9Tm63W29veVcnT+Xbyel0KioqSu3t7fJ4PD3Wnz4tb9S2FQAAAAAAAAAAAACAsYRKEuOMCURcf/U1Wr5kqbbv2qn806dUU1OrlpYWRUREKDUlVZnp6cqbOk1zZ89RSnLyaG8yAAAAAAAAAAAAAABjAiGJcSojPd1WlQAAAAAAAAAAAAAAAAPjHOB6AAAAAAAAAAAAAAAA4xohCQAAAAAAAAAAAAAAEBYISQAAAAAAAAAAAAAAgLBASAIAAAAAAAAAAAAAAIQFQhIAAAAAAAAAAAAAACAsuEd7A9DXlz/3hQnxHAAAAAAAAAAAAAAAjCVUkgAAAAAAAAAAAAAAAGGBkAQAAAAAAAAAAAAAAAgLhCQAAAAAAAAAAAAAAEBYICQBAAAAAAAAAAAAAADCAiEJAAAAAAAAAAAAAAAQFghJAAAAAAAAAAAAAACAsEBIAgAAAAAAAAAAAAAAhAVCEgAAAAAAAAAAAAAAICwQkgAAAAAAAAAAAAAAAGGBkAQAAAAAAAAAAAAAAAgL7tHeAAytNza+pTc3b9S03Kn6+EfuH/AyAAAAAAAAAAAAAAAmOkISY1hzS7O+9f3vyuv12ttf/NRnlZaaOqrbdOzkCe3au0dFZ86oobFBfr9fcbFxio+PU3bWJBvAmJGXZ+cFC2gYERER9mdJTEgI+hzVNTX67oM/tNc/9qH7NH3atB7Lf/nbX+vU6YI+9zOPmxCfoNzJU7Rq+QrlTp48hD85AAAAAAAAAAAAAGC8IyQxhu3Zv68rIGHs3LNb11551ahsi8fj0R+fXa8Dhw52zXM4HIqOilZ9Q71q62ptcGLbzh26Yt1luuqyy0M+Vnt7uw1M3HbjzRe0TU6nUzExMV23m5qaVFVdZafd+/boyksv05WXht4OAAAAAAAAAAAAAEB4ISQxhu3cvdteXrRylbZs36Zd+/bo6suvsOGAkfbyG691BSSWL1mqNStXKyMjQy6nUz6/X1VVVTqef0L7Dx6Uw3HuxzOBj0suWqP01LTz3iZTMaJ72xCP16tTBaf03Msvqqq6Whs2bVTOpGzNmTX7vJ8DAAAAAAAAAAAAADBxjPxoOwbkTEmxSspKbaWG6666RinJyWpoaNDRE8dHfA+2trZq+66d9vrKZct1x823alJWlg1IGE6HQ+lpaTY48Yn7Pqp1ay4J+ViJiYnKysiUz+fTa29uGNLtdLtcmjl9hj549/vlcrnsvC07tg3pcwAAAAAAAAAAAAAAxi8qSYxRptKCsXD+fEW43VqycLFtUbFz9y7NHeHKCBVVlV1tP+bNnnPO9SMiIkIuc8iha668So/+4XEdPHxIhWeKNCVn8pBub2Z6hrInTVJhUZGKiot7LCuvrNA7W95TfkGB6urr7LzYmBglJCRo+rQ8LV20WBlp6UO6PQAAAAAAAAAAAACAsYFKEmNQu8ejvfv32+tLFy0JXC5ebC+PHD+mhsaGUdu2uvr6C36MOTNnaVruVHv91Q1vaDgkJiR2VcHodPzkCT34i59rx+5dqqqustUs3C63/ZmKzpzRpnfe1r4DB4ZlewAAAAAAAAAAAAAAo49KEmOQqbDQ0tqi1JQUTZ0yxc5LTTbXc1VQeFq79+3TujUXj9j2mMoMpjpEe3u7NmzeaFttTM7OuaDHvO6qq/XzRx5WfsEp20Jk9oyZGko1tTX2MiYmpmvecy+/aCtimJYc1199jW370RlKMaEJs9+TEpOGdDsAAAAAAAAAAAAAAGMHIYkxyFQ6MEzrh+5MNQkTkti5Z9eIhiRMQOKyS9bp9bc2qL6+Xj/71S+Vnpam3MlTlJOdbQMTWZlZcjkHXpjEtNiYN2euDh05rNc2vKFZ02fI4XAMyfaaFh7FJSVdz2M0NDaqqrraXr/zlluVEJ9w9udzu21gojM0AQAAAAAAAAAAAACYmAhJjDFVNdW2uoKxZGHPkMTCeQv0wisvq6KyUgWFhV1VJkbC5WvXKSoyUhs2bVRzS7PdBjPt3LPbLo+KitKCufN02dp1turFQFxzxZU6fPSISspKtffAfi1ZuOiCttG0zTh5Kl+vvPGa/H6/nXfxqosC2xcZaUMYZn59Q0OPkAQAAAAAAAAAAAAAIDwQkhhjOkMH03KnKiU5ucey6KgoW31h34H9tprESIYkjDWrVmvF0mU6evy4Thbkq6j4jMrKy+XxeNTa2mq3fd/BA7rnjvdp7qzZ53y8jLR0LV+y1FbOeGPjm1o4b75cLteAt+fU6QL9y399I+gyp9OpG665VtOnTeuqhjF9Wp5O5J/Ub37/O61atkJzZs3SpKxJcg/iOQEAAAAAAAAAAAAA4xchiTHE5/dr9949QVttdFq2aLENSew/dFA3Xnu9rZAwkkzYYMG8eXYyvD6fis6c0fZdO7V73x61t7friT89pb/49OeUEB9/zse78tLLtGf/PlXX1Gjbzh02iDFQJggRExNjr5tGHW53hBIS4m0bEBPmSE9N67H+7Tfdosee/L1Ky8r01tub7GRCGaZlyLzZc7R8yTLFdjweAAAAAAAAAAAAAGDiISQxhhw/cdy2jDDWv/CcnUJpa2vT/oMHbBhgNLmcTlvRwkzJSYl6c/MmG5QwFSUuWR1oddGfxIRErVm5Wpvfe0dvvb1Zy5YsHfBzmzDExz9y/4DXT05K0qc//kmdOHnCVsMoKDptAxOnCwvttPGdt3XvnXdrRl7egB8TAAAAAAAAAAAAADB+OEd7A3DWjo5WGwO1c+/g1h9uK5et6LpeWVU54PtdeslaRUdHq7GpUe9seVfDyelwaNaMmbrpuuv16Qf+TF/70ld01213KCkxUS0tLXrymafl8XqHdRsAAAAAAAAAAAAAAKODShJjhAkIHD56xF431QxmzpgRct2Kygr97Fe/tNUPyisrlJGWrrEgslvrD9PGYqBioqN16cVr9eqG1/X2lvdsiGGkREVFacnCRYqLi9Ovf/eoGhsbVVZeppxJ2SO2DQAAAAAAAAAAAACAkUEliTFi97598vl8dtB+zuzZioqMDDlNzs5Relqavd/OQVafOB+NTU06U1J87p9h756u69lZkwb1HGtWrVZiQoJtI2Labgy1c1WHiHCfzQs5HI4hf34AAAAAAAAAAAAAwOgjJDFGdIYd5s2eI/cAqjAsmDvfXu7et1den29Yt62hsUE/ffgX+uVvf62tO7erorJSfr/fLjPBDnP7xVdf0YuvvWLnJSUmacHceYN6DhNSuPLSy+31I8eODvnPYKpu/Oihn+mdLe+pvKJCvo7tNz9HQWGhnn3pRXvbBDWyMjKH/PkBAAAAAAAAAAAAAKOPdhtjwOmiIpVXlNvrC+YFwg/nsnDefL319ibbHuLosaOaN2fusG2f0xnI0pw6XWCnznlRkVFqaW3pCkwYKcnJ+tA99/ZovTFQyxYv0dtb3rWhi+Fg2mi89PqrdrLbHxWl1tZWG/QwzO27b7+z6+cFAADhzeP1qc3jU6vHq9Z2r1o6Ls3U5vHK4/Pb4yBzJGQCmPa631w39+68HljudDjk6KhY5XSYy8D17vPMOpFul6IiXIqOcCnK7bK3O6+7XQ4qXgEAAIxx9piwrVU+851TW6v8nZcd8/ztbfY4UX6fnex1+92U39xZfnswaQ4kfeYLOHO0KEfnAaQ5enQ6O6467WSPKyMi5YiMlDMqWs7IKDmiouylMypKDnObqqkAAABAD4QkxoCde3Z1DdLPnD5jQPfJysxUelq6KiortGPP7mENSWSkpesrn/+ijhw/poLTp1VaVqaaulobkHC73YqLjbXVF+bOnqMlCxfZeefDhBOuufxKPf7Uk0P+M0zOztb777xLJ0/lq6j4jOobGtTU1GS3NTU5xe73zpYfAABgYnw53djqUW1zm+qa21Tf3KYWG27w9Qg7dAYg7KXH27FOYJ7HezYIOhaY78GjuoUmonoFKczUGbIIrOdUfFSEEmIilRQTqfjoCLldhEEBAAD6YwIN3vo6eepq5W2ol6+5yc6zAQd72SJ/W1vXvEAQoqXbOm1jbgcHAhRRckZ0BChsmCKyW5gi2oYp7Dpm3ehYuRIS7eROSLTLAQAAgInE4e9eBmAIvLn1gA6fPNN1e05ejpYvGNjAPwAAANAfc+ja0NLeFX6oaw5cNyGImqY21becne8LlHRAJ4dsaCIxJlKJMRFKio1UQnQgQGHnxQauJxCmAAAAE5AJN3jq62wAwltfK09d4NIGIhrqu66boAP6hixMWMKVkNRxGbjuSkiQ214myhWfKGd0NFUrAAAAMCTWv75FzS1nA8g3XbZMudnpQ/PgVJIAAADAWApAmKBDWV2zqhpaugUhAqEHe9nSZisTj/i22X+d22kLIHdb1vvKADl6XHRdt7dN2eSued3XuAB+2YCJmc5U979qbJTbhigSo88GKBKjI5QaF6X0xBilx0dTlQIAAIwZ3uZmtVeWyVNVEagAYYIPNvRQL29drTwNdaNU4aHbAWJ/56n1WBRsvW7Hg/0eGnY7iByqY0izRW1taq+ssFO/zx4REQhNJJrQRLcARWKSIlLTFZGeKVdM7JBtFwAAAHC+aLcBAACAEdXY2q7yumYbhiivb+m4DFz3eHzDEm4wRSU6v5fu6PIcaPvc57q/x/yhrbl2vvyBXtMdtwLXA7d6zO99u+O6uXQO8kvyplaPnYrVFHwFh5QWH62MhGhlJsYow0wJMfZ6cmwkZxACAIAh5/d41F5VofaKssBkB+3N9XJ5GxuG+tk6LnpeBo6ouh1U9rgeuGLXGRsHkQEOR8fm9U7o9gxUdG2xOXjsfjmI40h/e3vgNaoKHaZwxcYpIj1D7rQMRaZnKiItw4Yn3KnpckZEDOYnAwAAAM4bIQkAAAAMuTaPVxXdAxB15nqTDUKYwfchCT/4JROpsJedwQY7r2PZGPpu+kL1qF7R/Qv5gT9CICzREbBwdg9QnE+Ywi9V1rfY6dCZmh6L3G5nIDyR0BmeOBukiIvii28AANDPIYbfL09NtdqrytVeboIQ5YGpokyemqrBHwIFO4gJPFHXMZWNB3QLQzjO61hrDDM/U+BKx+3gq4U+CnTI33mwaG+aPdZR/6z7/AEeR3qbGuUtaJQK8vtsgDsppSs00XVpAhVJKXI4nQN6fAAAAGAgCEkAAADgvJnAQ2F1g4prmmx1CBOIKKtrUU3j+fdyNiGHzsoPneGHwOUYCD84HIp0ORXhdsnldNqAgZl3tmpDR40HRyCI0P3LYvOlv506r3eeZNhx3evzqd3jswETs3yomYf0nvNL//7DFM5elSyCMdVAiqub7BSsjYcNTNgARbSyEmOVmxZP9QkAAMKM3+tRW1mp2kqKOipDmDBEma1A4G8/30CtP3j4ofP2KIcfHG63nJFRgWoJTmeg8pYNGDjkCBxU2gMue5wVOJAM8HXURrMHwX75Oy7tMaQ9OPbJ194uX3ubreQw9Dr2XbfjU0d/YYpu4YnOeEbgALKrsVyop7EBGTM1Hz/S85Hd7o52HRmKSDMBinRFZk9RZNYkOVx8vQ0AAIDB4ygSAAAAgwpEFFQ2qLDKTI22ksD5MF/0mu93fR0D94HrgXlDzuFQbKRbUZFuRbpcNuAQ4XYq0u2yU0RH6OHsbXN5dl7Xpcspt6vjC+1h5vH61O71doUmzNTndsfUZtbtPs8bWKe13aum1jb5BrlTBxKmcDr8cnYEQXpM5ziD0LyH8svr7dRdXLRbuanxmmKnOHs9JS6Kth0AAEyYQESJ2s4UqtVOp9VWcsa2zziPR+sKPgRCEL7A9UDydMi33RkVZdtDOCMjAwGHjpBD4Hpkt8tIOSIi5YoKrOPos06EHE6XhpvfBCba2uRra7XBCb+57Lzdbb6vtfXs9W7L/PayTZ6mRvlaB3uc3y2I0vFSOIIGKbqHKJzdqlGEPo407xX7Hior6flo5rh9Uo4ic6YoKifXTgQnAAAAMBCEJAAAABB0MPt0VYOdzjcQ0T0IYSd7ItzZKhEXyoQVYqMiFBcdobioyMBldGRgXvfbkRFydj8bbxwwYQwzxURe2OOYswtb2j1qbGlXU2u7Glra7GVjS1tg6rze2i6v17xCA9MZagnyjB2BiUCAwtUtQNFf9YnGFo9t29G9dQfBCQAAxnkgouh0IBBRWjzIQET3lhj+rgCEvRyialvOqGi54xPkjo/vuEyQKy5eEQmJ9rJznq36MI6YlhSu6Gg7XSifp12ehnp5GhrkNZeNDR23z07exgZ5m5sH8aidr2PH9srbbeM7Kk+Yy87KE+do5eH3egPvs6LTqtc7Z4MTWdmKnJyrqOwpipo8leAEAAAA+iAkAQAAEObMALkJQXQGIk5XNqiqoXXQ7THOVoa48CBETFSEEmKi+gQg4qIiFN8RhDDrmMF49B8kiYmMsNO5whSm+oQJUwRCE21q6B6saDG321TX3HbOFzZUgMLh6BagsNUoJJczdPWJYMEJ067DVpxIi1cuFScAABgTgYjWokK1nTmfQERnVYjurTE6QxHnx7ZlSE7pCDkkng1BdAs+uOLj5XSPr/DDaDD7KDI51U7nClN4Gxvlqa/rFqQIXHobA2GK9poa2w6kX13vg76RiO6hic4gRajqEzY40VG1pLN2WZ/ghKk4MSmbVh0AAABhjJAEAABAGDFtHE5V1utkeb0KKwOVIs4nEGGKDng7AhHn+z222+1Sany0kuPMFKOUruvRiorgMHWkwxRmn5spNSGm3/dPXVOrahqbVd3QoprGwFTd0GwDFQNp4+HtVQq7Mzzhcjhs5Yn+ghOmwsnh4ho7BQtO5KXFa0Zmkp0HAACGjglUeqoq1ZJ/7GyFCNMyw+sd6CP0DEH4fecfhnA6FZGUrMjUdEWmpCoyNc1ej0hJlTshkXZdoxCmcCYl29ekv/ePqTrRVlWhtqrKrqm9ulLtNdW2TUh/ulcRcQQJT/gdzvMKTkRkZSvKtOqYPFXReTMVkZbB+wcAACBM8O0hAADABGYGtQsqG3SstNZOJyvq5fH4RiwQYdpcJMVGK7kjAJESF2Ovp8RF22oQZnAe44dpAWJCFMGCFK3tnm6hCXPZbK9XNbTI4/EOKjxxQcEJhzQ5JU6zspI0KzNRM7OSFBPJf3sAADifUETzyWM2GGEuvXW1IxqIMFUfTPAhMs2EIUwQIjBFJKXYAW6MH+aYv7OSR+zU6X0CDO21NYHgRHUgRNFeXWUvTXWKfh+3qxKJ77yCE6YtjJnqt71r57kSEhSTN0vR0wMToQkAAICJi28LAQAAJlgowlSH6AxFnCgfWCjiQgMRpvVFWoKpBhHTEYYwwYgYJcZE2aAEJj5ThSIrOd5OvQdZTEsXG6BoaFF1Z3iivlm1jS0Drzrh8AcCE+cKTviloqpGO7158Iz9TnxKSpwNS5jgxMzMREITAAD0+dvrl6e6Ui0nA4GIlvzj8tSerdw0sECEb9DtMhwut6LSMwJBiI5qELZCRGqqnJFRvE5hwLbC6AjASHN6LPO1tamtuiM0UVlhr5tqFK3lZSHbulxIcMJbX6+GvTvtZLgSEhXTEZgwl+7UdELeAAAAEwQhCQAAgHEeiiisatDRzkoR5fVqP0cowi9/YAD6AgIRmUlxgSk5cBkfHckXhgh55qB5f5hpSlpin+oTZbWNKqtpDFzWNoYMTuh8gxN+qbCq0U5doYnUjkoTWUmakUFoAgAQpqGImqpAKOLE0QGGIoYgEJGZqeisHEVn5yjKtDpIz6QqBEJyRkYqOivbTj3eiT6v2ioq1FJyRi2lZ9RSUqzWspIhD0546+vUsGeHnQxXYlIgNJHXGZpI4/9AAAAA4xQhCQAAgPEWiqhu1LGSzvYZdWprH1gowmQnTCDChCMGikAEhrv6RG56kp16ByfKO0ITpTWDC044TXDC6ZC7v9BEZaOdNhwIhCZyU+M1MytRsztCE9G05wAATEDmbPzO1hktJ4/KUzOAShFdYQifHKbsWPew4jkDEVmKnpSj6EnZBCIwpBxOl31/mSlJy4cuOOE0gQlnV4CiT2iirlYNu7fbyXAnJSs6b2ZXcILQBAAAwPhBSAIAAGCMn+VXWtus/UVVHe0zhi8U0RmIyOqoDpFBhQiM0eCEqTxhWnYEY8ZvfF6/2gcRmjhd2WCn7qEJU2ViweQU5aUnyG3uCADAOONtalTT0YNqPn7EVozw1FQPSyiCQAQmTHDCZ/7j5Os6WjxXaMJUXwkampg5V7Fz5ssV17MNHQAAAMYOQhIAAABjsFrEyfI67S+q1r7CKlXWhziL/gJCERFul3JSEwhEYMIEJ85U1auhuW1AoQm3s//2HJ2hiTcOFCk60qUFOSlaNCVV83JSFEOVCQDAGNZeWaHGQ/vUdHi/Wk4dD/whHMpQhMNhB6FjcnIDFSIm5SgqLYOWGRh/wYnS4kB4orjQhieC9R/sGZpwyO/s3p6jb4i2R2jCIUVPna7YeYvsFJmeOQI/MQAAAAaKkAQAAMAY0Nzm0cEzgVCEuWxp8w5LKGJKWqKmpCcqIzFOTmevwWFgHAcnTNWV2qZWFVXWqdBMFXVqbAkemmgzv0ADDE2Y38Ud+RV2Mr8zMzMTbWBi4ZRUpcVHj8BPCgBAaH6fT62Fp9R0aL8NR7SXlw5LKCI2N0+x06YrZso0uaL5+4cJEpxYvMzO87a0qLmwQE2n89V8Oj9EaMJvf2fOtuc4R2jCL7WcOmmnqpeeUUR6hmLnLrSTCU84XK6R+YEBAAAQlMNvvk0cQm9uPaDDJ8903Z6Tl6PlC2YM5VMAAABMCBX1LbaNhglGnCirky/EmX4mFOHrCEV4BhiKcLtdmpyaoMkmFJGWaNtnEIpAOOkMTZjARFFFIDgRLDTRm8kO2dCEU7ZFh6N3pYluslNitXByIDAxLS1eDluGGQCA4eVra1XzscM2FNF8+IBtqxGaKankO79QxNQ8xeSaUETMUP8IwJjWIzRRkG+rTgSrNNFTt9CEadPRzzGkMzZWsbPnB6pMzJonJ8EjAACAPta/vkXN3b7Lu+myZcrNTtdQoZIEAADACA7anqpssKGI/YVVKqlpCr2u/B2hCBOO8J/zOzkTishJSbBVIqakJSgjKU4u++UcEJ5MYCE5LtpOi6Zm2t+/msYWFVXWB4ITleeoNNFRzMXtPDv1rjJRXN1kp1f3FSo+JkILJ6do0eRUzclOVqSbswMBAEPHlPE3LTRMMKLl5DH5PZ7QK5tAhKkYYcMR5z43KipzUkcgIk+xuVPlionlpUNYM9VS4mfNsdPZ0MQpNRV0hCbKSvqvNOGV/CY863R2VJro+f8yX1NTV1sOU1EievqsrioTESmpI/iTAgAAhC8qSQAAAAyj1navjpbUaF9RlfYXVauhuT3kur7OYISd+v9C2+1yBtpnpCcRigDOQ2doojMwYdpzNLWG/v00AtUlHDYw4eqnaoTb7dTcSUm2ysSCyalKio3kNQIADPrvVFtJkZoO7VPjof1qO1PY39qBFhodFSPOla6NysgKhCKmmlDENEIRwCB5W5rVfDoQmjDVJlrLSvv/vXMEKkz4TYjdHkOGPo6MnJRjK0zEzVuoyJxcKpUBAICwtZ5KEgAAAOMvGLH3dKV2nqrQkdJaeUzqIQSv3z+wNhoOh3JS4jUtMznQPiOZShHAhVaaSImPsdPiaVk9QhMF5bU6VW5+d709f1/tiYF+tXpNWw6/bcsRCEz0bMthfuf3F1bbSTquqenxWpybplV5GUqOi+KFAwAEZf4WtZ7OV8Oe7Wo6tN9WjxhYG43+e7FFJCUrbuZsxU6dbqtFuGOpFAFcCNOCJn72PDsZ3uamQHuOUyfVcPyI2muqe/9yy+H3yuELHFuasESothxtJWfsVLPhZbkSEhU7b6HiF69QdN5MAhMAAABDiEoSAAAAQ8Dj9elISY22nSy37TTaQwQjTBsNU8m/s1pEfwUjTAuNvIwkzZiUYsMRMZERvFbACP5Om8DEydJqOzU0923N0cUhRXRUmDBT98BE7/VmZiZq1fQMLZ2arphIuh8CAKS28tJA6f092+WprjpHG42z4Yj+RGdPVvzsuYqfOVeRGZkMrgIjWQWmskINxw6r8dhhNZsqMP1UmegMS9j2HL3acnTnTk5W/OKVil+6UpFZ2cO09QAAAOFTSYKQBAAAwAV8AXaqskHbT5Zr56lyNbZ4QgYjTCiivaNihDnxL5T4mEjNyErR9KwUTU5LtG01AIz+73pFXZNOdAQmymoa+13f/NpGdFSZcIYITLhcDtuOY2VehhZMTuF3HQDCjKe+To17d9hwRGvIVhodbTRMIMIGI0IfRDrcbsXlzVDcrHmKnzlb7viEYdt2AAPnaWpU47EjgdBE/nH520O3d+sMSpyrLYdpyRG/dJXilyyXOzGZlwMAAExI6wlJAAAAjC3ldc3anl9uwxEV9S1B1/F1BSPO0UZDUmZyvKZnJdtwRHpiLGf6AWNcfXOr8ktrbGiisKJO3n7KnDsdprqEQ5Gu0IGJ6EiXlk1Lt4EJU2nCtAIBAEw8vtYWNR7cp4bd29R8/HCI4GxHMMJnSvObFUIHI1xxcbZShKkYETtthpwRVB0DxjKfpz3QksNWmTgiT0N9v+vbthxOV+jAhEOKmT5L8UtWKnbBUrliYoZv4wEAAEYYIQkAAIAxoL65TTtPVdhwREFFw7krRvTTR8PldCo3I8kGI6ZnptjqEQDGp3aPV6fKa22FifyyGjW3hj470GXacrj6rzCRHBdpwxIrp2coOzluGLccADAS/F6Pmo8dthUjGg/tlb/dE7qVhqkWYYN3oY8jozKyFDdrjuJnzbUtNQjWAeO3UllrabEajh5Ww/HDai0t6WdtR0dgwlSYcIasJhM7d6GtMBE7Z54cLtq6AQCA8W09lSQAAABGR2u7V3sLK23FiMMlNea766DBCK8/EIxoN1dCiImKUF5msmZMStHU9CRFuF3Du/EARpzP71dJdYMNTJgqE9X1zf225IjsaMnhCBGYyEmJs2GJFdPSlRwXNYxbDgAY8sHPwlOBYMTenfI2hWjTZFtpePttpeFwOhUzNc+GIuJnzlFEcgovFjABtdfVqvH4ERuaaCo4Kb/XG7olhwlMOEIHJpyxsYpbuFQJS1cpaup0wlQAAGBcWk9IAgAAYOR4fX4dKamxwQgTkGgz6Ydg6/n9gWCEz2++3w4ZjJg7OV2zslM1KSVeTkroA2GlprFFx4qrdKSoQhV1TSHXizBhCZfkNm2oQ5RSnpWVZCtMLJ2apphIzgwEgLGoraIsEIzYs0PtVRUh1vIHQhE2GOELGYyImzFLCfMXK27GbLmio4d1uwGMLb62VjWeOKa6g/tscCJ0YKKjuoSZQoRu3ckpil+60rbkiMycNMxbDgAAMHQISQAAAIyAyoYWvX20RFuOl6mhJXi5fJ/8avcGghGhumm43S7NnJSieVPSlZuWJKcz+JdVAMKLCUkcLqqwU0NzW9B1TI7KBCYinKY1R4gvul0OLc5N07o5kzQjI5EzAwFglPna29W4b6fqtr6t1tOn+g9GdLTUCCVmcq4SFy5R/NyFcsfGDts2Axg/vC3Nqj90QHUH9qg55GeMAu04HP0HJiJzpihx9VrFL1khZyRVygAAwNhGSAIAAGCY+Hx+HSyu1uYjJTp4pjpo+2fTTqOzYoQ3xHfaphf0tMxkzZ2cphlZKbTSANBvCfaiqnodLqzQ0eIqtYXoTe/sDEy4JGeIL7onJcfasMSq6ZmKNisCAEZMe2WF6rZuVv2O9+RrDtZeyR9op+Hz9huMiExNs8GIhAWLFZmcOqzbDGD8t+SoO7BXdft3q62ivP/AhNMVSOAGOY50RkcrftlqJV60TpEZWcO81QAAAOeHkAQAAMAQM5Ui3j1eaitHVDe0Bg1GeGwwQvKEKhkhKSslXvMmp2t2TppioyJ4nQAMisfrU35ZjQ1MnCyrtsGtYFzOjpYc5uTAIF90R0Y4tXp6pg1MZCfH8SoAwDAxJe+bjhxU3ZZNaj52OMRKHa00bDAi+Oe6Oy5eCfMX2XBEVFY2VYEADO6zyO9Xa3mp6vfvsS05PPV1IdZ0dAQmnKaPT9A1YmbMUuJFlyp23kI5XLR0AwAA4ROS4MgHAACEzRdJpyrqtelIiXYVVMjr7fultc/vV5tPajMDlSGyEclx0Zo7JV1zJ6fb6wBwvtwup2Zlp9qppc2jY8VVth1HUWXPL7pNFRtvR4DCBCUiXaYdh/naOxCYaGv32Yo4ZpqZlah1syfZlhzm8QEAF87TUK/67e+qfuvb8tTWhG6nYapG+IMfRDojIhU/d74NRsROzZPDnOUNAOfBVDKMzpxkp/QrrrVtOEw7jvrDB+Rr7X4SQGc1G6/8pqqE0xUITHQL3TafOGYnV0KiElddooRVF8udmMzrAgAAJjyH34wYDKE3tx7Q4ZNnum7PycvR8gUzhvIpAAAABqy13asd+eXafLRERVWNIatGtPXTTiMmKkJzctJsMCIrOY6z/QAMq/rmVh0uqtShwnJV1TeHbsfhcijSnBgYpLpEfEyELpmVpbWzJik5jp7TADBY5uuyllMnVL9lsxoP7LFVJEJXjQiyzH5YOxU3fZYSFyxW/Ox5ckZQeQzA8PF52tV47Ijq9u9R48ljwT+3erTjCBKodToUN2+xEtdcqujps/i/LwAAGDW02wAAADgPpbVNNhix9USZWtr6fjnkk19mdrvPb9pF92HOwJ45KdVWjchNT5TLnnEDACM7QFdR12SrS5jQRGO3EoPddYYlXLbvdC8OaeHkVF06d5LmTkrmi24AOAdfa4sadm9T3ZbNaist6adqhE8Of/CEbXTOFFsxImHeQrljaYMEYOR5m5tsZQkTmGguLAi6jt/R0YqjV3WJThEZmUpcvU7xy1bLFRMzAlsNAABwFiEJAACAAfJ4fdpXWGXDEcdKaoOvY1pqeCVPR+n63tISY7UkL8tWjYh0UwYZwNhg2gEVlNdqT36p8stqTIKizzqmBYcJTESEqC6RnhCtdXMmafWMTMVFcTYzAHTXVlpsgxH1u7bK3xYslNZRtt6WHuv7GeyMilbS4mVKXr5KkalD1ycXAC5UW3WVanZtU93enfI2B6tS5pDf5ZTfYapL9D2GdEREKH7pSiVetE5R2VN4QQAAwIggJAEAAHAONU2tevdYqd45Vqq6pragLTXaTUsNr1/BshFOp0OzstNsOCI7JZ4zrQGMabVNLdp3qkz7C8rV0tbeZ7n5bjvC6VCkS3IGCUu43U6tmJZuAxNT0xJGaKsBYOzxez1q3L9HdVs3qyX/RLA1bCjNhiN8watGRGVNUsqKi5Qwb5GckZHDvs0AcCHtOOoP7lfNzq1qKS46RysOcwzZ9zgyKjfPhiXiFi6lhRAAABhWhCQAAAD6aanx2v4ibc8vly9I+sFrqkb4pHZv8KoR8TFRWjwtUwunZiqWs6oBjMPqOceKq7T3VKmKq+qDruNySpFOh9whqkvMzErUNQunaF42rTgAhA9fW6vqt72jms0b5K0LVn3MBCN8kglHBKnc43C5lTB/oZKXr1Z09mQCtgDGnZaSMzYsUXdgr/weT98VHI6zgYkgx5CuhAQlXXKFbcfhjI4emY0GAABhZf3rW9TcrfXsTZctU2720FXtc/hNo9sh9ObWAzp88kzX7Tl5OVq+YMZQPgUAAAhzBZX1Nhyx53Rln2rHpmqEx1SN8PllqyEHMS0zWYunZSkvK1nOIOVEAWC8Ka9ttGGJQ4UVNjzRm/moM2GJiBDVJSanxumaBZO1dGq6ra4DABORt6lRde9tVO27G+Vrauq7gt/Xb9WIiKRkG4xIXLxc7tjY4d9gABhm3pZm1e7dpdqd29RWXXmO6hLOPstMQCJxzaU2MOGKi+f1AgAAQ4aQBAAAgPlixu/X0dJavbqvUEdL+p7x55NfbV6p3ec3VZH7iI50a35uhg1HJMdxpguAiam13aODhRXam1+q6oZgPacld0crDneQkFh6QrSuXjBZq2dkym3KUADABOCpq1Ht5g2q2/aO/G29W7P5JV9HOCLYQaTDobgZs5WyYrVip8+iagSACfv/7aZTJ1Szc5sajh6yrYb6rGOrS5jErTlG7Hkc6YhwK2HFxUpad5UiUlJHcMsBAMBEtZ5KEgAAINy/rNl7ukqvHShUQUXDoFtqZKXEa8m0LM3OSWPAD0BYfXYWVdZrz6lSHS+usrcH04ojMTZSV87L0SWzJynalJ8AgHGoraJMtRtfV8PubfJ7vUFbajjs/CCfkTGxSlq6QsnLVtkKEgAQLtrralW7e4dqdm+Tt7ExyBoO+V0hWnE4nYpfskLJl12jyMxJI7XJAABgAlpPSAIAAIQjUy5+R365Xj9QpNLavmdDe0w4wit5fEG+1HY5NXdymq0akZVMyU8A4a2hpU37C8q071SZGrv1cuxkumtEuYKHJWKi3LpsTrYum5ut+OiIEdxqADh/rWcKVfPWq2o8sDtI/sGEI7wd4Yi+Yibn2pYa8XMXyOl28zIACFsmXGaqSlTv2KLm06eCr+NyBQ9LSIqdt1DJl1+r6Ny8EdhaAAAw0awf5pAE/9sDAABjSpvHq3ePleqNg0WqaWwLGo5o9frlDdIqOjE2SkunT9L8KRm2vQYAQIqPjtSaOVO0alaOTpbWaPfJEhVV1nXtGpM1a/b4TUV5RbmkiG5hieZWj17ee9p+Jl8yK0tXzZ+s5LgodiuAMcdUzGnJP27DEc3HDgdbwwYjTECiN4fLrcRFS5W8fJWis7JHZHsBYKxzuFxKmLfQTq0VZarZsVW1e3fK7/GcXcd8rnq9NihhwxLd2rk1Hdpvp5gZs5R0+bWKmTGHlkUAAGDMYPQAAACMCU2tHm06Uqy3Dp9RY8vZL10Mv/zy+KQ2E44I0lUjLTHWDv7Nzk6T05wSDQDow+V0alZ2qp1Kquu19dgZnSypPvtZ65daPH61OkwbDinSdTYs0e7x6a1Dxdp8tEQr8zJ0zcIpykyMYS8DGBPhCDMIV7PxVbUGO9PZ7wtUjvD1Tdg6IyOVvOIipaxcI3d8wshsMACMQ1Hpmcq6/halXXqlara/p+rtW+RrbelaHvicNWGJjjYcDmfXsuYTx+wUlTPFVpaInb9YDufZ5QAAAKPB4Q/WnPYCvLn1gA6fPNN1e05ejpYvmDGUTwEAACaQ2qY2vXnojDYfLVZbu69POKK9IxwRpKuGslMTbDgiLzOZM1IA4DxU1jdp27EzOlJUaQcae4t0OWxYwtm7hLJDWpKbpmsXTlFuGm2NAIw8v9ejhr27VLvxNbWVlQwqHOGKjVPKqottWw1XdPTIbDAATCDe1hbV7t6h6q3vyNNQ32d5sLBEp4j0DCVferXil62ylXwAIJgHf/R9vfXmBl1+xZX6zOe+OCZ20pb33tX//e//dN3+8U8eUlJyssLR47/7rf701B+DLnM4nIqNjVV2To5Wrb5IN9x4s6KiosbFa4yxhXYbAABgQqpubNUr+05ry4kyeXuVhzDhiLaOcESwOOe0zGQbjpicljhyGwwAE1BaQqxuWD5Ll8zN1Y7jZ7T/dLm83foZmc/hNm+gqkSPsIRf2lNQaac52Um6YfFUzcjkMxnAyIQj6re/p5qNr8lTUx1kBV+g/Lu/bzgiIilZKRetVdKS5XK6I3i5AOA8uaKilXrRWiWvWK26fbtVteVttVdXdS03ATUz+R1O+V09wxLtFeUqf/pxVb/xkpLWXaXE1WvlcBOWADC2eTzteuy3v+4xr6DglBaHaUgi/+TJkMv8fp8aGxt07OgRO218a4P++V+/ofh4TrDA2MLRBwAAGFGNre16ZV+hNh8plqdXOMJnwhFeqd0XJBzhcGhOTqpWzsxRRlLciG4zAEx0ibFRunLxdF00Z4p2nSzRnvxStbV7+oQlIkxYwim5uvWbPlJcqyPFe7VgcopuWTZNOSl8RgMYeqbaTePenap67Xl5qip7L5UpO+bweUzJ1D73jUzLUOrFlypx/iI5zGAdAGBImMBZ8rJVNnxWf+iAqt7brNZu1X1MYM3hMWEJR6CyhG2zETiO9NTWqPL5p1T79htKufomxS9dRRsOAGPWC889p9LSntXLTp3K1+IlSxWOTuUHQhLm5//yX/1N13xzKF5XV6sTx4/r97971O6zosJCrX/6j/rwfR8dxS0G+iIkAQAARkRru9e21XjjYJFazEhbkHCEGYTrzel0aEFuhlbMzFFyHOWQAWA4xUZFaO28XK2cma29p8q080Sxmlvbu5a3e/1q90puZ6CyhLtbWOJAUbUOnKnWyrwM3bR0qtLi+cwGMDThiOajh1T16nNqKy7qvVSyZyubyhF9jyOjc6Yo7eJLFTdrLq3ZAGAYOZwuJS5YrIT5i9R08pgq392k5tOnzi73++XweiSfQ77eYYmaGpX/8THVbnpDKdfeoth5C/nMBjCm1NbW6umnnrTXly1fof379qq9vV0Fp/IVjmpqqlVTU2Ov5+ZOVXR0TI/lMTExysqapMmTJ+tvv/pXdt7u3TsJSWDMISQBAACGlcfr07vHSvXSvtNqaD470NYZjmg1lSOChCPcbpeWTMvUshnZio+O5FUCgBEUFeG2bY2WTZ+kA6fLbSuOuqbWruUen18en+Ry+hXlcpwNS/il7SfLtaugQmtnT9L1i3IVH01JewDnp+V0vqpeflYt+ceDhiOcPm/gdLVe4qbPtJUjYnLzGGgDgBHkcDgUN2O2nZoLC2xYovH4kbMr+P1ymrCE12HbcPi7hSXaykpU+uhDip6ap5TrblVM3kxeOwBjwu8ff1TNzU1yudy6/2Mf1/e+821bRcJMF6qoqFDPP/eMDuzfp+qqKjmdTqWlpWvJ0mW6+dbblZaWprHm5MkTXden5OaGXG/qtDzFJySoob5e9fX1I7R1wMARkgAAAMN21t+O/Aq9sKdAlfUtA64cER0ZYQflluRlKTqSQxUAGE1ul9N+Hi+amqmjxZXaduyMKuuaupZ7fVKTz28rS0S5zrbh8Hr92nioWO8dL9VV8yfrynk5fKYDGDAzUGYqRzQd3DfwyhEOhxLmLlDqmnWKnpTD3gaAURYzZaqm3PNhtZaVquq9Taozn+ldn92ByhKOIJUlWgryVfzQDxQ7d4GtLBHFZzqAUWSqRbz5xuv2+vU33qjs7BzlTp1qAxJnis7I42mX231+Jwa8ueF1/fynD8rr9fYJTpjp9dde1V9+5a+1dNlyjSWnTgZabRhTpoQOSRjOju8IEhIShn27gMFi5AEAAAx5OOJQcY2e3XlKZ6obey7rCEe0BglHxMdE2pYaC3MzFOGmVzQAjCWm9dHcyemak5Om/LIaG5YorqrvU1kiwhUISzg7zwhs9+mlPae16UixrluUq3WzJ9ngBQAEY/rTV7/+gup3brF5iB5MX3szoNYrHOFwuZS4cIkNR0SmprNjAWCMicrMUvZtdyvtsqtU/d7bqt27S35TTaJbZQm/z1SWcJu+HV33azp8QE1HDih+yUqlXHOTIlLG3tnUACa+Xz/ysHw+nxISEnXX3ffaeVNyp9pLr9ejosIiTcvLG/TjHjt6RD998Mfy+302dHHLrXdo6tSp9rmOHz+mZ9Y/rYrycv3f/35L3/zWd5SRmamxIj//bEhicj8hCRP0qKurs9cXLlw8ItsGDAYhCQAAMGTyK+r17M58HS8NHAB3D0e0+wLhiN4n/cVFR+qiOZO1IDdDLnv2CABgLJdQnp6VYqfTFbV6+9BplVY3dC037ZPavVKkKzB1hiUaWzx6ettJvXnojG5aMlUr8zJs8AIADG9To2reelV1722S39MxcNYjHGEqR/h6fyApafFypa27QhGJSexIABjjIpNTlXXDrfZzu/Ltt1S7e7v8vsBnuwnAOTzt8juctg1HV1jCLzXs3q7GfbuUsHqtkq+4Tu54zkYGMDK2bn1P+/fttdfvufcDiouLs9dNqKGTqShxPiGJx3/3qA1I5OVN17/82zcUGRnVtWzGzFlac/El+vrfflWVlRV66o9/0Kc+83mNtZBEenq6YmJigq7j8Xj0yMO/sNejoqJ10823jug2AgNBSAIAAFywktomPbfrlPadruoTjvB0hCN8vcIRkR397pdOz1KE+RIEADCu5KYn6d51iTpeUq13Dp1WdUNz1zLTTslUDorqCEs4OsIS1Q2tevTto3r9QJFuWTZNCyen2OAFgPDka2tV7TtvqXbT6/K1tPQNR5i2Gh0DaN3Fz5mv9MuvUVQalSMAYLwxIYes629RyupLVLHxddV3a61kAnEOj09+p1N+59mwhN/rVd27G1W/4z0lrb1SyeuukjM6ehR/CgBjoZJta2uv48dBcjicioo6G07ozrTRePQ3j9jrublTdc2113Utm5o7rUc7jsGqqanWgf2Bz773f+BDPQISnRITk3Tr7XfoV798SO+9+44++anPyGk+F0d53zQ1Naq8rMxen5Sdo5aWs98DmBPjmhobdfToYa3/09PKP3lCERGR+vwXvzSmKmEAnQhJAACA81bd2KoX9xRoy4myPiWRPX6/Wjx9wxEul1PLpk/Sypn0pweA8c4EHGZlp2pGVooOFpbrvSOFamhu61puQnJtvkBYIsJ5NixRUtOkhzYcVF5Ggm5bnqcZmYmj+FMAGGmmzHr99ndV/cZL8jY09F7a0ae+bzgidtp0ZVxxraKzJ4/YtgIAhkdkSqpybr9HLWvWqeKt19R44ljXMvM3wEyBsIRpwxE4hvS3talmw8uq37JZyVdcq8SLLpXDzRAHEI5MK4ovffGzF/QY6RkZ+t4PHgy67MUXnldpSYm9ft9HH+gRUEhLT1dsXJwNBJhKEoNl2mmYIIPL5daixUtCrrds2Qr9Sg+pubnZtvXoXsFitPZN/smTdtuNfXv36BMfuy/o/U3A4oorr9att92hyVOmXNC2AMOFIwgAADBoja3temVfoTYfKZbH2zMF4TXhCK9f3j4VkR1aODVDF82eoviYSPY6AEwgpnXGwqmZmjs5XXvyS7Tt2Bm1tAVK5pvvT0xortUhRbskd7ewRH55vb7/8l4tmJxiK0vkpATKlwKYmMwXqo17d6rqteflqarsvTTQVsPn7XO/6KxspV95rWKnzaD6DABMMOYzfsr771NTwUmVv/maWs4U9gpLtNmqErYNR8cxpGnTVPnCn1T7zptKufomxS9dJQftOwEMkbq6Oj39xyfs9eUrVmrxkqV91jHVJQ4fOqjTBacG/fgmxGBkZGYoIiIi5Hqm+oLb7batK8rLywYckhiJVhvn0traqmPHjqisvJSQBMYsQhIAAGDAvD6/Nh0pttUjWkwd9e7LTCk3r+TpXTpC0qycNF0yd4pS4oP3qQMATAxul1MrZubYwMT248XadcKE6XxdYYlmj19OR2dlibNtNg4UVevAmWpdPCtLtyydpvjo0F8UARifWk7nq/K5P6q16HSvJf5AWw1v33BEREqqMi6/RvFzFxCOAIAJLnbqdE2978/UcPSwrSzRVhkYRDQC7Ze8Nihh23B0hCU8NTUq/+Njqn17g9JuuVsxeTNH8ScAMJJMgODRx588r/u+ueF1/eTHPwy5/A+PP6ampiZb6eG++x8Iuk5nSMIEKqqrq5WSkjLg5zeVIYzY2P5PEnA6nYqJiVV9fV3XfUZ735hKEp2+/8Of2KoaPQLRjQ06cviwfvPrh1VUWKhv/fd/6R/+6V80f8HC89oeYDgRkgAAAANyrLRWf9x2QsXVTT3m+xQIR7T3qihhTElP0rr5ucpKjmcvA0AYiYpwa+28XC3Jy9LWo0Xad6qsqySnryMs0eb0K8rlkLujfLJp2/Tu0VLtKajUzUun6pJZk2yFCgDjm7exQVUvP2v7yPfkl3w+OW04wt+nX33auiuUtHi5HPbMYQBAODAVKBPmzFP8rNmq27dHFZvekKe+7uxyU3HI6+sISzi7whJtJcUqfugHil+6Uqk33C53Aq3cAJyf0wUFeuP1V+31ZcuXq7m5SSeOn20H1CkqOqrresGp/EGFJExlCMNUiTiXiIjAOh5Pu8aCUx2VJBITE3sEJDo/w+PjE7Ri5SplZGTqa3/zZfn9Pj37zJ8ISWBMIiQBAAD6VdvUpmd25mv7ybNncXSGI0wxibYg4YjM5DitnTdVUzOS2LsAEMbioyN11eLpWj4jW+8ePq0jRWfL65sCE00+v22/YSpLuDrCEk2tHj2x5YTePV6qu1fPVF56wij+BADOl9/nU/22t1X16vPy9T7zzeeV07TV6AhPdXJGRSv14kuVsnKNnP2UHgYATGwOp0tJS5YrYcEi1ezcqqp3Nsrb9bfEtGfyyOFzyGeqSnQLSzTs3q6mw/uVctUNSlxzqRwuhj8ADM6vH/mlfL5ANcTt27ba6VxOncrX0mXLJ/yubmtr05kzRfb69Bn9V+4xrUESk5JUV1urwsLeleSAsYGjBAAAEJQpjx5orXFare1nyx/75Ve7T2r1+nt/r63kuGhdMi9Xs7JTKYkMAOjx9+HGFbO1cmaO3j50WqfKas7+vfH55fFJka5AWMLR8SV3YWWjvvviHq2Zlalbl+XRggMYR1oK8lXx7BNqKw58idrF7wucBewPfPHcyeF222BE6pp1csXEjuzGAgDGLKc7Qqmr1ypp8QpVb31bVVvfkb+942xqv19Or0d+n9NWlpDDhCUkX0uLKl/4k61gZFtwTJ81uj8EgHHDBCL27d0z6PuZShLhoKDglLwdLfKmT59xzvWdHSdCuKgMhzGKkAQAAAjaWuPJrSdUUtOztYbX71eLx6/exSPioiN10ZzJWpCbIZc9iwMAgL4ykuJ0x5p5Kqyo0+ZDBSqtbuhaZioTmRBetEuK6NZm471jZdpdUKlblk7T2tm04ADGemuNypfWq2Fn7zPuzFm/gX7yPTidtqWGaa0RQWl0AEAIruhopV92tZKXr1blO2+pdtd2W7HIMME7h8cnv9MVCEt0tuAoLVHxL36o+CUrAi04Eql0CaD/FhiP/uYRez0zK0vf+t/vyu3uv7LZl77wWZWXl6ng1Kmw2LWnTgZabQykkkRRYaFqagInR+TmTh32bQPOByEJAADQpaapVet35GtnfkWf1hqtXqm9VzrC7XZp9awcLZsxSRGkggEAAzQlPVH3rluo4yXV2nywQLWNLXa+qVDU7PGrzelXtMvR1YKjpc1rw3uBFhwzND2DPtPAWOL3elW3ZbOqX3/BnsHbbYnk88lpzzjreRwZP2uuMq66TpGpPXsZAwAQijs+QVnX3aKUVZeo4q3XVH9of9cyE8Qzk8+02OjegmPPDjUe2qeUq25U0iWX0YIDgKblTddd99yruNi4rr3x0ovPq7j4jL3+oQ/ff86AhDF5yhQbkjD3a29vV8QEaBcXbN90ys8/G5KY0U9Ioq2tVQ/9/Cddt6+6+tph2FLgwhGSAAAAtrXGW4eL9dLeArWZ03gH0FpjzuQ0XTp/muJjItmDAIBBczgctj1TXmaydhwv1tZjRfJ6A3+DzEWjz9+nBUdRVaO+99JerZ6RqduWT1MCf4OAUddy6kSgtUZJ8YBaa0SkpCrr2psUN2P2yG4oAGDCiExJVc4d71fj0pUqe/UFtVWWdy0LtOBwyG/CEh0tOPxtbap6ab1twZF+692K4W8QENby8qbbqVNdXZ2e+uMT9vqcOXO15uJLBvQ4U6bkatfOHbYFReHp05o+49wtKMbbvgkWkoiPj1dcfJxaWpq7lnk8XtXUVOvQwQN6/rlnVFIc+L/BNdddr2XLV4zQ1gODQ0gCAIAwd6Skxp6dW1Z79sDW8Pj9ag3SWiMlIUZXLspTbjqlKgEAF87tctqWTfOmpOut/fk6UVLdpwWHCUpEOM+GJbaeKNPewkrdtGSq1s3Jlqtbew4AI8NTX2cHnBp2bx9Qaw2H2620tZcrZfVaOd18HQUAuHBxeTOU9/HPqHr7e6rctEG+9rbA3xy/Xw5Pu/xOZyAs0XEM2V5equJf/khxi5Yq7cY75U5K5mUAoCd+/zs1NTbaPXHfRx8Y8B4xlSQ6nTqVPyFCEqH4fF6dLgi0FWloaNAnPnZfv+tHRkbqzrvu0R133jVCWwgMHv8rBQAgTNU0turpHSe1+1TlgFprRLhdWjNnipZOz5LLlq4EAGDoJMZG6dbVc5VfVqO39uWrplsLjhaPX+1OKdqlHi04ntp2sqsFx8xMwnvAiLXWeG+jqt94aeCtNebMV+bVNyiCwSgAwBBzuFxKvWitEuYvUvkbL6v+4L6zy3w+OXxtfVpwNO7braYjB5VyxXVKWnclLTiAMFZYeFqvv/aKvX7J2nWaNXvOgO87ZcrUrusFp/IHcb8pumjNxcqZfDZkEcqy5SvV0FCvjIxMjaYzRUVqawsE0YKJiopSfHyCcqdO1cJFi7Xu0suUnJwyotsIDBYhCQAAwrC1xoZDZ/Ty3tNq9/RtrdFiwhG9qkfMnZyudQumKj6a1hoAgOFl2m9MuWKJdp4o1tajRfbvVvcWHBEdLTicHV9yF1c36Qcv79PK6Rm6bXmekmL5WwUMl+aTx1T53JNqKy0ZUGuNyNQ0ZV57s+Kmh+5ZDADAUIhISFTO7feoadlKlb7yvNoqBtCC45XnVL9zi9JuvVuxM+fyQgBhyLTM+M1jfziv+86cNUuPPv7koO938SXr7DQQn/zUZzQWTMmdel4/KzCWEZIAACCMHCut1e/fO67yur6tNcxZur5e4Yi0xFhdsShPU9ISR3ZDAQAK9xYcq2cHWnBs3H9Kx4qrupaZSkeeIC04tp8s177CKt28dKoum5stR0fFCQAXztvYoMoXnh54a42ICKWtvUIpqy6mtQYAYETFTp2uvAc+o+odW1S56Q352s7RgqOiXCUPP6i4hUuVdstdcifw/QcAAOGAkAQAAGGgtd2rZ3bla/PhkgG11oh0u3Tx3ClakjdJTvq8AwBGSUJMlG5eNUcF5bXaYFpwNDT324LD/L0zLTh2F1TqQ5fMUnpCDK8dcIEa9+9WxTNP2KBE39Yanr6/t/MWKuOq6xWRSAscAMAotuBYfYkSTQuODa+obv+ec7fg2L9bzSePKv2WuxS3eAWBWwAAJjhCEgAAhEH1iMfeOaqqhta+rTU8vUpHSJo3JUPr5ucqjtYaAIAxYmpGkj5yxWLtPFGiLaYFh8fbbwuOE2V1+u/ndunWZdN0OVUlgPNiQhEVzz5h+7b3ba3hsWfkdheZlq7Ma29SXB6tNQAAY4M7PkHZt96lpKUrVfbK82otL+23BYevqUllf/iNYvfuVPrt91JVAgCACYyQBAAAYVY9wuv3qzlIa430xFhduThPOamUlgQAjD0up1OrZuVo7uQ0bTpQoKNnKvu04DBVJSI6KiB5PD49ve2k9lBVAhiy6hHBWms4IyKVti7QWsOcuQsAwFgTmztN0x74lGp2blPFxtfla23t1YLDJb/9GxY4jmw6tF+Fp/7Ltt+IX7KSqhIAAExAhCQAAJiAjpbU6HfvHutTPaLNK7X2bq0R4dYlc6do8bQsWmsAAMZFC46bVs7WommZtgVHdf3ZFhzNnS04TPVkqkoAQ1o9wrbW6FU9ImH+okBrDfq3AwDGOIfTpZSVa2xbqPINr6pu366zy3xeOfy+QAuOzqoSzc0qf+K3aty3S+m3vV9u2kgBADChEJIAAGCiVY/Yma/NRwZWPWJ+rmmtMVWxUREju6EAAFyg3PQkffjyxdp9skTvHjnbgsPj86uxPXhVid0FFfrwJbOVnhDD/gd6adi/S5XPPDmg6hGRaRnKuu5mxU6bzn4EAIwr7rh4Zd9yp5KXrlDpy8+dbcHh98sZsqrECapKAAAwwTj8/l6nAVygN7ce0OGTZ7puz8nL0fIFM4byKQAAwAVWj4iPidQ1S2doWkYy+xIAMO7VNrXo1V0nVFRZ12O+2+noUVXCznM7deuyabp8bjalk4HBVo9wOJS65lLbXsPp5rwbAMD45vd6VfnuRlW9/Zb8Pt/ZBQ5Hj6oSnWLnLaSqBAAAI2T961vU3NLWdfumy5YpNzt9yB6f/9ECABBm1SMWTs3UpQumKiqCwwAAwMSQFButuy6Zr72nyrTpYMGAq0p86OLZykikqgTC12CrR5gzb6OzJ4/4dgIAMBwcLpfS112p+NnzVPL802otLRlYVYmb71L80pUEbgEAGMcYHQEAYJxXj3js3WOqpnoEACDMORwOLcnL0rTMJL22+4QKKwJVJcxJ8CY02O5Uj6oSJ8vq9c3nd+mWpdN0xTyqSiC8DKp6hNOp1DXrlLaW6hEAgIkpOnOSpt3/56p6d5Mq336zq6qECQw6/L4eVSV8zc0qf/K3aty/i6oSAACMY4QkAAAIg+oRi6Zlat18qkcAAMKjqsT7Lh54VYk/bT+pPaepKoHw0bDPVI94Qt6mxnNXj0jPUPbNVI8AAIRHVQnTTipu9twQVSWc8puwBFUlAACYEBx+f/fTAy7cm1sP6PDJM1235+TlaPmCGUP5FAAAhLXBVo+4dulMTc1IGoUtBQBgdNU2tfSoKtHJ7XT0qCph57mdtqrE5XOz5ewIUQATCdUjAAAYGL/X26eqhOVw9Kgq0Sl23kKqSgAAMMTWv75FzS1tXbdvumyZcrPTh+zxqSQBAMA4QfUIAACGv6rE7oIKfWTtbKUnxLC7MWE07t+tivV/oHoEAADDVVUi/7jSbr1bCUtXsY8BABgHCEkAADAOnKlu1CObDqu0tnkA1SOidO3SGVSPAADAnvDn0JK8LE3LTOpRVcLUVDQtqtqd6lFVIr+8Xt96frfuXTNTK/Iy2IcY13zt7ap84SnVb32n5wK/T06vJ/CL0MnpVOqadUpbe4Wcbr4uAgAgOnOSpt3/54GqEu+8ZStMGA6fTw5/e4+qEr6WFpU/8Vs1Hzus9NvukTMyih0IAMAYxv96AQAYw0xXrHeOleqp7SftGa6dvH6/Hdjx9WqatWhaptbNn6qoCP7EAwBwvlUlTPWmX286oiMltbpr1XRFul3sTIw7bWUlKvv9r9TWefar5ZfD65XDF3j/d4pMz1D2zXcqOnvyiG8nAADjoapE/Ox5Kn7+qXNWlWjYtU2thaeUee/HFMXfVQAAxixGUAAAGKOa2zx6/L1j2n2qsmse1SMAALjwqhJ5mcl6dffxoFUlYswJgR1fcr93rFSnKur1sUvnalJyLLse4yZk27BziyqefVL+9vZuC6geAQDA+YrKzBpwVYn2inKd+en/Ke2mO5Swep09BgUAAGMLIQkAAMaggsp6PbLpiCrrW7rm+RSoHuE9W1DConoEAACDkxgbZatK7DtVpo29qko0tAeCEu6OL7NLapr07Rd36+5V07VmZhZfcmNM87W2qOKZJ9Swe3u3uf7AAI5pr9FNVEamJt10B9UjAAAYqqoSLpf8TlOBzCG/x6OKZ55U8/GjSr/zg3LFxLCfAQAYQwhJAAAwxs78e/NQsZ7dlS+v92wvDU9He43ubaPjYyJ17dKZmpqRNDobCwDAOGbO6Fucl6VpQapKNLX7FeWSIl2BqhKm5dXj7x637TfuvWimoiP5rzTGntbiQpU9/iu1V1b0aq/hsSGJLg6HUi++VGlrr5DTzXsZAIDzrirx3iZVbn5T/o6/s6allemL6rd/XwOB28YDe9R65rQy7/2oonPz2NkAAIwR/G8YAIAxorG1XY++fVQHiqp7tNdo9Upt3QITxszsVF2zZAaDNLgg/+9Hj+jgiYIe8z7zgdt0xeql7FkAYVdVYvvxYr1z6LQNLBqtXr88/kBVCWfHl9w78yt0urJBH710rnLT4kd5y4EA856te2+Tql78U1fp78ACn5weUz3i7HGkOyFR2bfdrdjcaew+AGPemw8/pI2P/KLHvCU33KTbv/YPmug/i8/r1Z6XX9CBDa+r7MQxNdfVyduthVJS1iR98bEnz+uxMYRVJdZeodi8mSpe/4Taa2sC8/0+Odrb5XOfbb/hqanWmZ9/X6nX3aqkdVdSmQwAgDGAkAQAYFw5WliqO77+o6DLLpqXp4f/7uMaj17fc1xf+PYjQZclJqVo4bKV9rrT6dDlC/O0eFom/6nGmFZaUaW3tu/VoRMFOlNeqabmFjuIExsTrbiYaMXHxignM03TsrM0NSdLeZMnKTY6qs/jvLl1tx58/Jk+86MiIvSdv/ucUhITgj7/Ey+9qSdf2dhj3vwZU/VPn/vogB6/z/NFRtjtnjIpwz7OZSsXKy2ZKi7ARKoqsWpWjnJSE/TijmNqaG61802Lq8Z2f4/2GxX1Lfruy3t02/I8XT43m7/HGFXe5iZVPP07NR7Y26u9hjdwNms3cTPnKPuWO+WKiR3x7QRw1u4Xn9Mz3/yPfneJKyJSEdFRik9NU1ruVOUtW6H5V16j+NTUcbErq4oKtfeVF1WwZ7cqT59SS0OD/D6/ouPjFZ2QoJiERKVNnaasmbPsNGnWHEXFxY32Zo8Z7a2teuxv/0oFu3eO9qZgoO/1Q0dVdvyo2pqbbTQxwu1WZESEIiMjlBAfr6TEJCUlJqr92SfVfOKIMu/+iFxxZwO3+9Y/qRf++W/7PH5EdIw+uf5VxWdkBn3+zQ9+T2//5Ps95uWuvEgf/Plve8wL9fh9ni8mVtGJiUqbOVu5K1ZrwS13KnFSNu8DAMCEREgCADCu/HFj6C8Jth4+paLyak3OSNF44fP59er+Qn3niTdDrlNXW62WlmZNSk/VTStmKSOJL4+6K6+q0V/8xw/67LfHvvUPYbkdo83n8+mx517X8xvfs+/v3mrrG+1kHMkv7Jp/+1Vr9aFbrh7w87S2t+vJlzfqk/fcPERbfo7na2u3U1VtvfYcPqEnXnpLN1++Rh+46Sq5XIGzgwCMfyYk8aHLF+nV3Sd0sqS6R/sN03ojqqP9hmmJ9fS2kzpaUqsPXTJLcVERo73pCEMtBfkq+8Mj9uzUs/xyeDz2LNbuZ7qmX3GtUlZdTKgHGCe87W12aqmvV8WpfB3e9JZe+9mPdeXH/1wX3/shjVWm+sHrP/ux3nvi9/L7ega1jMbqKjsZhfvPhrvWfvA+Xf2pz47oto5l7/zutwQkLkBNSbF+8OF7+sz/h9c3a6Te661tbXZSo1RZbSpMBP7vO2fmTC2OiFDhD/9Hme+/XzHTZ/X7PO0tzTYEcf0//NuQbXu/z9fcZKf60hLlv73RhjBW3fdxXfb5r9CiCwAw4RCSAACMGx6vV8++0/0suZ7MWepPb9qlz7/vKo0HtU1t+s3bR3TkTLWKi4v7XdffVKsPXna5It2uEds+4Hz89A/P2eoMI2HDll265Yo1ys5I00jz+nx6ZsM7Kquq0V9+9O4Rf34AwycmMkK3rpqj3fml2nTgVFfgy7S+8vZqv7G/sErfen6Xbb8xPSORlwUjwhzz1m56XVWvPmf7nvfXXiMiOUU5t9+j6OzJvDrAOOdpbdWrD/5AkbGxWnHrHRqLnvv2f9tKGUNlyoKFuuiu9/eYlzN/gcajwfwsBza81mdeXEqKZq1Zq6jYQDUgc7b/+Tw2xsZ73Vtfp+Jf/lApV92g5Cuu73fdvX96Qqvu/4RSp03XSPN5PNry8M9UW1So27/5vRF/fgAAhhMhCQDAuLFxzzFV1jb0u86fNu/W5+4c+/0dDxVX67ebj6qhpV0VlRVqM2cY9KOoqFARnK2OMe7AsfygAYnkxHjNzctVUnycvD6v6hubVVhSruKKKjvQcyFBhcdf2DAsIYUrVi9VTEf7j5bWVp06U6aThX3DTO/tOahNO/bq0hWLh3wbAIwecxyxbPokW1nihe1HVdvY0qP9RrRbiug41qhpbNP3X9mrm5dM0zULJ4/5YxCMb97GBpU9+Vs1Hz3Uba7fttYwLTa6S5i3UFk33CZXdPSIbyeAwVt64y1dA+Ctzc0q2LNL1UVnK6912virX2j5LbePub83+bt2BB00jk9LU+7CJXaQ3+v1qrm2RuX5J22bAr/vbNWbYGZedLGdJoLB/Cw1xWf6zLvrn/5d05Yuu+DHxsi81z1tbao5dliVZ4pU39jUtU6g0pP5P7DDXlS//pJa8o/LG5vUb1Bh0w+/MywhhUV33K2ojrYfbY2NKjtySKUH9/VZ7/ArL+jA83/SgpvHZkALAIDzQUgCADBuPL3p3P04C8urte3wKa2el6exyOP16cU9BXptf1HXvKIzfb8ACdbK4eCJAi2YOW2YtxA4f29t71vpZd3yhfrsB+8I2pLCfFm058gJbdq+T07n+X3Ju2XvQR0/fUYzc3M0lO6+7jJlpCb3mPfGll366e+f7bPuq29vJyQBTFCZSXH64GWL9MbefB0pqrDzTLarud0vb7f2G+b77ud2ndLR0hrdt3aOEmIiR3vTMQE1nziqsid+Y88+7eI3AYle7TXcbmVec6OSlq4cc4OoAEK77KMfV/Kk7K7bXo9Hj//93+jEti091quvrLDhidQpuWNqd+59+cU+8xZec53u+Nt/kNPV9yvoptpa+7Pte/Ul2xYIZ5kB9t6SsrLYRePwvV67b7dOP/uUikuKdepMicz/ih2edvnNeo7A/5GbTxxTdVlZv895+NUXVbJ/ryYtHNpw/tpPfUFJOVN6zNv79B/04r/+fZ91d/3hMUISAIAJhZAEAGBcqGlo0oZdR3rMM1/63rxmkZ57t+fA7FMbdw5ZSOJj//lLbT2U32PeNz55p9532fJBP1Z1Y6se2XRY+eX1XfPa2ttUXl7eZ92ZedN0PP9Uj3lvbd19zpDEEy+9qSdf2dhj3uWrluizH7xdlTW1emHjVu06eEwV1bVyOB2alJ6qi5fO142XXqSoyIhhf7zeKmvqbOUBEwApKq1QY3OzHXyKj41RTmaa5s2YqitXL1V6St+zKn78u/V6a9uekI/9ob/+9z7z/vEz92nBrMB7o6m5RQdOFCi/sFgni0rsz9DY3GKn9naPoqMilZQQp6nZWVo2b6bWLJlv5w31dnRXW9/YsT9O6XRJuRoam+Xz+5UYH6tpOWY7ZumK1UsUGXHufdvS2qbnN76nLXsOqawy0KvcDPqvWjRXN112kd3HQ63gTGmfebdceUnQgISREBerdcsX2cl3jrPIQjHvl989/7q+/un7NNyuumiZNm3fqwPHe/5uHjlVJK/X1+PnNO8v874wIZCisgrVNTSprb1dMVFRSoiLUWJ8nCZnpmv6lGz7Ps+dlDHs2w/g/ERFuHXD8pnKTU/Uhn359vc9VPuNI8W1+ubzu3T/ujmaM6ln0Ao4X+ZM6+o3XlLNmy9376Qh+Xxyett7rBuZlmHba0RlMpgGjHcut1vLbr6tT0jCaKqrVarOPyTxyJe/oILdPU9CuO2rf2+rWZyv0uPH+sy75N4PBw1IGLFJSVp0zXV28nl7VsLp9ObDD2njI7/oMW/JDTfp/7P3H9CRXOed9/9UJzRyHuQZTI7M5DBHSRSDSCqQkijJkmXZXnmt9a69ftf2eb27//V67fXf2ZZs2RJFURKjRIoUcx5yOJzEGU7OOSHn0LGq3nOr0VXV6MYMZgZAN4Dvh6cP61YPgMLt5vB21a+e5/4/+tOMfz4aCsmm556Rfe+tke4zp6wPC6U1tbL4+hvl2ge/bN3hP97f/Ww/u6+9TTY/+3M5tGm99LW1WucmKhqaZPltd1htL/wZKvic63fJ9Lzbd7/yYMr45q//htz669+6oHlSwoODsuvtN+T4x1ul9dABK7QSi4SloKTUmqfaxUtl/lVXy+LrbpS8wsKUr1Pz13LwgLQe3C99bW0SHhyQ8EC/Fe5Q7WAKyyukZsFCWXjt9bL8ltslkD/2Z8+JnOdf/dWfy47XXx3zZ/35HTem7fva3/2zNF9+pUzWe7101WWSX98gRc//XObWtVmVFDX1iMfEUH/ekwgIGZHI2X+oacp7//Q38qV/e0wm2yWffUj2vPIrObF5Q8r+Mzs+tqpaeHypv2dkYEB2vficHNvwgXQdPSzD3d0Sj4StChXB0jIpqKiUyvkLpWb5Smm6arVULVw86b8DAADjQUgCADAtvLR+h8TiqSdOLl/YKN+696a0kMQbH+2RP/36vVKQ4YJ2thxu75Mfv7/faq+RpJumHDt1Jq3dQFN9rfzW5++UP/67H6Ts37hzn/z65+7KeKH+XNZs2iY//uXrEomlnkg/drrVemzcvlf+329/TQrzg1Py/eK6Lk+/8q68unaT1TJhtJ7+Aeux+9Axef6tD+TTN10jD99zh/h8E3eH0ZY9B+VfnnxhzOeTgYkz7V2yYfseeea1NVY4ZNXiie8DqgICz725Vn717odp7/NkmEQ9tu45aP253/nyfXLp0oVjfr+TLe3yf3/4pHT3OYEc5URLu/V4Z8PHk9KiIhRJv+NJhVDmN9Se82s9nsxBikwCfp/E4nErIKHsOnjMCiNcumSBTLbmhtq0kIT6b1hVxVBtRZS9h4/L3z32CxkcDo35vmrt7JEDx05Z1SlUYOUHf/ZfJ/3YAVw4dVJ+5dw5UlteZLXf6B5I/Pet8hKDUdMKSvhHKuIMhmLyr2/vlgeubJZbl9VzJz8uih4KSccvfirDB/aes71G6aVXyJxP3C2eQO6sgQFMjqKKqpyb2mjIaSmQ1HJwv9QuXnLOr/VMQCWJzuPH5Mk//q/WxXQ31dpDPT5++VfyuT/9Xxf9c7a9+rK8/s9/J7FwohVXkgoaqMfe996Vr/3tP0qwqFhykfrssvHnT1nhhFg4/fPKYHeX9VBBANVS4qE/+0tZetMt9vMH16+TF/7yz8b8/uEBFZgYkK4Tx2XPmndkzSP/Lvf/8Z/K/CuvnjHzfL7v9UBFlcz9+m9KxztvSO/Hm+39Hl19pjUSVSUy8AWDElfhiZEPvic2fWgFEZqvu0km25ylK9JCEiq0OdzbI0VVTsD/5JZN8sIffkdCvYkbM9zC/X3Wo/fkcTmzfatVoSK/rFy+82568AsAgGwY/9loAACy6Pm129L23X3dKlk2t1YW1KfegT0cjsobm/dIrlh/sNW6WOIOSEQN0+pp3t7akvbn71h9mVU1QN1lProywMYd7hPk47Nt32H5t2deSgs0uKlKCo+/+NaUfD8VkPjrR56Wl97bkDEgMZr6M6+8v1H+6pEnra/NFhU4+JsfPSPHM1RLuNiAxN8/9gurYkemgMRovQOD8n9/+JSs37Z7zOP839//WVpAYvT3+KtHnrLauEyk0qJED2W3R559Vb77xPPy4ce7pW2kosXFUlUYbrhiVcq+p15+Jy1wNBlUwGGs4EaygsTf/+TZjAEJANNfZXGBfOnmVVZgwi0UNyWkm6L+sZgiL2w5Jk+uPySx+IVVygGine1y5t//PjUgYRrW3afugITHH5C6z3xeau9+gIAEMIOo6go7Xn8lbX/NwkVSVnvuEPJUKygrT9v36t//tTz/F/9Ldr/zlvSccVpOTjR1Uf+nf/CdtICEW6i/X575738s3adOXPDPObxpg7z013+RduHeTVVXeOv735VcpC5y//LP/6d1fJkCEpNhoLNDnvnTP5K2wwdnzDxfyHvd4/NLzZ33Sv1nvyiePOdmEs1I/H/dvgPAJb+kTJbffV/Kvvf/6W+m5HNveKAv436/69hVBYmxAhIAAEwHVJIAAOS8g6faZM/x1DCB1+ORu1YnLpLevXqlfO/5NSnPP7/2Y/nsTZdLNsV1Q57felTW7XdO1KiLJ2FdJKabMjw0KEODqReyPR7NalehXHf5Cnn2jfdTnlfl+2+95rLzOo7+wSHr36p1xGVLF1oX5Tfv3J8Wcli7Zac8fO8dVguEyfx+T7+6xrrrfzTVUmPlombxaJpVQaJ91AV8VS1AVZ/46n2ftMaqakBBflBC4YjVomK0u25enbavoqwkbZ9qC6JCKepu/uKCfMnLC1htEdq7euXg8VMpwQX1O6rWDn/0mw/b+y72ONRr/NHu1FYyijom9VAnQFTFAXfAQO37/tMvSnNDndRVV6R83aO/fM2qajBabVW5rFjYbFVg2L7/iPU6qmOeSKrKxv5jp9JCLuu27rIeiqouMr+xVpY2N1ntQxbNa7ign/XFT99qVSxJBmdUMEdV/Lj+8pUyWVQAYvv+w2n71XtcvQeUj/ceSpt/9R5T7+2K0mLrtVOtN1SFkjMdXVNyggvAxPJ7vfKJyxZIY1WJvLPjqP3/CfX/dlVZosDvtN/YfKRd2vuH5Zu3LJfSAu7ux/gNH9wn7c88Job7ApGhW3eduuXV1FrtNdRdqgCmt7U/eVTyChKfnaLhkJzcuUO6TqZe0FftBe76z38ouUhVCji1a0da0GPXW29YD0Xd9V+7ZKk0rbpUFl17nTQsn5i1+2v/9Hcy1JN+obasrl6ar7jKagOhLryH+vtk8FxtDc4i+TNUO4mFq68VQzdk/wfvpV3M3/nm63LHb/1Hq6XIeDWuWGm1kFA2PffztOdVO5Dk+yP558/X2p/+WPa8+3b6E5omDctXSHXzAvF4PdLb0iInd+08a5DCH8y3Ajv5JSWSX1JqtdSIRSLS23JGTu3eJXrMqTKo5uedH3xfHv6/fzup87zg6tUSLCySyPCwVQVjtOT8upVUpwZfJ/u9Xrx0hQRr6uTMi89KWLWEUdNvmuIZVR1K0Qf75ao775EDb74q+sj5lra9u2X/G6/Isk9feGucc1Hhh6PrUtuuKqoKRF6xU7nj8AfvpgUk/PkFMvea66RojjqXYUiop1u6jx6R7uNHrZAOAAC5hJAEACDnPbc2tV+ocs2yZqkqTZS2v+e6S9JCEpv3H5fTHT3SUJ2e8J8KQ5GY/HjtfjnU6qTvDTGtO01HWplLe+uZtK9bsXCelBUnfq8bLl+ZFpLYe+S4dfd/dcX59TlfPK9R/vi3HpaCYJ41PnT8tPz/vvdYSiUHdbFZXeC+euWSSft+qoXG6x+kl1a87rIV8rsPP2C301Bf+/2nfiXrPk6tlvD6us1y9y3XWhebb7xylfVQ85EpnPCNB+486++waG6D/M/f/br1b98Y5V3bOrvlT/7hkZQwgQoYqIvlyYviF3McqqKDqqgxOiij2nrcdOUl9j4VRPnx86/Lmx9usfdFY3F59s335Ttf+ay9r6WjW7bs3p/2c26+6hL5D1+8T7zeRBGx4XBE/u8PnpCDxyf2bq47b7pG3tqwVfoGEkGasSoxqMCLeqjqGfXVlfLQXbda74HzMaeyXD5x/ZXy+gdOudJnXntPVl+y3P49J4p6/VUFkSdefkd6+wfTnr9qpdNTtaMn/Y6bP/tP35S5dXMyzsWeQ4m5ADD9LG2okpqyInlt60Fp7038vWeYYlWKUu03fFoiKHG8c1D+7rXt8q1bl8ncytwsvY3cocJzfR++J92vv2BVJBnZa1WOUC023MquXC3Vt3/KujsVwPSX6aKuW9W8Zvn8f/8zmbNg7LZ72XTN574gW198XoZ6usf8M+HBATm29SPrsfYnP5LKprly6zd/S1bcdscF/9zu06dk39r30vYvv+V2+ez/+z/F60/8HTnc1yc/+6+/J+1HDsnFaFyxSh7+q7+TvMJCa3x672557Pd+x7pInqQuaKuL6EtuvHnc33fh6uusx1ghiZu//k0pq6274ONWwYP1Tz+etr9kTo08+L/+j9QvTdys4W4poVpeBF0XxRUVpvj6P/6LddHf6/ON+Zo88u3fkMiQ87nwyEebJDw4KMGixPmOyZjnVZ+403r0trZk/O/pzu/8F8mF97q/rFzmfuWb0vn+29K96cOxf5BpSuitl2TpdTfKnrXOOa8P/uUfZMknPi2eMeb/QkWHBqV9/1557x//WoY629OeX3TrJ1LG/WfSz2l99bFnpHrx0rT94YF+q33HiU3rJ/SYAQC4GIQkAAA5TV0sf2n9zrT9917nlNqfX1dltd3Yd8JVscE05fkPtsnvfu72i/r5j/3JN8/7a1p6h+SHa/ZK96BzYV03TRmOm3YFRZWo7+5Ib9ugghFJ9XMqrUoC7vYO6uvf+2iHPHin0xN0PL7x2TvtQIOi7t5fOLfeqlDg1j7OdggX+v227D6Q1lJC3WX/Ww/eYwckFBVa+I0v3GNVWIhEnQoV6mu37jkgn7z+KrlYqgJDsgqDmuMTLe3S2dMn4WhUYrG4fV0iGAikhCTUe+vY6VZZsaj5oo9BzYcKO7hdvXJpSkBC8Xg88vA9d8hb67emVB7YvHOfVRnCP3Jy5OO9B9OqdObnBeQ3Pn93SnBAvXZq35/8/Q9lIpUUFlhVNv720Wekq7d/XF+jqin840+fswIbv3b/p87r533uEzdZwRTVikZp7eyWdzZ+LJ+64eLfH7/3F+Mr3arabKjjSHL/d5Gk5iJTSEJV1bjmkmXWA8D0VFYYlAdvWCnr9p6Q7UcT6xD19/BwzJSgTyTgSQQl+oej8s9v7pIvX7tIrpqf2iYMSDJiMel88ecy6OpXbgUk9LhVjjtJlemuvecBKV6SekELwMzWefyYvPG9f5R7//CPpbyu/qK+19f/fuLbFBSUllmVAlRLi/728bUoVJUynvuz/y6n9+yST/3H37ugn3tw/bq0VgUqGHHnd/6zHZBIHF+pfOI//Ed58o/+QC7Gnf/pv9gX7hUVFqhftkJO7U49b9HTkn4BOZv2r1ub3r5C0+ShP/sLqVuS/nkkkF+QsfJCRWOT9VDaDh+S9iOHrTYnKlShKnYkX4tAMD8lJKGqCLQeOiDNl1857ed5It7rmtcr1bffKQXz5kvLS8+d9WvnB/1yIBBIzK/6nU8ckx2/fEYuf+grF/27/Pu94ztn5gsG5brf+o8p+wIZAi8DrS0ZQxLB4hJZcsed1gMAgFxBSAIAkNPW7jgkXX2pd277fV755NWpd53fc92qlJCE8sK67fIfP3ubaCN3ck6FXSe75KcfqgvfzonsmJGoIOFW4tUlMqrMpwoGjL5YqkIT7pCEsvajHfKFT9087t+roaZKFjaln0QrL0m/m3U87Rcu5vupihOjLV8wz67K4KYuNqvntu07lPY9JiIkocIGr6/7SF5as37cF/ST+jO0s7gQB0eFSpRNO/fJw3/45+P6ehWwOHqqVZY0N1rjo6dS29Ioag6Deekl3psbaq2KHN19qS1fLtb8hlr5m//n21bVj3c3bktpE3I2r7y/0WrXccXyReP+Warly723XGtVpEh67s21csvVl8pUUAGJ3//GQ1ZVi6SVi5vVucaU87T//0eesuZa/bdTU5kI56jQhKpikum1ATC9+LweuXVVs9SUFcrb24/aVZXCqnqUVyToFdFEk3jckJ+tOyBneofk3svmWZWDgKR4f5+0PfWoRE4edybFVAGJmFWGOylQUSkNX3iY9hrALHXs4y3y09//jnzzu/8mxVW5F7qrXbxEvv3o47L5l7+Qba+8KD1nxle5buMvnrZaGCy67obz/pktB9Ir6akWB5nmZ+E111qVEcIDF/YZSFXzGF1xQSmuTG95FBkeu7peNqiL86M1rrwkY0DiXJ+jP/rlL2T9M0+OOyCQNNyb2lJzOs/zRL3XCxcslnlf/205/tf/Z8yvCeblyeJ582TvwYP2vg///buy8jOfk6mgAhKf/ZvvSVlDIhyTNG/19TL6w++zv/dbUlxTKxXzF0p50zwpn9cs1YuWSt0ll0mgwAm9AACQCwhJAABy2vMfpLfauGnVIiktzE/Zd/e1l8jfPfNWyr5THT3y0f7jVmuOyaZOFLy565S8ut3pGWuKKRFdJKo7HxhVsEFdSHn7vfT+jpcuXSBFBam/1/WXr5AnX3knZV97d6/sPXLCas0xHnPGaM2hwiaZfo/J/H59g+knMKrKx+7TWlVekravb3BiAgr/+tSvZO2W9Col4+GubnExJuJ3US07kgaG0nvGVpSlz2FSZVnJhIckFHXh/4E7brQeZ9q7ZN/RE1a45dCJ03KqrSOt2kWSqgJxPiEJ5d5br7PakCSDK2o+Xl27USaTCjRdtXKJPHzP7VJTlahGktQwp8r6vZ9/e13KfjXP6rFTjtr7VHWPK5Ytkgc/fatVNQbA9LassVrKCvPl5Y8OyFA4cadfTDetFhyq/YZHEqGId3aflpbeYfm1G5dIfoCP5BCJnD4hrY8/IvqAK7RpGuKJq2pTzv80Cxcskrr7HhRvMD1cCmD6+84Tv7DbKag7xtXd+aqkv7qo6qYuTH/45M/k0//p9yUXBfLz5cav/Jr16DpxXE7s3G61Sji9d490HDuaVvUh6eOXX7ygkESovy9jC4mxlM6pueCQRNkYFTy8gfTgs6kWADkkU2uI6ub55/19fvVXfy4733jtgo4hFhlVyWKaz/NEvddV+43K628RWT+qFYX6etMQ0TyyeOFCOXL8uERGqkkMdXbIlid+PKm/n6rEsui2T8rN/+m/WoGH0SrnL5TrfuPbsuGRf03ZP9DWaj2Ob3A+E6vWYAtuulVu/PbvyZwMARgAALKBMzIAgJzVOzgsa7YdSNvfPxyWv3z81bT9Rfl5MhhKrVzwy7UfT3pIIhLT5ckNB2X78a6UgEQoLhJ3fWAPBnxyz1VLpKzAL3+9x7kDIGk4FJbHXngjbX9+MC+tIsP7m7ePOyRRXFiQcb9q4XAhJvr7ZcOO/YcvOCAxsS7+hE5KK5AM328KC6lkpNrGqMcd115hjfsGhqwWGc+8vkZ03am4ohw/nVoNZjzUfx+f/eRN8hPXfzsvvrtebr7IahK3XnOZ9b0VbaQtjGqP0VBTLYvnNaQFmty+dPftsrS5SV77YLPsOXwsrcVMkvr9VUuZHQeOyP/4na9bLWsATG+15UXypZtXyUubD0h7byLEpv6qG4qZUuAT8Y78pbz3dI/8w+s75Fu3Lpc5JWP/fYKZb2D7R9L5/NNiWoGIEYYuHj21HVf56huk+tZPijaN1lsALpwvEJDKprlWWf7+jg7Z+15qcH7X22/mbEjCrXLuPOtxxb33W+Ohnh7Z/trLsubRH4jh/ntPtc47nP4ZeTwyBf39ZwmTadqF/z2qWnZk4nG1NsxV47kh4lwOb954wQGJkYOYsfN8se91LcNNJ4onHhPT67NabC5dtEh27NljP7fpxz+QlZ/57EUd96oHviB5hSNtMzRN/MF8ySspkar5i6Tu0sslvzTzTTJJN3/nD6Th8qtk65M/kRMfbRB9JMQxmhGPyaE1b8mxDR/Il3/4uNStnJrqjwAAnA0hCQBAznpp/Y6MFxe3HDhuPcbjjY/2yJ9+/V4pmKSS9j1DEXnkvb1yutupkGCIafUid9/QUFlSIJ+5ZomUFgStC6dxPf332nf0pPUYj40798mvf+6uaVeqv7QovbxiZ0/6nT/Oc+ltMEqLMoc0zodqaTFaSWGBfP2BO612CWo7Gfr4H//8qBzM0CZksuZDXYBfOLdh3N9DtXBIUsc9WleGObSfO882IxNBtci4/44brKoiqsWGWySWevJovD51/VXy6vsbpWPkvTQcjsiaTdsu6jhVS5vqMaqmjMflyxdZj1g8Lida2qW1o9uqAtPS0SU7DxxNqQCi2qb84o335I9+8+GLOmYAuaEoGJAHb1ghb+84IvtPddrXBFRQQlWU8I+02WjvC8nfv7ZdvnHzUllW57TtweygerN3v/Wy9K11X/hU7TV00Qxnnah5fVJz131SuuqyrBwngOybe8mlaSEJVT0hPDgowaKRi5vTRGF5udzw8NdkqLdHNv78qZTn4uOsMjBaQYaLuGdrA9HXfv7B7JmgqKIybZ9V7eA87Fv7Xtq+grIyufN3/7M0X3G1FW7weBMX+x/9zm/L6T27ZbaayPe6poKTpiELm+fJoaNHZTiUqCAZGRyQnS88e1HHecNvf0dK6xPtOy+UqhChHvFoRDoPHpDuE0el7/Qp6T52VI5vXGdVvUiKh8Oy7l//SR787g8v6mcCADARCEkAAHLW82sv7iKnMhyOyhub98hnb7pcJtqR9n559P19Mhh2Wi/EzURAwm1hbYXcecVCux2FqgJxscKRqGzcsde62306WTSvQd77aEfKvr1HjltVNAryU+/2URe6VZuGTN/Dbaye7oZhjFndIlP44r7br5cbr1yVNs/qAvd4XMhxZJoPdYfINx64c1w/c/T3nt9YJx9s3ZU2v+r3GB2oOX6mbcJbbagKEYvnNVqVI86lrDj9hG7xqDY64+XzeeWhu26Vf3nyVxPeEuViqddzYVO99UgaHA7Jd/73P0kk5hzjZAVxAGSHz+uROy9fKNUlBfLB3pP2nZOhuCmGVyTgVVVqNAlHdfm3d/bIA1fOl1uX1VltuTDz6aGQdPzipzJ8YK9rrylaPC6aKqs9wldULPWf+5LkX+TFCwDT22BPT8b9RobgfTZtf/0VaVy+0rqb/lyKylNb1in5JWO3YTyb+qXLZNdbr6fsO7Fju8TC4bSKEke2bJZQ/9QHxXNBw4pVVmUDt1O7d0rLgf1St2TpuL5HpvDJ9V/6qqz6ROrn12hoWNqPHJFsGavqkvpvJhnimA7vdffvoRmGeDWRFUuXyEfbnHNKsdDEtCOdCL5AntSuvMR6JIX7++T7n75ZYmGnNeiZHeltdQEAyIbcrVEFAJjVDp5qkz3HWybkez2/9sI/gH3jLx+VFd/4nykP1cJj/cFW+Ze3d6UEJCJ6ekBi9ZJGuefqxXZA4mRLuxy9gJYCmbw/6uL6dHDVyiX2XLgvZv/w2Vck7qoaoipt/OjZV6yL+27qa69csSRlXzAv0RJhtJOtzt0KmS6qj6ZCA6NDGt978oVxX2y/kONIzEdqZnXP4ePy1CvvWNUFMlHHpSph/MNPnpVHnk1tO3PF8kVp7TVCkaj8+PnXrUCFvS8ckR89l96y5mJ9tGu//OFff1/+6odPybqtu6xjzaSju1fe3rA1bb87SHC+brziEmmqmyPZdvjEGfnhL16xXkf3e9ptaDiUVk1mrNcbwPSlAg9XLqyX+1cvlYDfl7JeUC257BZJpsgLW47Kk+sPSXxUGyLMPNHOdjnzg39IDUiYhlVO2x2QCNY1yLxv/DYBCWCW62k5Ix+//ELaflUeP7+k5IK/709+/zvy53fcmPIYfQH9fO3/4H35/m98TZ76kz+UXW+/IZEhp9qiW29ri2x9Kf13ql+2/IJ+7qLrbki7KB4ZGpTXv/sPKUGS0EC/vP1v35PZaumNN6e3ITFN+fn/+BNpOZBeaVFVBdjyq1/K8e3O+RTvqM+uStvhQylj9bq/8Bf/O+Wi+FTLK8hcffJ8K2dk+73uLSqW/CYniKHWCc1z5khJcbHkgpbdO+SNP//vcvKjjaLHMrfZCPX1ih5PPaeiRzOfJwAAYKpRSQIAkJOeyxBsWNQwR371F7971q/bsOeI/MZfPZayb/P+43K6o0caqiemlPVHRztk+LiT1lcXOcK6SEw3U+8gvWKRLKpLvWtgTYYqEo011fLX/89/OOvP3HXwqPyff3s8rUKAuth8MS0Bplp5SbF8+qbV8tKa9Sn712/bY91Jv2pxs3Vn7e5Dx6zWBKN9+sZrpKI09YRAYX5QSooKpX8w9cTEX/7gCbls6UK7QkVFSbFVLUJZ0FQnW3YfSPnzqgLD6bZO67nB4bDsPXxc+ofGf1fGhRyHmo+7b1ktv3rnw5SveeGdD+Wt9Vtl6fwmKS9JVFwYGApZrRrOtHeJPhJ4uOXq1D6eddWVctWKJfLRqN9NVXjYf/SkrFg4z7o4v33/YekbyHwiZyJ63W7bd8h6qAuEjTVVUj+nyqoSoX52R3ef7Dtywv4d3G6+yrnj5HypSh5fvvt2+esfPS3ZFIlGrQCIeuT5/dJYWy1zKsus94eiqneo9/fo39/dNgXAzNI8p0y+dNNKeXHzAekdTFwwiBumDMVECvzqzoVEum3zkXZp7x+Wb96yXEoLplc7LYzP8KF90v70Y2KEXWW2VVUoPfXiQcnKS60WGx6fn6kFZpG1P3nUvrirx+PS19YqR7d+JLqr+ljS0ptuzsnqQ6qV0KGN662HCi5UzWuWqrnNVqDD+p1aW+TEjm0Zq2Bc8qm7LuhnVjQ0ytKbbpF9769J2b/tlRetnzXvsiskHovK4Y0bZLgv/TPmbGr/cN0XvyJrf/KjtOoQj/zOb1qVEarnL7BeN7Xv1O5dEh4ckIf+7C/tP1u3dLkc+PCDlK9XVTw6jx+TuqXLJDwwIMe3b5Xh3uzOc7Co2Pp9h0ZVYXniv/2+LFx9rQQLE5+xi6uq5fovfzVn3+vqv/GmL31d2t56Rfq2bRnZJ3Lp4oXywdaLr7x6sWKhkGx/9inr4Q/mS+XCxVLW2CR5xYkA12B7m5zYvEGMeOoNARXzF2bpiAEASEVIAgCQc9SF1JfW70zbf9fqlef82tXLmqWytEi6+gZTLto+/8E2+d3P3T4hx3ewtVca6gutbUPU3aCmuG/8LMrPk/uuWSLVpYk/k6Trhqz7OLUVgnLdZee+i2DFwmYpLS5MubCtKnerVg0P3nmLTCdfuvs2OX6mVXYeSL2Lo7OnT9ZsGrsViQpQfOmezK/hVSsWy7ubUk8SqLlyV9uYV19jhxPuuPYKeWnNBquigpuq8uGu9FFVXirBQEBOtY1dDeJijkP54qdvkxNn2q1QgdtQKCxb9xyU8/XNz98l+46etFo6uLV2dlsPd1UOFdyYrLBE8r89VUnjbNU03AGJVYvnX9TPu3LFYlk2v8n6/XOBaqdx+OQZ63Eud910zZQcE4DsKC/Kt4ISr245JCc6EhcODFNkKGZKvk/EN3Kh63jnoPz9a9vlW7cul6bK6dVnHmf//2H/+vek67UXrMohI3tFM3TR3BdPNE2qb/uUlF9zfU5e/AQwucZbyUGV6r/tN347518OdRG54+gR63Eu6qLx/CuvvuCfddfv/YGc3Lk97aJ496mT1iMpr7DQqsKRqW3EbHDL178pnceOyt733019wjTl1J5d1uNsrrj3ftnwzBNplRNaD+63HkmlNbUSyM+fsMoNF2Lx9TfKtldeStk31NMtO153KirWLFx0wSGJqXqva16v1H76PgnOqZW2t161wpV11dVSVV4mnT2Zwyip9U2nhqoc0rp7h/U4l6se/vqUHBMAAOdCuw0AQM5Zu+NQSsjhfEISHo9HPnVVeujghXXbrRPUE0k31V2gqQGJ+opi+fLNq9ICEoq6CJ7pgvR1l68Y113y16xalrZ/7Uc7Jvz3mmw+r1f+27e+LJ+59TrxjtEr1E39mbtvXi1/9K2Hra/N5At33iJlxeO/mKT+7H/5+hckP2/sO3XnVJTJH33ry1YFhPE63+NQvF6P/OE3vyhf+NTNaa1Izqa4sEAWzW1I219RWiJ/+u2vWVUqxqKqGvzBNx6S+upKmUhz6+ZIXuD87npV7+17b71O/sMX75uQY3j43jskm873opZ6fz/06Vvl1msum7RjApAb8vw+uf/apXLFwjp7n/pfuGrVFVWJiRF9w1H57ls7ZV9L5h70mF7UOq37tRek69VRAQk9nhKQ8OQFpfHBr0jF6hsISAAYU82ixfL1f/ielFRnv83caHMWLLTuJj8fmscr1z30sNz33/7kon52UUWlfO1v/1lK5tSM+WdUZYEv/cVfZ/wzvkDm1okzjap48Pn/8Wdyx2/9TnrrjXEoqqiQL/zPP5fAGO0slLK6evnyX/6N5Jdmt+LlLd/4lvW+mCnv9bIrrpGmL/6aePMTP/eSxYvH/LOxrg4ruJFrn309Pp/c+Dv/WVbd/4VJOyYAAM4HlSQAADnnhQ/SywYuaayRBfXV4/r6u69dJU+9szll36mOHvlo/3G5ZlnzhBxjzEhUkHBbNW+O3LqqecwL/+5qAklNdXOkYc74yuxff/kKeWt9osRikmpJsffICauNwnSiwg5fve+TctfNq61WEKp1yOm2Lrv6QWFB0LqAv3zBXLlt9eVWRYezqSwrkb/8g9+Ul9/bKDv2H7bakISjUevi01guXbJA/uq//ra8uGa91X6iu3fAusCv2pesvmSZfPqma6QgeH4nyy7kOJJBiQc/favcedM18sGWnbLn8HE5cabNmg9VjUC1bSguKpDaqgpZ2FRvvd7LF8yzvi4TVa3ib/7bt+XV9zfK5l377QoSah6vWLZI7rn1WitE8at3U9t8XKyH7rpNPvvJm+TAsVNW+5Rjp1ut92h3X79VtSMW1605LsoPSkNNtSxb0CQ3XrFqQlvGLGlukqtXprccmSrLF86Tv/1vv2O11Dhy6ozVHmVgaNhqlzIcDovf65OiwnzrtVTvb1VBY07lxLQCApD7PJomN6+YJ1XFBfLOjqN2652wqkrlFQl6xWo7FY0Z8oN398pXrl8sV80f3/oHucfU49Lx3JMyuGOra6cKSMREcy0OAhWV0vCFhyVQQeslAA5fIGBdjC6vq5eaRUtkyQ03ycJrrrUudOei2775W3LTV78hp3bvlNN7d0vrwYPS23Ja+js6JDI8ZLUN8eflSbC4RKqb50vTJZfKqk/cKWW1TnjwYqjv+e1HH5dNzz0j+95bI92nExUkSmvrrLm79sEvS0FpqQx0ple5KyibPi0sL5Z6/9zw8NfkyvsekJ1vvi7Ht22V1kMHJdTfJ7FIRPKLS6wwhHrPzb/qamm+4qqUr19w9Wr57R/+RNY//YQc3rTBmk9/XtB6HZfdcptc87kHrYod2aaCRL/57z+WjT9/Ug5v3iS9rWckGgolEqrT9L1eMG++zPv6b8vp554UtWKon1MtZ9rT3896f5+0P/1jqX7w18Tjn7zWXU1XrZZv/fJ1q6VG6+6d0nXsiIR6e6xHZHBAfP6ABMvKpHxuszRdeY2s+MxnpayhadKOBwCA86WZE3z76Xub98j+o05J4SXN9XLFigUT+SMAAJgyRzv65Qdr9koo4vRQjOim9XCn51U44tLmse9aAQAAcGvtGZCXPzooQ+GovU9l3wp8Kibh3Jn3wFXz5bbl9UzeNGNEwtL21I8ldMgpPy6mIR6rL7ezjixcsEjq7ntQvBdwRy8A4Pyc2b9XfvQ7v5kWGvj9Z1+UgixXPgDGy4hGpOWlX8rgwX2p+31+9Ya2x8HmBVLzld+0q08AADDd/OqdTRJynTO5++bLpalu4m4uyM3oMQAAOWDXqW75l7d32wEJU0wJjQpIBAM++dx1ywlIAACA81JbXixfunmVzClz2jSpFl6q/Ybhuoj+wpaj8uLHx6Zde63ZTB8alJZH/yVDQCKWEpAoX32DNHzhKwQkAGACvPvIv8vBDR9ad/Fn0nb4oPzyf//PtP1zL72MgASmFU8gT+o/9yWpvOHW1P1qnWE6bTbCx45Iy4/+WeL9fVk4SgAAch/tNgAAyGDj4TZ5euMh+/OlFZCIi8RdPcPLi/Kt3uKlBdz5BwAAzl9RMCAP3rBC3tx2WA6e6bL2qSymCkoU+NVdDYmKEu/sPi39oah86dpF4huj1RJyQ6ynS1p//H2JdXc6Ow1dPHo85a7lmrvul9JLLs/OQQLADHRs62ZZ9/hjEsjPl9oly6w2Jf5gUKKhYWk/ckRaD7qCa0maJrd/69vZOFzgoqiKplU33y551XOk5aXnxNR1OyhheH0iHq81jra2yJkf/KPUfuPbEqiaw6wDAOBCSAIAABd1l+bbu0/Ly9uOO/vElGHVK9wJ5EtNeZHcv3qp5Acmr78jAACY+VTo4a4rF0lBnl+2H2219qlM5pAKSvhEvFoiKPHRkQ4ZDMfk129eJnn+xIlv5JZIyylp/em/iz4wYO/TDF00V0DC4w9I/WcfksIFi7N0lAAws0VDITmx/WPrcS53/d4fSOPKVVNyXMBkKF62UrwFhXL6uSfFiESsfSqYqc5jmZ7EpZ94b08iKPG135JgUzMvBAAAI7gFBQAAV0Dil1uOpgQkVLlrdZHCHZBorimXz1+3nIAEAACYsLsBb1k5T25YPtfep7prqDVI3NVmY9+ZXvnXt3dZYQnkltDRQ9Lyo++NCkjEUwIS3vwCafzy1wlIAECWldbWycP/92/l6gc+n+1DAS5awdxmafrKN8VX6LRw0/RkSDOxjjSGh61WYMMH9jLjAACM0MwJbmz63uY9sv/oGXu8pLlerlixYCJ/BAAAEy6uG/L4hwdl23GnNLJhmjIUN62LFEnLm6rljkvni9dDzhAAAEy8PSc75O3tR6zwZlK+TxO/J1FRQqkuyZdv37FCKopo+ZULBndvk46f/8wuda0uSKgLE5rhpGz9pWXS+MWvSaCiKmvHCQAzWX9Huxz48AM5sWObdJ88KUN9vRLq67WeyysslNI5tVK7ZIksvu5GWbj6OvF4qcqEmSXW2yOnnvmZRHsSLdwU0+MRU7XfGGnhJh6PVH/2y1J8xTXZO1AAAMbpV+9sklA4ao/vvvlyaaqbuM/UhCQAALNeOBqXH72/Tw629tlzoZuJFhvugMRVi+rlhmVN1t2eAAAAk+VoW4+8suWg6K5SVkGfJgFXUKKkICD/4fYVUl9eyAuRRf2bPpDOl55N3qiZMSCRV11jBSR8RcXZOkwAADALxIeH5PQvHpdwi3MTq6l5xPS5ghIiUvHp+6XsptuzdJQAAORGSILbYAEAs9pAKCrffWtXSkBClbVW5a3dAYmbV86TG5fPJSABAAAm3fyR1l7BQKKXtBKOmxLWncVJ/3BU/vnNnXK43VnDYOqoSh/db78qnS+OCkjEUwMS+U3zEiWwCUgAAIBJ5isolKYv/7oUzl9k79NMQ7S4atXmrCO7X/+VdL36fErlMgAAZhtCEgCAWatzICT/9MZOOd09ZO+LGaYMx5wPiR6PJp++cpFcsaAuS0cJAABmo7qKYnnwhpVSlB+w90V1U0K6KeofJRzV5ftv75adJ52yyph8qq1G56+ekd41b7j3Whcg1IWIpKIly60KEt4gbVEAAMDU8AQC0vCFh6Vk5aX2Ps00xaOCEq51St+H70nHs4+Lqcd5aQAAsxIhCQDArHSqe1D+8fWd0jkQtvdF1IWHuBOQ8Pm8cv/qZbK0gd7RAABg6lUU58tDN660/p0Us9Yr6pJ8Ys0S102rbdiHB1t5iaaAEYtJ21OPysBHG5ydpiGemApIOOvIssuvlvoHHhKPz8/rAgAAppTm9UrtvZ+T8tU3ODutoEQ8JSgxuH2LtP7sh2JEI7xCAIBZh5AEAGDWOdDaa5WnHgyrcoOJiwyqfLUKSSTl5/nlC9cvl7nVpVk8UgAAMNsV5+dZFSVUZYmkuKp8FTfFSJZNNkV+vvGwvL7zJGWTJ5EeGpbWx/5VhvftTg1IjCphXXXT7TLnzntF83DKBQAAZIemaTLn9jul+vY7XXvTK0qEDu2Xlh99T/ShwawcJwAA2cIndgDArLL9RKf8+7t7JBozXAGJRPnqpJKCoHXXZk1ZURaPFAAAICEY8Mnnrlsu82vL7SnRDbFahNlBCRF5bfsJefajIwQlJkF8oF9aHvlnCR8/6uw0kgGJEZomNZ/+jFTeeKt1YQIAACDbKlbfYFWVcIc3rfWLodvjyOmTcuYH/yjxvt4sHSUAAFOPkAQAYNbYeqxDHvtgv+gjgQgVkFB3Yaqy1UlVpYXy0I0rpKyQ3tEAACB3+LweufeqJbJy7hx7n2GKDMVM0V1tHtbtb5WnNx4mKDGB4v190vKj70q0zdXSxNDFozsBCc3rk/rPfslqswEAAJBLSlddJg1feFg0v9MGzKPHRXMFJWJdnXLmkX+WWE93lo4SAICpRUgCADArfHSkXX667oBdUVDddanuvlR3YSY1VpVYLTYKg4GsHScAAMBYPB5N7rh0vqxe0mDvU/kIFfqMu4ISGw+1yZPrD4mhUhS4KOqOypZHviuxzg57n2bErQsL9uuSF5TGL35NipcsY7YBAEBOKlywWJq+/A3x5ufb+zQrKOGsaeI93dLyo3+WWHdnlo4SAICpQ0gCADDjbTrcJo+vP2i3irYDEq7rBovqK+X+1cskz+/L2nECAACci2rjcN3SJrntkvlWewc7KBFLDUpsPtIuT6w/SFDiIqg7Kc/86LspFwrUhQRNd+669BUVy9yvfFMK5jbz5gUAADktv75R5n7tW+IvLbP3qXWNCkskxXt7peVH37MqSwAAMJMRkgAAzGgbDrXKkxsOpQUk3DdWXjq/Vu6+cpFVxhoAAGA6uLS5xlq/qOoSSaODEluOdsjPPjwgOhUlzlusp8tqsRHv7kq929IVkAhUVFoXGvLm1FzMSwkAADBlAhVVifVLtbN+UW03EkEJ066kpVpvRDvbeWUAADMWV4MAADPWugMt8vSGw2cNSFy9uEFuXTnPuisTAABgOllcXyn3XbNUvK6g5+igxMfHOuWn6/ZL3N1jDGel7pxULTbivT2jylE7AQl1YaHpq7+RcicmAADAdKAqYTV95dclWFc/Kiih1jqJdaQ+0G8FRqPtrVk8UgAAJg8hCQDAjLR2/xn5xaYj9jhTQGL1kka5fmkjAQkAADBtzZtTJvefIyix/XiX/OQDghLjEevqsFpsqDsoxwxI1NRK45e/Ib6Cwgl7HQEAAKaSN5gvjV/8ugTrG8esKKEPDFitN6JtLbw4AIAZh5AEAGDGeW/fGXlu89GzBiSuXdoo1xGQAAAAM0BTdak8sHpZSuswtfaJuYISO092y4/XEpQ4G1VS+swj3xW9v29kj5kWkAjW1kvTl74uvoKCSXglAQAApo43GJTGL35N8hua7H2aYaQGJYYGraBEpPUMLw0AYEYhJAEAmFHe33dGnv8oNSAxNCogccPyuXLtEicpDwAAMN01VpXIA9cuE5/Pa+8LqaCEaxG0+xRBibNVkFAXAFRp6TEDEnUN0vilr4s3n4AEAACYGbx5I0GJpnljByWGh6T10X+hogQAYEYhJAEAmDE+ONAiv8wQkHDdRCk3rZgrVy9yei4CAADMFA2VJfLZa5eJ3x2UiKcHJX667oDEdSNLR5l7Yl2dciZjQMKZI3WHZeMXf8264xIAAGAm8QTypPHBr0rB3OazBiVaVFCivTWLRwoAwMQhJAEAmBE+PNgqz246ctaAxM0r58mVCwlIAACAmau+olg+d90yCfh9YwYldpzokp99eEB0d6mtWSrW3Sktj34vQ4sNV0Cica51hyUBCQAAMFN5AgFpUEGJ5gVnb73x6Pck2tGWxSMFAGBiEJIAAEx7Gw61yc83Hj5rQOLWVc1yxYK67BwgAADAFKotP3dQYvvxLnl8lgclYj3d1on+eF/v2AGJpnnS+NDXrDssAQAAZjKP3y8NX3hYCucvGjsoMaiCEv9itSoDAGA6IyQBAJjWNh1uk6c3HjprQOK2S+bLZfNrs3OAAAAAWVBTViSfv355elDCtUj6+FinPLn+oBizMCgR7+1JBCR6kwEJEU3X0wMSD37VurMSAABgNvD4/FL/+S+dPSgx0G+1KlMtywAAmK4ISQAApq2PjrTLkxsOJT+jWQGJ4VEBiVtWNculzTVZO0YAAIBsmVNamKgo4fPa+0IxU+KuxdKWox3y1IZDYroXUDOcqhyh7oCM93Tb+xIVJPTUFhsEJAAAwCwOSpy19UZ/nxU4jfV0ZfFIAQC4cIQkAADT0tZjHfL4+oNpAQn3jZA3r5wnl1NBAgAAzPKKEp+9bpn4XUGJ4VFBic1H2q3KXLMhKBFPntDv7hw7INHQJI0PUUECAADM7qBEw+cfloJ588cMSljBU1VRwhU8BQBguiAkAQCYdnaf7paffXjgrAGJG5fPlSsW1GXtGAEAAHJFbXmxPHDtMvGdJSix8VC7PL/l6IwOSujDQ9Ly439NKQ09OiARrG+UBisgkZelowQAAMgNHn8iKKFakKUGJfSUFmatP/5X0YcGs3SUAABcGEISAIBp5WhHv/x47X4xjbEDEtcva5KrFtVn7RgBAAByTX1FsTyweqn4vJ4xgxLv72uRt/eclpnIiEal7fEfSqyjzd6nGaMCEnX10vjQ18SbF8zSUQIAAOQWTyBgtSBTlbaS1PopUVEiQVXoav3Jv4kRCWfpKAEAOH+EJAAA00Zr37D8YM1eiccTCQkzQ0Di2qWNcs3ihuwdJAAAQI5qqCyR+1cvE++ooITuCkq8/PFx2XjYCRLMBKauS/szP5HwiWOpAQnXXZB5NbXS+MVfE2+QgAQAAEBaUOKLX0sPShhOUCJy5pS0PfmomK7wBAAAuYyQBABgWugdisi/vbNbQpG4E5CIpwYkVi9pkGuXNGbvIAEAAHJcY1WJ3H/N0tSghFpTJfuYicjTGw9Z7c1mAtU+pPNXP5fh/budndbdj66AxJxaafrS18UbzM/OQQIAAOQ41Yqs4cGvSrDOuTFJrafcVblChw9Ix7NPzOj2bQCAmYOQBAAg5w1FYvL9d3ZL71DUDkiE4iL6SMsN5cqFdQQkAAAAxqGpulQ+c/US0TQtsbYyRYZiTlBCtTVT7c1Um7PpruftV2Rg60Znh2GIx3WHY6CyShpVQCK/IDsHCAAAME2oiluq8pYKmCZZbTdcQYnBnR9L1yu/JCgBAMh5hCQAADktGtflh2v2SltfyN4X1kXirhISy5uq5cblc+0T/QAAADi7eXPK5FOXL7THKiihWm+ofxTV3ky1OVPtzqarvvXvS+97bzk7TBWQiNlDX3GJdaLfV0BAAgAAYNxBiYe+Kv7SMnufFUBVKdsR/RvWSt/at5lQAEBOIyQBAMhZcd2Qx9bul2MdA/a+sG5KTDdTTvDfcel8AhIAAADnaVljldy8cp49VhlU1XojGZRQbc5UuzPV9my6GdyxVbpe/WVqQCLuBCQ8eSMn+EtKs3OAAAAA05SvqNgKmnoLCu191jrLFZTofvNlGdiyIUtHCADAuRGSAADkJNW/8JmNh2XP6R57X9QwJeoKSNSUF8k9Vy0Wr4f/nQEAAFyIKxbUyZWL6u2xamem2polgxKq3Zlqe6ban00Xw4f3S8dzT6hfIsE0xRN3WmxoPp80PPgVyauuydoxAgAATGeBikppfPAr4vEH7H3WessVlOh44WkZ2rsrS0cIAMDZcVUJAJCTXt52XDYfabfHMcOUcNwJSJQX58v9q5eK3+fN0hECAADMDDcua7LalyWptmaqvVmSanum2p+pNmi5LnL6pLQ98SMx9eSxmqJZLTZG1pGaJvX3PygFjXOzeZgAAADTXrCuQeo//yXRvMlzc2ai9UZy3WWKtP/8MQmfOJrNwwQAICNCEgCAnLNm7xl5e/dpexw3TQm5AhJF+QH57LXLJD/gz9IRAgAAzByaplnty5pryu19qr2ZanOWpNqfqTZoqh1arop1dUjrT/9dzGh0ZI8pWjwmmun8HrV33SdFi5dl7RgBAABmksLmhVJ7z2edHWZi/ZUMSpixuLU+i7a3Zu8gAQDIgJAEACCnbD3WIS9scRLmumnKcMw5sZ3n98kD1y6T4vy8LB0hAADAzKPal9191SKrnVmSanOm2p0lqTZoqh2aaouWa+ID/dLy2PdFHxoc2aNO0MdTAhJVt9whpZdembVjBAAAmIlKVlwicz5xlz1W6y+1DksGJYxw2FqnxXudlroAAGQbIQkAQM7Y19IjT6w/aI8NMWXYVUHC6/XIfauXSmVxQZaOEAAAYObye71WOzPV1ixJtTtTbc+SVDs01RYtl+ihkLT+5N8k3tPtarGhAhJO1Yvyq66ViutuztoxAgAAzGTlV18nFdfdZI/VOkytx5JBCb2/T1p+8n3Rh4eyeJQAADgISQAAcsKJrgF59P19oo+UdbYCEjFTVemzy0DffeViqa8ozu6BAgAAzGCqnZlqa6bamyWptmeq/VmSaoum2qPlAiMWk7YnfyTRVud4NF0XzXACEsXLVkr1J+6y1pMAAACYHFW3fEJKL7ncWZMZKiih2+NYR7u0/uwHYkQjvAQAgKwjJAEAyLr2/pD8+7t7JBpLnMw2xZRQzBTXTYvyiUvny4Jap082AAAAJodqa6bam6k2Z0kqvKraoCWp9miqTVo2mYYhHc8+LuGjh+x9mhEXzXBOxhfMmy+1936OgAQAAMAkU4HUmrvuk8KFS1xrMxVeVRUlEiInj0v704+JaVWZAAAgewhJAACyqm84Kt9/Z7cMheN2QEK12BgpKGG5YflcWTF3TvYOEgAAYJZR7c1UmzPV7ixJrdFUta8k1SZNtUvLBtM0pevl52Ro93ZnpzoJ77pbMVhTJ/Wf+5J4fE7YAwAAAJNH83il/oGHJL+hydmn1meuEOvwgb3S8fzT1noOAIBsISQBAMiaaFyXH6zZIz2DEaeCRFxEd6ojy2Xza+WqhXXZO0gAAIBZSrU5U+3Okm0q1HlsVVEiGZRQbdJUu7QzPVPfW7pv3bvSv2mds8M0xOO6I9FfXiEND31VvHnBKT82AACA2czj90vDFx6WQGW1s0+t01xBicFtH0nPO69l6QgBACAkAQDIEpUWV3cfnu52TqpHdJG4q8fGkoZKuWXlPMojAwAAZIlqd6baniWppZpqi6b+UVS7tEfe2yuD4diUHdPwgT3S/caLqQGJuPPzvYWF0vjQ18RXWDRlxwQAAACHN79AGr/4NfEVl6QGJUznzqjeNW/I4K5tTBsAICuoJAEAyIq3dp+S7ce77HHEMCXq6rHRVF0qn7p8IQEJAACALFNtz1T7syS1ZFOtN5JBie7BiDy2dp/E3eXAJkm0vVXaf/5TVYIswTRTAhKeQMAKSATKKyb9WAAAADA2f0mpNH7x18QbzLf3Wes2V1Ci47nHJdJyimkEAEw5QhIAgCm362SXvLLthD2Om6ZE4k5AYk5Zodx79RLxevjfFAAAQC5Q7c9UG7QklYdQVcCSDrX1y/Nbjk7qMeihYWl7/BExwuGRPaZouhOQ0Lxeafj8wxKsoVUbAABALsirqpaGB78ims9n70u0SEucBzRjcWt9Fx8cyOJRAgBmI64+AQCmlOpZ/dMPD9hj1dNa9bZOKsrPk/tXL5OAz8srAwAAkCM0TbPaoC2scyo0qCpgMVertHUHWmXdgZZJ+fmmrkv7049JrLszuUc0PS6a6fz82ns+KwXznNYgAAAAyL78hiapv/9BZ4dpihZ3ghLxvl5pf/JHYlrhCQAApgYhCQDAlFG9qlXPatW7WjFHBSR8Xo/cd80SKcjz86oAAADkYFDizssXSmVJgb0vFDetqmBJz310VA619U34z+5+40UJHXaCtpqhi2Y4pZorrrtZSlZcMuE/FwAAABevaPEyqbrlDnusmYZoulOWLHzimHT+6hdiutaVAABMJkISAIApoXpUq17Vqme1HZCIm+K6+VDuvGKRVJcW8ooAAADkKL/PK/dds1SCAX9KUEJVB1MMw5RH398nXYPJlhgXb2DLBun78D1nhwpIuE6qFy1aknLSHQAAALlHhVqLl61MCb2qdV3SwNaN0r9hbZaODgAw2xCSAABMCdWjWvWqTlI9rFUv66TVSxplkat8MwAAAHJTSUGe3HP1YquyhKJu+AvFVAQ2EZQYjsSt6mGRmHPS+0KFTxyVzhd/4ewwjZE+1gmBymqp/czn7WMBAABAblLrNdUeLa+m1t5nretM5wRh12vPy/Dh/Vk6QgDAbEJIAgAw6VRvatWjOkn1rlY9rJNUb+trlzTwSgAAAEwTjZUlctslzfZYLe3CrjbSLT3D8viHBy6qZHK8t0fanlD9qZNhCzMlIOEN5kvDFx4Wb17wgn8GAAAApo7H75eGzz8s3gKnkqwnrtZ3I2tGw5T2px+TWFcnLwsAYFIRkgAATCrVk1r1pk5SPatVSeYk1dNa9bbm7j8AAIDp5ZJ5NXJpc01KEDbiCsLuPNktr+04eUHf24hGpfWJR0QfGhzZY4qmTqAnQxcej9Q98JAEyqlEBgAAMJ34S0ql4XNfEs3rda3zYnZQwgiFpPXxH4oRnrj2bQAAjEZIAgAwaVQvatWTWvWmVlSvandAQvWyVj2tVW9rAAAATD83r5wnjVUl9liFJFRYIumNnSdl2/HzuxNQVZ/o+OUTEm05be/T9LhorlLMc+74tBQ2L7jo4wcAAMDUy2+cKzV33muPNdO01ntJsY42af/FT8U0XL16AQCYQIQkAACTQvWgVr2oVU9qRfWotnpVj5wzV5UjVC9r1dMaAAAA05PX45G7r0pd06lQrO5qs/H4+oNyqjtZEeLcet9/S4Z2bbfHmqGL5jpBXnrZVVJ25eoJOX4AAABkR+mlV0r51dfZY7Xe0wwnKDG8f4/0vPVKlo4OADDTEZIAAEw4dfef6kGtelEnqR7VrurLVg9r1csaAAAA01t+wC+fuWap+FzVwVRQQlURU+JxwwrPDoSi5/xeQ3t3pZ4MN42Uuwqtuw4/dQ+t2gAAAGaA6ts/JYXzF9pjTddFXOHY3rVvy+COLVk6OgDATEZIAgAw4VTvadWDeqyyy6p3tephDQAAgJmhqqRA7rpioSoXZo3V0k8FJdQ/Su9QVH70/j6J62OXTI60nrHKKttMQzxWf2qnf3V9Sv9qAAAATGeaxyt19z8ogfJKe59Hj1nrwKSO55+SyOkTWTpCAMBMRUgCADChVM9p1Xs6SYUjVEgiSfWsVr2rAQAAMLMsqK2Q65c22mOVh4jozvPHOgbkF5sPW1XHRtOHBqXtiUfEjCarTaT2pdb8fqn//JfFV1A4ub8EAAAAppQ3mC/1X3hYPHlO+zaPtQ5MrBnNWFxaH39E4v19vDIAgAlDSAIAMGFUr+kn1h+0x6oXtbqDMEn1qlY9q1XvagAAAMw8Vy+ql8X1zp2AUd2UqKui2MZD7bJ2f0vK15h6XNqefkziPd0pAQnNFaaou/dzEqypm4LfAAAAAFMtr7JK6u9/0K5KJqYpWtwJSugD/dL21KNixJwqYwAAXAyuUgEAJkQoGpcfr90vsXiiHJ7qQe0OSKge1apXtepZDQAAgJlJ0zT55OULpLrUqfgQjpsSdwUeXth6TI519Nvj7rdflfDRQ8730HXRXL2oK2+8VYqXrpiS4wcAAEB2FC5YLNW3fcoea6ZhrQuTIiePS/drL2Tp6AAAMw0hCQDARVMlk5/eeEi6BsKJ8UhAwr5pUNOsHtWqVzUAAABmNr9XhWOXSH6eE4611oYjdwIahimPfXBAhiNxGT6wV/rWvuN8saECEs7J8KIly6Xyxtum9hcAAABAVpRfc72UrLzUHlvrQtfasH/TOhncvY1XBwBw0QhJAAAu2vpDbbL9eJc9Vr2nVQ/qJNWbWvWoBgAAwOxQnJ8nn7l6iXg8iZLJqpCECkqof5TeoYg8885WaX/2Z84XmcZI/+mEvOoaq82Gqk4BAACAmU+t+2ruuk+CdQ32Pmt9aDonGjuff1piPc55SAAALgQhCQDARTnTMyS/3HLUHqtSyqr3dNKi+kqrNzUAAABml7qKYrnjkvn2WIVoVZjWYhriffM56ezoTu4QzRWQ8ObnS8MXHhZPIDDFRw0AAIBs8vj80vD5L4uvqNjZZ60TR6qShcPS/sxPxHStHQEAOF+EJAAAFywS0+WxD/ZLPJ5Ic6sSyuoOwaSyonz55GULuPsPAABglloxd46smFttj1WYNmaaMv/oVinrbZHWvmEJxeJWQEJT5SZG1H3m8+IvLcvSUQMAACCbVECi7r4vWC18LWZqoDZy6oR0v/ly9g4QADDtEZIAAFywZzcfkfa+kLWtSidbJZRHzm17PR65+8pFEvB5mWEAAIBZ7NaVzVJenG+PCztOS/Oxrda2Wjt29w2LGE4J5YrVN0jhgsVZOVYAAADkhoK5zVJ14232WFPrRSNZlkykb90aGd6/O0tHBwCY7ghJAAAuyOYj7dYjKaonSign3bxynlSXFjK7AAAAs5zf55W7r1wsXq9HAtGQXL7vXTGMRLLWr2lSEtAkHEvcGRisb5SqWz6R5SMGAABALqi4/mYpmDc/te2G6ZyAbH/uCYn39Wbp6AAA0xkhCQDAeWvrG5ZfbD5sj+OmKRHdKY+8sK5CLpk3h5kFAACApaqkQG5dOU+u2L9GgtFhq6O0aZpSXeAVj6ZJXDdF9wWk/v4HRfNSiQwAAAAimsdjtWHzFjg3YiXabiTOQxrDw9L+i5+KqTsVJgAAGA9CEgCA8xKLG/LYB/slGkukto2RNhtJxQV58snLFoiW7BkIAAAAiMjK1l3SHEpWIjNl4YIhqWzot+dGv/YTohcUMVcAAACw+YqKpe7ez9ljzTRHghIJ4WNHpOfd15kxAMB5ISQBADgvz289Ki09w/Y4rKrcjWQkVDDi7isXSZ7fx6wCAADAZp4+JvLB61JVXCA+r1cWL+2T2qYBqVt1SvwFEfGtuFw8zYvlVPeQ3YoDAAAAUAoXLJKK626yJ0MzDBHDqR7R+/6bMnx4P5MFABg3QhIAgHHbdrxTPjzQao8jhilx10nsG5c3SW15MTMKAAAAmxkaEnn5Z1b/aI9Hk/lNIksuT1SU8PgMabimVbxX35hYX8Z0aelzArkAAACAUnXT7ZLf0ORc3FLVJMxEpVvVfaPjFz+T+OAAkwUAGBdCEgCAcekcCMvTGw/ZY900JeJqszFvTplcsaCO2QQAAIDNVCXHXntGpL83cRLCq8uCy46I5kmWIhMJVsSkMvaS/TW9QxHpHY4wiwAAALBpXq/U3f+geIP5zj6r7UZiXakPDlpBCWv9CQDAORCSAACcU1w35Ccf7JdwNFHGzhRThl0BicJgQO68YqHVbgMAAACwffyByOHd9rBhyVEJFIRVNkKspWMgz9ooibwjwdhO+8+d6R22qkoAAAAASf6SUqm95wF7rJmmaLqzZgwdPiC977/FhAEAzomQBADgnF7adlxOdg3a45CqZpfMSGiafPrKRZIf8DOTAAAAsJmtJ0XecypElNd0SGldpz3Oq6wWzeusIauHfioeoy/xtYYpp3oGxXC1dgMAAACKFi+T8quvsydCM3QR9RjR886rEj5+hIkCAJwVIQkAwFntOtUt7+09Y4+jhilx18nqa5c0SGNlCbMIAAAAmxkJi7z0uMjInX2BYFjqlh5znY3wSKB+rng8TiUyjzkoVUOP2WlcVcWsrT/ErAIAACBF1a2flGCN0/bXo7vu6DJMaf/5T0UfHmLWAABjIiQBABiT6gf95PqD9lg3TQm72mw0VpXINYsbmEEAAADYrD7Qb/5CpHekaoRmyNyVh8TjS97hp4knL180j8dqu+FxtWzLj++T4ohTIrl7MCx9oSizCwAAAOfCls8ndQ88JJ5AwN6n6TGrSbAS7+uVjueeSKxLAQDIgJAEACAj9SHiZx8ekOFIPDEWU0KugER+nl/uvGJRykltAAAAQHZ/JLJvmz0Rdc2nJFjqtG7z5OVZAQl7rGmiudaUFaEXxR8/aY/P9AxJTDeYWAAAANgC5RVS8+n77LFmmqKNVDFThvfvkf6NHzBjAICMCEkAADJau79FDrf12+Owau/nCl/fecVCKQo6aW0AAADAHOgTefdX9kQUlfZLRbPTuk3z+a1H2skJFZSwR3GpHn5UxExUkDAM0wpKcCcgAAAA3EpWXCKll13lrDUNXcR0wrXdb7wosa6R6mYAALgQkgAApOkcCMlL247b45hhSkx3EhJXL6qXedVlzBwAAABsVojhjZ+LREKJEw7euDSsOCx2kQjVXiMvL+OMWW03XNUl/HqrlId+aY8HwzHpHabtBgAAAFLN+cRdklc9xx574qoqbuI8phmLScfzTxG2BQCkISQBAEg7uf3UhkMSiydS14aYEnYFJOoqiuW6pU3MGgAAAFLt3ixydJ89bFh8TPz5EecERF5Q3PUiMgYlXG03SiLvSV5stz1u7Rum7QYAAABSePx+qbv/IdF8vuTZTdH0RPtgJXzssPRvWMusAQBSEJIAAJy9zUZcBScS2z6vR+68fKF4PGOf3AYAAMDsYw70irzjtNkore6S0nqntLEWyBPN4z3n97HabriCEtXDPxPNGLS2absBAACATPKqqqX6tk85a0/DEFGtN0Z0v/kSbTcAACkISQAAUtpsvDiqzUbccKpI3Lh8rpQWBpkxAAAAjGqz8QuRaNgaewNRqV921HXmwSsef2DcM+auJuE1+qRy+Cl7TNsNAAAAZFJ25WrJb5rnrCl1FZKg7QYAIDNCEgCAlDYb8THabDRUlsilzTXMFgAAAFLtcrfZMKVp6RHx+p0Sx57g+YVsR7fdKIxtlYLoZntM2w0AAACkrSE1TWrvfkA0v99el9J2AwAwFkISAIBxtdn45GULUkofAwAAAFabjXedNhsVtR1SVN2b2mZDO/9TD6PbblQOPy0eo8/apu0GAAAAMgmUV0j1rZ901qK03QAAjIGQBACANhsAAAC4sDYbr//cbrPhy4tI7ZLjF9xmYzR3NQmPOSxVwz+zx7TdAAAAQCa03QAAjAchCQCY5dTJ7SfX02YDAAAAF9Bm49j+5KpSmpYdEY9P9X6+sDYb52q7kR/bLYWRD1Pbboy0igMAAADsthv30HYDAHB2hCQAYJZ7f3+LHGkfo82Gz0ubDQAAAJy7zUZ9uxRWJtphXEybjXO13agIPSseo8dpu9E7lKhoAQAAAIwIlJ2r7caLEuvqZL4AYBYjJAEAs1hHf0he2uaURI4ZpsQN5yTzjcuapLTw4u4ABAAAwMxus+FXbTYWnZiwNhtnb7sRkqqhx+0xbTcAAAAwVtuNgrnNzjpSj1vVzxQzFpeO558kbAsAsxghCQCYzW02NhyU+EiJYkNMCetOQKKhskQuba7J4hECAABgOrTZaJzgNhvnbLsR3yMFkfX2mLYbAAAASFtDaprU3H2/aH6/s88KSiSEjx2R/g3vM3EAMEsRkgCAWdxm42j7wNhtNi5fkFLaGAAAAEhrs1E3OW02ztV2ozL0C/EYiZ9L2w0AAABcWNuNl2i7AQCzFCEJAJiFztVm46blc6W0gDYbAAAAGLvNhi8QldrFk9dm41xtNyqHn7THtN0AAABAJrTdAABkQkgCAGaZc7XZaKwqkUvmzcniEQIAACAn7dzkarMh0rDk6KS22ThX242C2A7Jj26xx7TdAAAAwFhtN9xhXtpuAAAISQDALPPevrO32fjEZbTZAAAAQCqzv0dkjdNmo6y6U4rn9Ex6m41zt934uWjGkLVN2w0AAACM1Xaj6rZztd3oYPIAYBYhJAEAs0j3YFhe3k6bDQAAAJynt54TiUasTY8vLrVLjk9Zm43R3NUkvEa/lIeeS2m70ReKTtmxAAAAYHoou+IaKZjbbI89elxFga1tMxaXzl89k2gvBwCYFQhJAMAs8sLWY7TZAAAAwHkxD+8WObLXHtcvPC6+vNiUtdk4V9uN4uh6CcScNiBtfSHRDU5wAwAAwLWG1DSpvfuBMdtuhI4ckqFd25gyAJglCEkAwCyx70yP7DjRZY8jOm02AAAAcHZmPCbyzgv2uKi0X8oaOqa8zca52m5UDT+hbgG0tuO6IR39oSk/JgAAAOQ2f1m5VN/+qdS2G6Zhj7tefV6MSDhLRwcAmEqEJABgFlAnip/96IgzNk2J6c7ddTcua5LSgqm9AxAAAADTwKZ3Rfq6E9uaIfXLjmStzcZo7moSfqNDSsOv2uOuobCEY86dgQAAAIBSevnVkt80L2PbDX2gX3rWvMFEAcAsQEgCAGaBNXvPSGd/IgVtiinhuBOQqC4tlEuaa7J4dAAAAMhFZm9XIiQxombeaQkUOnfWefLyJJtGt90oC78lPr0lMTBFWnqH6SsNAACA1DWkpknNp+4R8YxcHjNN0Qzdfr7vw/ck2t7KrAHADEdIAgBmuJ6hiLy+66Q9jhki7hbNt61qTjm5DAAAAFjefUFEtdsQkUB+WKqaW5yTy36/aB5v1ifKarthj+JSOfyk3VNuOBKXvlA0i0cHAACAXJRXXSPlV662x5ru6ktsGNL18rOEbQFghiMkAQAz3PNbjko8nuitZ4yqIrFibrXUVRRn8egAAACQi8zDe0TUIzGShiVHRfOM9GtWwYRAdqtIuHk8TkwiGD8kBdEN9ritLyS6OyEMAAAAiEjlTbeJr7DIngvNcFq1hY4ckqHd25knAJjBCEkAwAy2r6VHdpzosscRp3KcBPw+uWHZ3OwcGAAAAHKWqapHvPOCPS6t7pbCyj57rAIS7voN2aaORZVNTqoM/VI0Y8jajuuGdPSHsnh0AAAAyEXevKBU336nPdYMQ8QcCQWLSNerz4sRjWTp6AAAk42QBADMUOqE8HObjzpj05SY7txFd/3SRinI82fp6AAAAJCzNq8R6UsEbT1eXeqWHHOe83jF48u9NaS7fZzHHJTykBPy6BoKSzjm3BkIAAAAKMUrLpH8pnnOOlJXa8bE+VO9v0961rzBRAHADEVIAgBmqDX7zth3zZmj2mxUlRbKJc01WTw6AAAA5CKzr1tk4zv2uKb5pPjyYvbYk5c7bTbctFFBieLohxKIj4Q7TJGW3mH6SgMAACB1DalpUvPJe1T/tpF1oyma4ZTi7Vu3RqIdbcwaAMxAhCQAYAbqHYrIGztP2uOYIeJuxXz7quaUk8gAAACARbXZUO02RCSvcFgq5jonhTWfXzSPN2cnSq1vnRWuIRXDT9slk4cjcekPRbN4dAAAAMhFeXNqpPzK1fZY03UrLGExDOl6+TnCtgAwAxGSAIAZ6PktRyUWT5wQNkZVkVjeVC11FcVZPDoAAADkIvPIXpHDu5MjaVh8TDRtZB2pAgg5WkXCzeNxYhJ5+nEpjH5oj1v7QqK7k8MAAACAiFTeeJt4CwvtudAMp1Vb6PABGdq9nXkCgBmGkAQAzDD7W3pl+4lED2kl4lSIk4DfJzcun5udAwMAAEDOMlX1CFVFYkTpnG4pqOi3x1ogT9x1GnKVOkZVNjmpIvSCaMawtR3XDekYSLSjAwAAAJK8waBU33ans6Y0DLsimdL92gtiRCNMGADMIIQkAGAGUSd+n918xBmbpsR0526565c2SkGeP0tHBwAAgJy1+T2R3k5rU/PoUrvouPOcxyse3/RZQ7rbynnMISkLv2iPuwbDEo65UsQAAACAiJSsvFTym+Y560hdVZNInFeN9/VK75o3mScAmEEISQDADLJm3xnp6E/cHWeOarNRVVIgl8yryeLRAQAAIBeZfd0iG9+2xzXzTos/GLXHnmnQZsNNGxWUKImsFZ9+OjEwRVp6h+grDQAAgNQ1pKZJzSfvUf3bRtaNpmiGE67t+3CNRDvbmTUAmCEISQDADNE7FJE3d520xzFDxN1y+bZL5qf0aAYAAAAs7/5KRLXbUO3ZghGpnNdqT4zm84vm8U67iVIhCWfla0jl8M/t0XAkLv0hJwQCAAAAKHlzaqT8ytX2ZGi6boUlFFPXpeulZwnbAsAMQUgCAGaI57celahKRlingVOrSCxvqpb6iuIsHh0AAABykXl0n8ihXfa4buFx0Twj/ZdV0GCaVZFwcweEg/EDkh/92B639oVEdyeKAQAAABGpvPE28RYW2nOhGartRkLo8AEZ3rODeQKAGYCQBADMAAdbe2X78S57HHG1WQ74fXLj8rnZOTAAAADkLFP1WX7neXtcWNovxTXd9lgL5Im7HsN0o45dlU1Oqgj9UkwzcZI7rhvSMZBoUwcAAAAkeYNBqb7tU86a0jBEzJEQsYh0vfq8GLFEFTYAwPRFSAIApjnTNOXFj4/b47hpSkx37oq7bmmjFOT5s3R0AAAAyFk7N4n0dI4MTKlfcsx5zuMVj2/6ryFV240kn9EpJeF37HH3YERiceeENwAAAKCUrLxM8hudm840FS6WxPnWeF+v9G9cy0QBwDRHSAIAprntJ7rkZNegPY64AhKVJQVy6byaLB0ZAAAAcpUZjYh8+KY9rqztkLziYXvsmcZtNty0UUGJsshrohn9dti4nWoSAAAAGL2G1DSZ86l7rPZz1tg0ExUlRvS+/5boIaqSAcB0RkgCAKYx1Uf5le0n7HHMMEV33Qx326rmlF7MAAAAgGXLWpHhAWtT8+hSvfCkPTGazy+axztjJkqFJJIrYo8ZlrLQy/ZzvUMRCcdcveoAAAAAEQnOqZWyK67JWE3CCIWkb+3bzBMATGOEJABgGtt0uE06+hOpZVPMlCoS82vLpaGyJItHBwAAgFxkDg+JbF5jj2vmnRFfXrKvsiZaICAzjeYKDpdE14lPP2OP20fW0wAAAIBb5Y23isfvrI01wwnX9m14T+L9fUwYAExThCQAYJqKxnV5badzx1/MEDGSGQlNkxuWNWXt2AAAAJDDNr0tEg1bm75AVCrmtthPaX6/aNrMO1XgUeEPu+2GIeWh5+3nBkJRGY6qOwMBAAAAh6+gUMqvvcEea7oKSSROwJqxuPSseZ3pAoBpauad+QCAWeL9/S3SPxzNWEVieWOVVBYXZPHoAAAAkIvM/h6Rj9fZ49r5J8XjHenXps3MKhJJ7i50BbFdkhfbZ4/b+obFNJ31NAAAAKBUXHO9eAsKRwUlEga2bJBoZzsTBQDTECEJAJiGhiNxeXv3KXsc1UWS53Q9Hk2uXdKYvYMDAABA7lr3usjIid28wmEpre+0n9L8AVVvQWYqLaWahEhF6Jf2IlqtrwcjVJMAAABAKk8gTypvuCW15YY5EjI2TOl5+xWmDACmIUISADANvbPnlIRVMmKkikTU7rMhcmlzrZQU5GXx6AAAAJCLzM5WkT1b7HHdwhOiaaZTRcLvl5nOXU0ioJ+UgthH9phqEgAAAMik9LKrxF9alhqUGDG0a7tETp9g4gBgmiEkAQDTTO9wRN7b7/SNjriqSAR8XrlmcX32Dg4AAAC5a+2r9sKxsLRfiqp67ae0QN6MriIxVjWJ8tCLYpqJChKRmC59oUQ7OwAAACDJ4/NJ5U23O2tKw3CqSYhI95svM1kAMM0QkgCAaeb1nSclHk8swg1VRUJ3qkhcubBe8gMz/w5AAAAAnB/z9DGRw7uTI6ld5LrbzeMRj2/2rCE9rpCEz+iU4uhae9zeHxYjmUAGAAAARpSsvFTyqmvs+dBGWtgpocMHZPjwfuYKAKYRQhIAMI2094dk4+G2lCoSSfl5frliQW12DgwAAAA5y1QX/dc6vZJLqnokv3Qwpc/ybKIiEu5qEmWh10TMsLUdi+vSOxTJ4tEBAAAgF6n1Y9Utn3DGZmo1iZ43XkqsuwEA0wIhCQCYRl7edtxee6sqEjFXFYnVixvE7/Nm7+AAAACQm47sFTl1ZGRgSs2Ck85zHq9oXp/MNu5qEl5zQErDb9vj9oGw6AYnuAEAAJCqcOFiyW+aZ481PdG2TYmcOSVDu7czZQAwTRCSAIBp4kTXgOw40WWPw84aXEoKgrJq3pzsHBgAAABylnU32wev2uPy2g7JKwrZY08gILORNiooURp5WzQjUV1D1w3pGkxUlgAAAADsNaSmSfWtn3TGaq1tOKV+e956WUxXcAIAkLsISQDANDm5/eLHx+2xbpoSd93ddv2yRvF6+CsdAAAAo+zZKtLRktjWDJkz/7T9lKogMRurSCS5W25oZlhKw2/YYxWSiOtO+WQAAABAyW9okqJFS511pBWSSJynjXV1ysDWTUwUAEwDXFEDgGlgf2uvHGrts8dhV5uNqtJCWVJfmaUjAwAAQK4y43GRda/Z46q6dvHnR+yxNkurSIxVTaIk+p54jMSa2zBM6RygmgQAAADSVd36CZW4dVWTcMK1Pe++LkY0yrQBQI4jJAEA06CKxEuuKhJx0xT3TW03LGtKuQsOAAAAsOxYL9LfY21qHkOq3FUkfD7RPN5ZP1FqHZ1cSWtmTErDTmuS7qGwRONO+WQAAABAyauaIyUrL7Mnw6M71ST0gX7p3/A+EwUAOY6QBADkuG3HO+V095C1bYopkbhTRaKhskTmVZdm8egAAACQi8xoWGTD2/a4qr5NfHkxe6wF8rJ0ZLlFBSTcgeOSyIfiMbqtbXVTYEc/1SQAAACQrurm212t60zRXNUkete+Lfpw4nwuACA3EZIAgBymyvy+suOEPY4bIq5OG3LjcqpIAAAAIIOtH4gMD1qbmkeXqnln7Kc0v180jdMBSarlhhOTiEtZyKkm0TsckUiMahIAAABI5S8plbIrr3HW2HrcriZhhMPS98G7TBkA5DDOigBADttxsks6R+5es6pIuBISC+sqpLa8OItHBwAAgFxkxqIiW9ba4+qGVvHaVSQ00fxUkRhN8zgxieLoRvHqnfa4c4BqEgAAAEhXef3N4slz1taa4YRr+zevEz0UYtoAIEcRkgCAHGWapry1+1RKFQkjmZHQNLl+aVPWjg0AAAA5bOdGkdCQXUWicl7rqCoSTiAACR4VHrHnJS6lYVc1iVBEomoxDgAAALh48wukYvUNzlpb11OqSQxsXsd8AUCOIiQBADlqX0uvnO4eylhFYmlDpVQU52fx6AAAAJCLTFXmd/N79ri6sVW8AVcViUAga8eW61zFJKQ4usmpJmGKdA1STQIAAADpyq++Xrz5+RmrSfR9+J4YseRaHACQSwhJAECOenusKhIicvWi+uwcFAAAAHLb3o9FBnqdKhJzR1WREKpIjEXNjTM/upSGX7ef6xmKSFynmgQAAABSeQIBKbtydcZqEvrQoAxs3ciUAUAOIiQBADnoaEe/HG7rt8dRV0Jifm25VBYXZOnIAAAAkKtMwxDZ+I49rqpvc1WRUCEJqkici+Y6S1Ic3Sheoysxt6YpXUORiX7JAAAAMAOUX3WteFxrbU2ty0f0ffB2otobACCnEJIAgBz01i5XFQnTFPdNa9dQRQIAAACZHNol0tNhbWoeQyrntaRWkdCoInEunpRaG3EpCb9pj7oHw6K7y7sBAAAAIuLNL5DSy64c1XIjsW6M9/bK4I6PmScAyDGEJAAgx5zpGZI9p3vscUR3TsQ2VpVIbXlxlo4MAAAAuUpVOnBXkaisaxdfHlUkLoQ7TFIc+VA8Rp+1bRim1XYDAAAAGK38mutF83qTi3O1eLSf6137dmK9DgDIGYQkACDHvLXbqSKhj6oicfWihuwcFAAAAHLb8YMibSPrSE1VkThjP6X5VBUJPv6fT0giGZPQrGoSb9nPdQ2GrbAEAAAA4OYvKZWSlZdlrCYR62iT4X27mDAAyCGcJQGAHNI5EJKPj3fa44haS4+YU1YoTVUl2TkwAAAA5LZNThWJippO8Qej9lhz9UfGuWmjqkmURD8QzRi0tuO6Ib3DVJMAAABAuoprb1QLycSacnQ1ifffopoEAOQQQhIAkEPe2XM6GTAWwzQl7rpLTVWRoI80AAAARjPPHBc5cSg5kip3FQmvTzQPH/3Pl3vdrZkRKYm8a487B8Oc4AYAAECaQEWlFC9dMaqaRELk1AkJH02u2QEA2caZEgDIEX3DUdl0pN0eR1xtNsqL8mVhbXl2DgwAAAC5baNTRaK0ulsChWF7rAWoInEhVETC464mEXlPxEzMayxuSF/IqdQBAAAAJFVcd5OzplTVJMzUahIAgNxASAIAcsR7+86IricqRxhiSmxkW7lqUT1VJAAAAJDG7GwVObw7OZJqVxUJ8XpF83iZtQmoJuExh6U4ss4edw5QTQIAAADpgjV1UrhgkbOm1J1qEqHDByRy+gTTBgA5gJAEAOSA4Uhc1h1sscdRZ+0sRfkBWdpQmZ0DAwAAQG7b5LSBKCrvl2DJkD32+KkiMZHVJEojb4tpxq3tSEyXwUjsor4/AAAAZqaKa93VJAyqSQBADiIkAQA5YO2BFonGDLuKRNRVReLKhfXipY80AAAARjH7ukX2fmyPq+e6qkh4PKJ5fczZBFaT8Bq9UhjdbI87+qkmAQAAgHT5TfMkv6HJWVMazh1xQ3t2SrS9lWkDgCwjJAEAWabuQnt/35mMVSSCAb+snFudnQMDAABAbtu8xr4rLVg4JIWVffZTnkBeFg9s5tBGBSXKIm+JqN7SqlxyNC7D0URlCQAAAMBeQ2qaVFx3szM2RlWT+OAdJgsAsoyQBABk2YbDbVa7DcUUU2KGU0Xi8vm14vfSRxoAAACpzKEBkV1OVYM5c53WbVSRmFjulht+vUWC8V32uHMgzFsTAAAAaQoXLpa86pqM1SQGt2+ReG8PswYAWURIAgCyKK4bsmbv6ZQqEiM3ponP55VLm52FNAAAAGDb8r5IPGZt+gNRKa7tsp/y+ANM1CRWkygJv21vD4ZjVkUJAAAAIL2axE2jqkmMnPg1DOld9y4TBgBZREgCALJo67EO6R2K2lUkoq4qEpfOmyPBAH2kAQAAkMqMhEW2rbfH1U0tomkj60hNE83nZ8omsZpEfvyA+OMn7DHVJAAAAJBJ8bIV4i8rz1hNYmDLetGHBpk4AMgSQhIAkEUfHGi1t2OuMLHX45HLF9Rl78AAAACQu/ZuFYkm2jx4vLqUNbTbT2lUkZiaahIRp5pEfzgqMd3pMQ0AAABYa0iPVyquvXFUSCJxAtiMxWXg401MFABkCSEJAMiS450DcrLLSQtHdaeKxPKmKikKUiYZAAAAqUyVqv14nT2urGsTj093qkj4qSIxWTxORkKKoh+Lx+gdeVFEeoYivFUBAACQpmTVZeIrLMpYTaJ/07rE+h4AMOUISQBAlqxzVZGIm6a4Om3IFVSRAAAAQCanjoh0tY0MTKloSm6L1WZDs2oeYDKouXWqScSlOPye/ZwKSRic4AYAAMAoHp9fyq5a7awpDVWBLHEiON7TLaGDe5kzAMgCQhIAkAVDkZh8fLzDHkedALE0VZdKeVE+rwsAAADSbfvQ3iyt6hF/vlPBgCoSk88dQSmJfiBiRq3tuG7IQDg2BUcAAACA6ab00itF83oTAxWsdd0tp6pJAACmHiEJAMiCTYfbJT7SXsMQU+KuhfGlzTW8JgAAAEhjDvaJHNxljysbncpkmtcnmsZH/MnmVJIQ8ZhDUhjdbI97Bmm5AQAAgHSq3UbRkuUZW24MH9gjsZ4upg0AphhnUABgiqk+c+sOOie0Y64qEkX5AZk/p5zXBAAAAOl2bhIZOaGaVzgsBRX99lNUkZgaKiLhcQUlSiLvplSLi7gX9wAAAMCIsitdLTdMQ0Q9FFNkYPN65gkAphghCQCYYvtaeqVrIGxtm2JK1FVFYtXcGvF46CMNAACAVKYKR2zfYI+rG5zQrXg8ViUJTH01iYB+RvJiB+xx9xDVJAAAAJAuv6FJ8qqdCsKaMRKSEJGBLRvEiNG6DQCmEiEJAJhiH+xvsbfjKjQ8kpFQ4YiVc6t5PQAAAJDu8B4R1W5DrRt9cSmp67Sf0nx+ZmwKaaOCEsWRNfZ273BEdFcIGgAAALDWkJomZVdc7awprQpxiXWjPjwkQ7u3M1EAMIUISQDAFOoeDMueMz322F1FYlFdhRQGA7weAAAASLftQ3uzsrZdPN7knWcarTaywJWRkMLYDvEYiTW+YZjSF4pm45AAAACQ40pWXiqegOv8r6uaRP+mddk5KACYpQhJAMAU+vBgazIgLIZpiu6sg+WSeU65NQAAACDJ7G4XOX4wOZLyxnb7Oc3nUzEJJmuKeVJm3ZDiyAf2qGcoLGayXBwAAACQXEMG8qRk1WWjqkkkRE4ek0jLKeYKAKYIIQkAmCJx3ZANh9vscdQVkKgsLpD6imJeCwAAAKTbvsHeLCrvk0BB2B5rflptZEtKy43oOhEzbm2Ho7qEos4JbwAAACCp7IprnPWkCtaaVJMAgGwgJAEAU2TbiU4ZCidOnJpiSlR37i67pLkm5SQrAAAAYK0boxGRXZvtyahqdEK34vGK5vEyUVniXr97jX7Jjzl9pLuHnCALAAAAkJRXNUcK5jZnrCYxuH2L6KEQkwUAU4CQBABMkXUHWu3tmKuKhM/nlWWNVbwOAAAASLdvm0gkcaLUF4hKYVWv/ZSHKhJZpY0KSpRE3re3+0NRq5IcAAAAcNZqEoZaMyZupjNjMRnctokJA4ApQEgCAKbAqe5BOdYxYI/dVSSWN1ZJwMcdgAAAAEhlqvK72z60x1UNbaJpI+tITRPN52PKsszjKgYXjB8Un54IRquXrmc4kr0DAwAAQM4qWrxMfIVFGatJ9G9al/gcAACYVIQkAGAKrDvoVJGIm6YYrnXupc01vAYAAABI13JCpP30yMCUsroO+ynN5x+pZYBs0kb+SSqKfGBv9wxFOMENAACA9DWk1yull12VsZpErLNDwkcOMmsAMMkISQDAJAtF47LlaEfGVhsNlSVSWVzAawAAAIB0rioSpVU94gtG7bFGq42c4eq4IcXRjWKacWs7FjdkMBLL3oEBAAAgZ5VedqWIZ+QSnaoc4aoeoapJAAAmFyEJAJhkm4+0WydIFUNMiblabVBFAgAAAJmYw0Mi+7fZ44q6dntb8/pE0/g4nys0V0rCYw5JYexje9w9SMsNAAAApPOXlErRoqUZW24M7dsp8b5epg0AJhFnVQBgEqn+cR8ccFptxJy1rhTk+WVBbTnzDwAAgHS7NonoicWjPxCVgqo++ymqSOQWFZHwuIISRRHnzr/BcEyicdeHAAAAAGBE2RXXpLbcSFaTMEzp/2g98wQAk4iQBABMoiMd/dLRH7K2TTElajhVJFbNmyPeZEk1AAAAwBW0lR0bU6pIaNrIOlLzWJUkkLvVJPLjB8WrO+32eoacNikAAABAUsG8+RKoqMxYTWJgywYxVXACADApuDoHAJNoy1Hn5KjqspEMA6uTqCvnzmHuAQAAkK7lhEhv58jAlLJ6Z01JFYncpKUEJUwpijp3/vUORxLBFwAAAMC9htQ0Kbty9aiQRGLdqA/0S/joIeYLACYJIQkAmCRx3ZCPjydPbovEXMHf5poyKc7PY+4BAACQbu9We7OorF/8+RF7rPn8zFiOchWTkOLoejFNw/5cMByNZ+/AAAAAkLNKVl4mms9VKc5ViXhwx5bsHBQAzAKEJABgkuw53SPhqG632oipUhIjljdWM+8AAABIY+pxkX3b7HFFnauKhNeX0tYBuUWz6kkkeI0+Ccb32OO+YVpuAAAAIJ03GJSiRUudNaXptNwY2r1djFiMaQOASUBIAgAmyZZjzgntuKuKRMDvk+Y5Zcw7AAAA0h07IBIasjY9Xl2K5nTbT6XcYYYcb7khUhxxWm70haJi0HIDAAAAGZSsuMRZUxrqRHLiZjsjEpHh/buZMwCYBIQkAGASDEfisvu0c0I76iqTtriuQnxe/voFAADA2VttlM3pFI83mbbVRAhJ5DyPq9BHQWynaEYi8GIYpgyGuQsQAAAA6QoXLBZvfr6zwwpKJAxu+4gpA4BJwFU6AJgEO052ij7SXsMQU3RXJYmljVXMOQAAANKY0bDIIedOsfK6zpQqEu52DshNmutV0iQuhTHnpHYvLTcAAACQaQ3p9UrxslXO2BWSGD64V/ShQeYNACYYIQkAmAQfHc3caqMoPyANFcXMOQAAANId3CUST1Qb8AfDkl82YD9Fq43pw91yoyiy0d4eDEdFd1WYAwAAADK23DCdlhuqqsTQ7m1MFABMMEISADDBeoYicrit3x7HRipKKEsbqlJOmgIAAAC2PVvszcpaJ3Qrmiaa18dETRPu9X6efkx8epu1bZoi/aFoFo8MAAAAuSrY0CT+0rKM1SQGtzufEwAAE4OQBABMsC3HnBPaVqsN181iKiQBAAAAjGYO9omcOJQcSWltl/2c5vMzYdOIiki4Y9GF0U32du9wJCvHBAAAgNwP2pasvNTZYej2ZvjEMYl1O634AAAXj5AEAEwg0zRli6vVRtRZy0pVSYH1AAAAANLs25YoNSAiBSWD4i8I20/RamOat9xQIYmR13Y4Epeoux8fAAAAMKJ4hROS0NT60Wq7kUA1CQCYWIQkAGACnekdltbeYXscd/UcpooEAAAAxrRnq71ZXuO6S8zjEc3jZeKmcUjCZ3RJQD9ij/tCVJMAAABAurzKKgnW1TtrSndIYscW6wY9AMDEICQBABPoo6Pt9nbcNMXOSGiaLGmoZK4BAACQxuxqE2k/PbJuNKWkptt+jlYb07jlhisoURjdbG/3DUc5wQ0AAICMStzVJAwVkkicYI51dkj0zElmDQAmCCEJAJgghmHK1mPOXX8xVxXdxspiKc7PY64BAABw1ioSxeW94g3E7DGtNqYvJyKhWm5sETETvfgiMV3CcVdfPgAAAGBE8fJVVjU5i9Vyw6keQcsNAJg4hCQAYIIcbu+T/uGotW2KmdJqY1lDFfMMAACANFbJ3L1OSKKspst50usVTeNj+3TlriThMYckL74vpZoEAAAAMJqvsEgK5y1w1pSGE64d3LFVTJ2wLQBMBM62AMAE+ehoh70dN5yQr9fjkYV1FcwzAAAA0p0+KtLfY21qHkOKqxPb1tjnZ8ZmUMuNouhH9jYtNwAAADCW4hWXZGy5oQ8NSujwASYOACYAIQkAmACxuCHbT3RlbLUxv7Zc8vw+5hkAAABnbbVRUtErHp9zZxitNqY/V0ZCCmI7RMxEK5W4bshQNJ69AwMAAEDOKl6yXDS/KzDtqlg8uGNLdg4KAGYYQhIAMAF2n+62egtnbrVRyRwDAAAgjRmPixzYYY/LajpTAhKaVYsA05n7NfSYIQnGdttjWm4AAAAgE08gIMWLlztrStMJUg/t2SFGNMLEAcBFIiQBABPcasNdRSIY8Mm8OWXMMQAAANId3ScSHrY2NY8uhVW99lO02pipLTecO//6Q1ExXOFqAAAAIKlkZeaWG2YsJkN7dzJRAHCRCEkAwEUKReOyr8XpHR1znehcXF8pXg9/1QIAACCD/dvszbKqHvF4R9K2miaa18uUzRAed8uN+C4RM2ptq4DEQCTRfgMAAABwK2heIN6CQmeHFZRIGNr5MZMFABeJK3cAcJH2t/SKrieCEYaYorsqSSxtqGJ+AQAAkMbU4yJH99vjkupue1vz+kZqEGCmtNxIvpqaGZF8V8uNwRAhCQAAAGRYQ3q8UrJ8lTN2hSRCRw6IEU0EbwEAF4aQBABcpF2nnBPacVdAojAYkLryIuYXAAAA6U4dFYmErE3NY4xqtaFCEphJ3C03CmLOnX8DkaiYJi03AAAAkK542UpnPWm6W27EJXT4AFMGABeBkAQAXIS4bsju05lDEgtqy1NOhgIAAAC2w041gZKK0a02CEnMNO6PBQWxXWKacWtbVaQLRfXsHRgAAAByVrC+ccyWG8P7dmXnoABghiAkAQAX4VjngIRHTmqaYkrccO4Cm19TztwCAAAgjVU54JATkiid42614WXGZiCn4YaIxwxLfnyvPe4PUyoZAAAAGdaQHo8ULVw8qppEwvD+3VQkA4CLQEgCACaq1YarSq7P55XGyhLmFgAAAOk6W0X6exLbmiFF7lYbVJGYHS03otvs7YFwLEtHBAAAgFxXuGiZva1ZlSQSJ6H1oUGJnDqexSMDgOmNkAQAXMQdgLtPZW61Ma+6VHxe/ooFAADA2VttFJf3i8fnarfgo9XGbGi5URjbITJyJ2A0pkskRssNAAAApCtsXpAapFZV6UbQcgMALhxX8ADgArX1h6RzIJyx1cYCWm0AAABgLIf22JulVU7oVrzelLYMmMktN4YkL37IHlNNAgAAAJl4AgEpmDd/VDWJhOF9TvgaAHB+CEkAwAS02lD5CDvEq2nSXFPGvAIAACCNOdQv0noiOZKi6pG2G7TamPG00S03Ytvt7YFwNEtHBQAAgFxXtHipva2NVCNTou2tEuvqzNJRAcD0RkgCAC6Qu9VGzNVqo768SPIDfuYVAAAA6Q7vtTcLSgbFlxezxxqtNmY8d52QAtVyY8RwNC5x112BAAAAQFLhwiXOZKg79VxBieH9u5goALgAhCQA4AIMhKJyrHPAHrtbbcyn1QYAAADGctgpiVta5VSREI9HNI2P6DOdu5KEz+gSn346MTBFBsNOYAYAAABI8heXSLCu3llTpoQkaLkBABeCMzAAcAF2n+6xTmQqhphWu42kBbXlzCkAAADSmLGoyPGD9rjIFZLQvD5mbLa03HDVk3BXkxgIEZIAAABAZkULnZYb7pPRoWOHRQ8NM20AcJ4ISQDABdjlarURd1XFLSvKl/KifOYUAAAA6VRAIp64EO4PRiSvKGQ/RauN2cNVTEIKYk555MFITAxVPhkAAAAYpXDx0lGVJJJ38JkSOui09AMAjA8hCQA4T7G4Iftbe51xSquNMuYTAAAAmR1yt9roTrlqrnm8zNosbLmRFz8mHqPf2jYMU4Yj8SweGQAAAHJVXnWN+EtKnR2Gc+fe0D4neAsAGB9CEgBwng609Up8pHyEKaborkoSC2potQEAAIB0pqoQcGSPPS6udEK3tNqYjS03kkwJxpzwTH84mqWjAgAAQK4HbQsXLRlVTSIhdHCfmDphWwA4H4QkAOA87R6j1UYw4JO68mLmEwAAAOlaTogMD1qbmkeXgvJE9QBr7PMxY7O4moS75cZAKJYI1AAAAACjFC1a5qwnrUoSiXWjEQ5L+NgR5gsAzgMhCQA4D+qE5e7TPfY47jp/2TynXDweV4NhAAAAIOmwUy2guLxfNI9rIeml1cZs48pISH58n5imbm3HdUPCscQ2AAAA4JbfNE88gYCzw9UGmpYbAHB+CEkAwHk42T0o/cNRu9VG3LUQXVBLqw0AAACM4bDTaqOksiel1Ybmar6A2cJ5zT1mSPJ0586/gXAsS8cEAACAXObx+aRwwWJ7rI0EbZXhfbuoSAYA54GQBACch12uVhu6qSpLjPxl6tFkbnUpcwkAAIA0Zm+XSGdrciSFlX3Ok7TamLURCXfLjfyoU2mEkAQAAADGUrhwibOmtG7gS5ygjvf2SLSthYkDgHEiJAEA52H3KVerDdX2bURTVakEfJRJBgAAQAZHnCoSwfyw+PMjKZUkMDu564cUxJ2QRDgal5ju+rABAAAAjChauFjdsTcyct3Fp6pJ7HfWlACAsyMkAQDjNBiOyZmeIXvsbrUxv4ZWGwAAABjD8UP2ZrGr1YZ4vCnVBDC7uF/7gH5aPIZTYWQoQssNAAAApPPmF0h+Q5OzpjSdcG34yEGmDADGiZAEAIzT4XbnpKUhprgyEjJvDq02AAAAkM5Ud3adPmqPiyqcNaXmpRLZbDa65UYwvs/eHorEs3RUAAAAyHWFCxY7A9dJ6vDJo2LGWUcCwHgQkgCAcTrc3m9vu6vfFhfkSWlBkHkEAABAuo4zIuHhxLZmSH75gP0UrTbgriOSH9trbw8TkgAAAMAYCuY2j6okkQhKmLG4RE6fYN4AYBwISQDAOB1sde76i7uqSDRWljCHAAAAyOzEYXuzqHRAPF4nbat5+Ug+27m7reS7KklE47rE4q5kNgAAADAiWFsnHn/AmQ9VvW5EiJYbADAunJEBgHEYDMektXfYVUnCWXg2EJIAAADAWE46IYniil5nv9Vqw11HALOT8x7wGv3i00/b46FoLEvHBAAAgFymebyS3zR3VDWJhPCxQ1k6KgCYXghJAMA4HGpzqkgYYrpbvVFJAgAAABmZhiFy6og9Lix32rdpVkgCs52KSGiuchL5MaeaxBAtNwAAADCGgrnznYHrZHX45DEx43HmDQDOgZAEAIzDoXYnJKG7qt6WFORZDwAAACBNxxmRSCjx4dsXl7wSpzKZ5vUxYUi8F1zzEIzvt7eHCUkAAABgDAVzm0dVkkgEJcxY3ApKAADOjpAEAIzD4Tbnrr+4q4oErTYAAAAwphNOqduisn7RNGchqXn4OI6R94IrJZEfPyRi6tZ2NK5LLO5KaAMAAAAj8mpqxZPnunnPdFWTOOa0/AMAZMZZGQA4h8FwTFp7nbv+dFf5sqaqEuYPAAAAmRj/qv4AAQAASURBVJ10Tk4WlfeNarXhrh+A2c15L2hmWAL6cXs8FI1l6ZgAAACQyzSPV/Ib542qJpEQOuqEtQEAmRGSAIBzONTmnNA2xHS3eKOSBAAAADIyDV3k1FF7XFg+4DxJqw2MikhornISwfgBe3uIlhsAAAAYR8sN90nryMljYsQI2wLA2RCSAIDzCEnormq3JQVBKc53lTQDAAAAktrPiETD1qbXF5e8ouFRlSQAh7uuSDDmDklwchsAAADnDkkkKkkkghJmPC6RU051MgBAOkISAHAeIYm4q4pEY1UxcwcAAIBzttooLOt3PaFZpXEBN1chCQnqR9WZbWs7FjckGteZLAAAAKTJm1Mrnrygs8N0Tl6HabkBAGdFSAIAzmIgFJW2vpA9jrvKljVWljB3AAAAyOyEE5IoKndCt5qXj+HIxElJaGZEAvoJe0zLDQAAAGRcQXo8UtA017WOdMoghwhJAMBZcXYGAMZZRcIQ0x3GlQZCEgAAAMjANHSR00fscWHZgPOk18ecIWNEQnOVk8iLH7K3hyOJqhIAAADAaPmulhviusEvcvKYGDFatwHAWAhJAMBZHGp3SiPHnSCulBYGpTg/j7kDAABAurbTItFI4kO3Ly55xcP2U5qXVhvIzNVxQ4KukMRQlJPbAAAAyKxg7vxRlSQSQQlT162gBAAgM0ISAHAWB1udShK6q4oErTYAAAAwppNOq43CUlcVCdFE8xCSQGauQhISjB9WJUms7VjckGhcZ9oAAACQJq+6Rjx5QWeHqxQyLTcAYGyEJABgDH3DUenoD9njuKtcWWNVCfMGAACAc4YkikqdymRUkcDZOSkJjxkSv3HGHg/RcgMAAACZVpAejxQ0zXPGhlMOOXzMqU4GAEhFSAIAxnC43akiYYjpDuFKQyUhCQAAAKQz9bjIqSP2uKBs0HmSVhs4R0RCc5WTyFPVJEYMRWi5AQAAgMzy5za7PpA4IYnIyeNiRKNMGwBkQEgCAMZwqM0JScSdtaWUFeVLUTDAvAEAACBd22mR2MiJSM2QYMmQ/RSVJHAuro4bkhc/klJJwnSntgEAAIARBa6QhGatGRPrRlPXJXLyGPMEABkQkgCAMRztcPpH667zkY2VxcwZAAAAMjt91N4sKB4SzZNM22qiebzMGs7KVUhCgq5KEnHdkLj7QwkAAAAwIm9OrXiD+c58uMK14RPO5xMAgIOQBABkEInp0to3bI91w1lY1lfQagMAAABjaDtlbxaVOqFbqkhgfJyUhM/oFo/hVLcLxeJMIgAAANJXkJom+Y1znbG75caZk8wYAGRASAIAMjjTO5SsSiammOLKSEhNWSFzBgAAgHOGJPJLBl2fvvn4jfFFJDRXUMLdciMU1ZlCAAAAZBSsq89YSSJympAEAGTCWRoAyOBkl3NC213V1u/zSllhkDkDAABAGjMcEunptMf5pc6akkoSGDdXy41A3OkhHaaSBAAAAMYQrHVCEprrjj99oF/iA/3MGwCMQkgCADI42Z05JDGntNAqXwYAAACkaT9tb/oCUfEFo65P33z8xvi4P20EdaeHdCimi+m6KxAAAABIynOFJBIlkp11Y5SWGwCQhrM0AJDBqe4he9twWrjJHFptAAAAYBytNgrdrTY0TTSNj98YH3coO08/ITLSU1rXDYm7E9wAAADACF9BofiKS5z5cFWTiJxxPqcAABI4SwMAo0RiurT2Ddtj3XW3lqokAQAAAJwrJFFQOmBvax4vE4YLoplR8Rtn7HGIlhsAAAAYT8sNce78i1BJAgDSEJIAgFHO9A7Z1chMMd2hW0ISAAAAGFdIIr/YqUxGqw2cD1VHQnM13QjEj9nboajOZAIAACCjYG2dM3Dd+Bc5fZIZA4BRCEkAwCgnu5zSyO5qtgGfV8oKg8wXAAAA0pjhkEhPZ3IkwRInJKF5qSSB8+RkJCQvftzeDlNJAgAAAOOpJOG6808f6Jf4QD/zBgAuhCQAYJQTY4QkqksLU/oDAwAAALb20/ZmID8iHp/rjn8PH71xftyfOvL0E/Z2KKaL6borEAAAALDXja6QRKJUsrNujNJyAwBScKYGAEY51e2EJAyndZvMKStkrgAAAHDOVhuFJc56UjRNNI2P3jg/7nB2wGgRMePWtq4bEncnuQEAAIARvoJC8RWXOPPhqiYROeN8XgEAEJIAgBSRmC5t/SF7rLvu0ppTSkgCAAAA5w5J5Bc7IQnNQ6sNXCQzLn7deX+FaLkBAACA8bTcEOcOwMjpk8wZALhwOwsAuJzpHbKrkJliusO2hCQAAAAwrpBEsHjI9ambj904f6qOhOZquhHQnZPaoairlQsAAADgEqxztdxw3QAYod0GAKTgbA0AuJzocu76c1exDfi8UlYYZK4AAACQxgyHRHo6kyMJFg/bz2leKkngAjkZCcmLOyGJMJUkAAAAMIZgTZ2znHTdAagP9Et8oJ95A4ARhCQAwOXkGCGJ6tLClL7AAAAAgK39tL3pD0bE43Pd6U8lCVwg96ePlEoSMV1M112BAAAAQFKeq91GomSys26MUk0CAGyEJADA5VS3E5IwnJZtMqeskHkCAADAOVttFLhbbWiaaBofu3Fh3CHtgHFGxEx8QNF1Q2LuRDcAAAAwwldQKL7iEmc+XNUkaLkBAA7O1gDAiEhMl7b+kD0fuuvurDmlhCQAAABw7pBEfpETktA8tNrAxNDMmPiMNntMyw0AAACMJVjX4KwjxbkTMHLa+dwCALMdIQkAGHG6Z8iuPmaK6Q7ZEpIAAADAuEISwaJh1yduPnLjwqk6Epqr6YZfd95noairpQsAAADgEqytcwauGwGpJAEADs7YAMCIk65WG+7qtQGfV8oKg8wTAAAA0pjhkEhPpz3OK3Iqk2leKkngIjkZCQnET9vbVJIAAADAWII1TkhCc90JqA/0S3ygn4kDAEISAOA42ZU5JFFdWpjSDxgAAACwtTsXrj1eXfz5Eec5KkngIrk/hQSMM/Z2KKaL6borEAAAAEjKq613TYZaMzrrxuiZk0wUABCSAABHa59TGjml1UZZIdMEAACAzDpb7M38QlerDU0TTaN4Iy6OO6wd0J2QhK4bors/tAAAAAAjfAWF4i8pdebDtW6MtjmfXwBgNuOMDQBYrdlMae93SiMbroVjVXEBcwQAAIDMXK028ouGXJ+2+biNieUzukUznSBOJK4zxQAAAMgoUF1jb2uuShKxrg5mDAAISQBAQl8oKrG4kbHdRnlRkGkCAABAZj3OSca8Qid0qxGSwATQRrXc8Omt9nbU9fkFAAAAcAtUVDoD01k3xjrbmSgAICQBAAkd7ioSrmStUlpISAIAAABj6M4ckhBabWCijNFyg0oSAAAAGF9IwtmMdTmV8ABgNqP+JwCIpLbacC0agwG/5Af8zBEAAADSmPGYyECvPQ5QSQKTwF1Jwq87PaSpJAEAAICxBMqdkIRmOie89aFB0UNOCzcAmK0ISQCAqiQxEM4YkqDVBgAAAMbU0ykycsLR44uLLy/m+rTNx21MvEBKSEJnigEAAJCR311Jwiol4Zz0puUGABCSAID0dhuukEQZrTYAAAAwlh6n1UZ+QWhUhwRCEpjwbhsSMFpTQhKm665AAAAAIMlXVCwef8CZENe6kZYbAEBIAgDO2m6jvCifGQIAAMA5QxJ5ha6StVSRwIRyUhJeo1fEDNvnuWO6wVwDAAAgfQWpaeKvqMgckuhsY8YAzHrc2gJg1ovrhnQNudttOAtGKkkAAABgTN2ukISrkgRVJDCRXIUkLH693d6OxAlJAAAAILNAudNyQ3O32+hyPscAwGxFSALArNc9FBFz5NyiKSbtNgAAADA+PZ32Zl6BE7qlkgQmMyjhN9pSWm4AAAAAmQQqqsaoJEFIAgAISQCY9dytNlI6+moalSQAAAAwrnYbAUISmEyaE5Pw605IIkJIAgAAAGMIVFRmDkl0dYjpGgPAbERIAsCs1+EKSeiutWFxfkB8Xv6aBAAAQDozNCSiHopmij8/Yj9Huw1MaiUJV0giGqPdBgAAAM4dktBcoQgzFhO9v49pAzCrcfUPwKzXPuCEJAxXSKK8MDjr5wYAAADnbrURCEZE87gWkh4+amPy+AynggmVJAAAADAWf7mrkoTF3XKjnYkDMKtx5gbArOeuJOEOSZQV5c/6uQEAAMC5W23k5YdS7vnXXK0RgIngfkv5XSGJuG6I4f4QAwAAAIzwBoPiLSgco+UGIQkAsxshCQCzXsdA2J4Dw7VQpJIEAAAAxtTtCkkUOOtJ8RCQwGRw3lceMySaMWCPo7rOlAMAACCjQGXVGCEJpzIeAMxGhCQAzGrhaFz6h6NjVJKg3QYAAADGUUki6IQkNI+XKcOEGx29cVeTiMQMZhwAAAAZBcornDWlKyQR7WhjxgDMaoQkAMxq7ioSpvrHFZIoL6TdBgAAAM4dkgjkR5z9VJLAJHG3cfEZzp1/VJIAAADAWAIVla6Rc/I7TiUJALMcIQkAs1rHQChjFQmvxyNF+YHsHBQAAABymqmStT3ORWp/gROS0DQ+ZmPy+fQuezsSo90GAAAAMvOXj9Fuo6dTTD3OtAGYtTh7A2BWa+/PHJIoKwyKx3WnFgAAAGAb7BOJx0YGpviC7koSfMzG5HB/OkmpJBGn3QYAAADOXUki0W5j5CS4KRLrdoK3ADDbcPYGwKw2ZkiiKJidAwIAAEDu62q3N33+uHi8rovUhCQwBSkJn+GqJBHXE9VNAAAAgFH8ZeWq3F3mahKdTgtBAJhtCEkAmNW6B8P2tvv+K1VJAgAAAMiov9veDOQ760l18lH9A0wG93vL7wpJGIaZEvgGAAAAkjw+n/hLyzJOSNzVQhAAZhtCEgBmtf5wzFVJwjmzWJyfl6UjAgAAQM4bGrA389ytNjQ+YmNq+IxeMU0n5h3XabkBAACAzPwlpc7AdQ48Puh8rgGA2YYzOABmLVWStj8UdY2d5wqD/uwcFAAAAHLfYL+96XeFJDR3GVtggqW+u3Txmn32KGYQkgAAAEBm3sIi15rSOQmu9zvrSQCYbQhJAJi1hqNx0XVnUeg+rViYF8jKMQEAAGCahSTynNCteAhJYHK532E+V8sNKkkAAABgLL7iEmfgulFQH3A+1wDAbENIAsCslVJFQq0OqSQBAACA8RjKXEmCdhuYdK5qJV6jx96Ou8LfAAAAgJvPVUnCfRI8TkgCwCxGSALArNWXEpJIVZBHuw0AAACcu5KEL+isKWm3gankM3rtbSpJAAAAYMx1Y1GxZOo5rQ/QbgPA7EVIAsCs1T/snNA2XCmJ/Dy/eD389QgAAIB0pjqpODRgj/15sYx3+QOT326j296OGe7mgQAAAIBr3VjkVJLQXOfBjXBYjJjr8wwAzCJcBQQwa/WHYpkCtFJIFQkAAACMZXhQxExckNY0Q7wBQhLIDq/h3PlHJQkAAACMq5KEVVOZahIAQEgCwKzVH87cbqMwGMjK8QAAAGAaGHK12nBXkVA0PmIjW+02RjcQBAAAADKFJFLpg06VPACYTTiDA2DW6huj3UZh0J+dAwIAAEDuG3RCEv68SMpTGu02MMncbzGf6YQkaLcBAACAsXgCeeLxu24MdJVVjvc71ckAYDYhJAFg1uoPZa4kUZBHJQkAAACMYci508rvriRBQAJTwklJeI0Bu/WLaZiiu5PfAAAAwDiqSegDTggcAGYTQhIAZi13SMJwpWeLaLcBAACA8VSSCDjrSVptYOrpopmD9iiuJwITAAAAwGjeoiJn4DoXTkgCwGxFSALArGSaZmolCddNVwV5tNsAAADAGIbc7Tac9SStNjAVXN02LD7TeT/GDUISAAAAOHclCc1VVzk+QLsNALMTIQkAs1IoqktcdxaD7sK0RUFCEgAAADh3JQmf39VuwzP68jUwOdzvNK/hCklQSQIAAABj8BW6K0k4m1SSADBbEZIAMCulVJFQ/7gWhoW02wAAAMB4QhKBuLNfIySBKeJ6r3lclSRirhA4AAAAMFYlCXdKgpAEgNmKkASAWakvJSSRinYbAAAAGE+7DW/AWVOKxsdrTD2vMWBv024DAAAA4wpJuO4YjLtC4AAwm3AWB4DM9koShislEQz4xevhr0YAAACkM9XJxKGBjJUkNCpJICvtNpz3Y4x2GwAAABhHSMK9njSGh8WIudoIAsAswZVAALNSSiWJlFYb/uwcEAAAAHJfaEjE0EcGpnhpt4Es85iD9nackAQAAPj/2PvvILnONL/zfY5JW94beG8JgCRA702TbMN2bG/Y3sxo1JIidvXHTMSuNu5u7B2trq5WWs3VSDOa0bT3zabpJrvZbDZN07NJeO8KhUJ5m/6cG+dk1jlvVmUCBaCqMrPy+2EgcN5MAPXirWLhzTd/53kEKMyYVUlCabkx4QdvAaBaEJIAUJXGi7TbqAkFSzIfAAAAVAClioRuWKLplv8c7TZQAkZeSGJmI0EAAAAgy6ypLboUmXFabgCoPoQkAEi1V5JQ221QSQIAAABFTYx6l2YgvyQt7TawWNTOLoalhCQsJbQDAAAAKPRQSLSAUkVZKa+cGfdf5wBAtSAkAaAqTcRTRSpJ0G4DAAAAl64kkReSUN+1Bhac//Wm25PetWXZ7g8AAABg1g5S08RUW24oMsrrHACoFoQkAFSleCpTKDQr4aBZmgkBAACg/CUT3qUZSPuPE5JAGbTbcFjqixsAAABA3TuGw/5A2Tda8TjrBKDqEJIAUJUSakhCqSURMIwSzQgAAABlL+WHJAwzXfDOfmAx6XZcbNtvs0FIAgAAAEX3joFgwcctJQwOANWCkASAqhRP+yEJVcAkJAEAAIAilMNDQ6kkQSEJlI4tuj3ljTK02wAAAEARejBYMOZtJQhJAKg+hCQAVKW8ShJKRdqgybdFAAAAXF5IgkoSWEwz65bodsy7ppIEAAAAitGDIWXkH4rbVJIAUIV4NxBA1bEsW1JpvySt2rWXShIAAACYU0jCUCqTUUoCJaRWkqCQBAAAAIrRlEoS6qE47TYAVCNCEgCqTrJIqw1HkHYbAAAAmFMlCTUkwZJhcWnFKkmQkgAAAMCcKkn4aLcBoBoRkgBQdeIzQhJquw0qSQAAAKColB+S0NVKEqQkUMKYBO02AAAAMBe6WklCbbeRiLOAAKoOIQkAVSeZ12pDbbYhEjSMEswIAAAAFSGZ9C51k3YbKI9SEoQkAAAAcPkhCZ+lvM4BgGpBSAJA1Ymn0t51fkTCqSTBt0UAAABcupKEoYYkgBLSbP/rknYbAAAAKEYPKO02lINxS2krCADVgncDAVSdRKr4gTbtNgAAAFB8I+mXodWUdhuaptzWDywyXfyvy4zaSxAAAACYQyUJO0FIAkD1ISQBoKpDEuoZomnoonPADQAAgGKUO6zy222wZCgd3fJDEhYhCQAAABTbNwaVShJKKQkr6e8nAaBaEJIAUHUSacu7Vu+zoooEAAAALirl9+rVDX9PSUoCi03N5WhCuw0AAABcXiUJdT9pp9JiZ2gnCKC6EJIAUHUS6cIbvqBpLPpcAAAAUBnsTFrE+VEoJEE1MpSQZvvhHYtuGwAAAJhLJYkZFcispL+nBIBqQEgCQNUp1m6DShIAAACYS6sNpx6ZpquVJIDS0W0qSQAAAODyKknMZOe93gGApY+QBICqE1dDEsrjQYNviQAAAChCOTTMb7VBJQmUliYp79qacUcgAAAAUDwk4e8drUSchQJQVXhHEEDVKdZug0oSAAAAKCrll5+dXUVC7egLLALlS07Pa7dBSAIAAABzaLcxg0UlCQBVhpAEgKoOSdBuAwAAAJddSWJmSIKMBMqkkkTGIiQBAACAIvvGQEBEU168KFtHO0G7DQDVxSz1BABgsSWKtdswyY2h8nz/yWflncMn5OadW+UDd99c6ukAAFCVIQmtTFMSJ46NyT/814Pu9b/4n3dKU3PxO8dQWeuofsVpNu02AABYaM/+t/+fnHj9Vdl6171y8yc/w4KjImmaJnogIFYyqZyOZ3eWVJIAUG0ISQCo6pCEKmAYiz4XVK6/+f5j8vzr78gdu3fINz/58GX93lffPSj//h9/7P9Z/8u/kMa62iuaR2drs/zi2ZckFk/Ma0jiB0/9Tn7+2xeLvqCKhkPS3d4iu7dtkgdu2yOhYGDe1wkAgLKSKhKSUO/EWmKe+uUp+eOLfd74f/rLa6W2rvC/+eXmfO+U/M1/2Fv0eV0XCYUMaW4Ny7oNDXLjzR0V83ebSZN0XrsN27bd/RoAAEvBY//v/5e88+unZMcDD8nD//qviv663/3df5EXv/M/Cj6n6bqEamqkZcVK2XTrHbLnw49IIBy+rHk0L1shL333nyQxNbXgIYmDzz8nP/5f/9Ib/4sf/1Jqm5vnZZ0Ap+WGH5LwWVSSAFBluG0aQHW321AeD5qEJLDw0umMfPfx3+Y9drr3whX/eddt3eAegp8fGJKevgGZLyd7zhd9zjl4n4zF5cipHvnek8/KX/6Hv5OJqdi8fWwAAMq9koQ2s93GEtR/ISavvpy/RznfOymVorfn4nO1LJFYLCM9Zybl+WfPyX/6/7wjPWcnpHL4IQjN9kMSzgsctaUgAADV4vyRw0Wfsy1L4uPj0rN/nzz7X/9G/u6bX5bY+Nhl/fkbbr7FDVsMnT0jA6dOykLJpFLy27/9z3mPXTh+dME+HqqPHgwWfNxOxhd9LgBQSlSSAFB18vr0KpemQW4MC+/JP7wifYPDeY+dPtcnOzauvaI/r762RjauXi6HTpyR1/YekmUdrfMyz5M92btGr9m4Rv7Vox/LC0iMTU7J8TPn5AdPPef+XZxwhlPN4jPvv3dePjYAAGXJeVc9R9fVd6G1JVtFwrJsCQZ1SSazf/fec1OyfmOjVAJnrtM+9Mga6V5ek/d8Ip6RnrOT8uLzvTI+lnIDEz//0Qn58395jVRyJQkHGQkAQDXqO3rE/XnN9XvkY//b/5F3jjE1MiLnDh2Q5/7ub2X4XI8bcnCqQtz79T+f859f09gky7dtlzPvviOHXnxeWletXpC/xys/+aE7R1XfsaOydvcNC/LxUH00Q3lb0O+2IXZm6QfBAUDFO4IAqo7z4si7LulMUG1Gxye9Fha7Nq+XQK56yalzfhnrK7F720b35zf2Fb9r4nKMjE3IyHj2TsoVne0SDgW9H5FwSDpamuTmXdvkXz76iPd7/nTo2Lx8bAAAypbtHxpqakhiCWYkDuwblmNHsndXfuAjq/NaWFSKc0oliU1bGqWjM5r3Y+XqOrn5tk757Bc3eR1TLvTFZHjIrxhSMezC7QQBAKgWE0OD7g9H+9p1EoxEvR+haI00dS+TbXffJ4/8Gz88cey1Vy7742y85Xb358Mv/kEWwuTwsLz4nX90r9ffeLMYgaAXkgDmja5d8vUOAFQDQhIAqk6xYAR9e7HQfvir5yQWT4hh6PL5D75HutuzVR9OXUW7Dcf12za5Px870yPDY+NXPc8TSquN5Z1tRX/dqu4OqY1G3Ovxycp50wQAgCuS18Ng6R4gptOW/PqJ0+71hk0NsmNXq9TXB9zxeaU6w9VwKlS8/Ua/fPdvHpP/639/S/63v3xN/s9/84b8l/+0T559+qxMTaau+s/vywU6GhqCEq3Jzr+Qzq6oNDWHvPHkVX7scqgkAQBAtTl/+JB33bZ6TdFf17FuvUTqG9zrqdGRy/44m27NhiR6Dh6Q8cH5a3k67bm//1tJTE6Kbprynj//lrSuXOk+TrsNzCctL+Wt3EzI3YQAqgwhCQBVp9iGbwneBIgy4lSLeO61t93rB27dI11tzbIiF0A4d2FA0ukrvwPQ+bOWtbe6X9tv7s+Wl7waJ5WQxIqO4iEJh5679bKuJnrVHxcAgErZROZVklhiXvx9r1tNwTA0efD92YP59s7sv/ODA3FJJq+uaoETQvhvf7NffvajE3J03ykZH09JJmO77S7OnZ2U3z97Tv7Dv31HTh6/vD7hqoF+Z57ZIEtn96X3KLpyN11tXfFARbnSZoR21Mp5AABUg/O5VhuXCkk4tNy/+9GGy28h1rx8RbbNhm3LkZezlULni1Mt4u2nnnCv93zoo+7Haluzzh0PnD4lmVTlBTlRpqgkAQAuQhIAqk6xQ8PpMrvAQvinx55x72p0wgQfuf92r5WFI5OxpOfC1d2BcP32bMuN1/f6d0/MR0hiWUe22kUhzpzHchUktq1fmF6cAACUDUtpt6EtzTehR0eT8sLve93rG25ul9a2bMWojs7sz842+sL52FXtw7//P45Iz5lJd+99zZ5N8pkvbJRvfmu7fOkbW+Tu+5ZJOGxIPJ6R7/zD4StufdF7zm+10XWJkMTIcMINfzjaOyLS2OhXlagcS7eyCQAAc3H+qN9+1A0xFOGEDaZGshUkVu+67ooWd+Mtt7k/H3rh+Xn95Dzzn/9vsa2MG964/fNfdB9rX7PW/dlKp2Xg9Ml5/XioZtolX+8AQDUgJAGg6tBuA4vttb2HZN/R7IvZjz1wp9REwu71iq5sSGK60sTV2L0tG5JwPk48kbyqP+vkuWxIorWxXiLhwm8UpDMZ+cef/9q9DgUD8t7bb7iqjwkAQEXtIpdouPbpJ0+7FRiiNabcdd8y7/GOXCUJR2+ujcWVePdPg3L61IR77VSp+PCj75GNmxvdlherVte5H/PL39wqwaDuzuPpp7JtPy6XU5FiWmd3TdFfl0hk5Gc/PO4VCbnvweVSibQZIfClGeEBAKC480eyIYn69g4JRQv/259Jp+XX//Hfu9eBcERueOQTV7SkG3MtN06+9aYkY/PTiswJXJx86w33+s4vfEXCtXV5IYnpShPAvOBOQQBwmdmfAKB6WMUqSSz6TFANnDYa3338N+61017j3puu9Z5b2dU2byGJ9SuXSWNdrYyMT8ifDh6TG3duuaI/ZyoWl/6h7F0VnW0teYEL5+7PqXhcDp/skV/+7iU50XNeAqYp/+zTH5K25ssvUwkAQEWxlHYbS/Bt6FMnxmXvn4bc63ves1zCYbNgSOL8uSt/M+Ct17OVs9raI3LjLR0F6x841RxuvLVD/vC7Xjm0f0SmJlMSrbm8Fhi9yhxNU5O+8/7YeSkwNZmWM6fG5fVX+2VsNOm2Fnng/Stl05YmqUzc9QcAqF7xiQkZOZ+thNWyYkVecME5x3Ce79m/V1763nfk/JFDYgaD8qG//F+ksbPrij7esi3bpLa5RSaGBuXYq6/Iljvvvqr5O200fvNf/h/3um3NWrn2/Q97z7WtzbbbcBCSwHzRioQkaNkGoNoQkgBQdYq26CVFiwXwqxdelfMDw+71Zx++X3TdL+LU0tjgVpWYjMXldG/fVb/AuW7rBnn2lbfk9X2Hrjgk4bTamP5/ZO+RE/LFv/zrgr8uFAjInXt2ygfuuvmiLTkAAFg6lm4lCacl2JOPnfJaa1y/xw9yOlrbw6LrmvvrzvdOXvHHOJOrIrF5W2PRw1nH1u3Nbkgik7Hl7JlJt9rEXDmHu+eVdhvf/u9++e2ZnIoVTlhjz03tbnCjcs14gbP0MjwAAFy81UbuIOPEG6/LX7/v/oK/LhAOy84H3yc3f+LTF23JcSnOHmbDzbfKW088Jode/MNVhyRe/emPZLjnrHt9/zf/QnTD8J5raO9wq0rEJ8YJSWD+FNuH024DQJWh3QaAqlMsFbvEzrpRBsYmp+Rnv3nBvb5uywbZsdEvkzhteWf2TYjT5y5c9cfbvX2T+/NbB45KJnNldxSenGNFi0QqJUdP98iFXNUJAACWPLtYJYnK30W+8Wq/nM+10XjoA6vcQITKNHVpac22C7twPuYGHi7XyHBCUqns/qRTqUwhRapJTJ/dXuiLXdbHGRyISyIxt32Q09Lj4P7hq6qOUR5otwEAqF59uVYbl5KKx6XnwD4Z6T131R9zU67lxtFXXhIrk77iP2dqdERe+PY/utcbbrpV1u6e3cq0bc0a9+cLx2m3gXlSNKxM0hZAdaGSBICqo273bGV0sbvZgCvxw6eek6l4QgxDl88+fF/BX7Ois10OnTjjBiqGx8alqT7bd/JKbN+wWsKhoFuZ4sDxU7J9Q/aF9OVWkpj2n/7qL9xqF2rAyPmzD588K//02DPS0zcg//bvfyB/9Y3PytZ1q6543gAAVHY5ssoWm0rLs09n717cur1J1qyrL/jrnAoT/RdibrBgaDAurW2Ry/4402rrLt4+wwllRCKmTE2lJRa7vDceenv8wMPtd3XJfQ+uyHveqU4xOZGSY0dG5ZmnzsjoSFJ+/P1j7lnx9p0tsjQsza9VAAAKOX/0iHf9F9//qVt9Ia/dxvi4nN33rjzzN/9RBk6dlB/81b+Wz/67/1tW7fTboV6u1dftlmAk4v7Zp/70J1lz3fVX9Oc89/d/K4nJCdFNU+775j8r+Gva16yTM+++I1MjIzI+OCB1LVTzxAK127iCIDQAVDIqSQCoOkv0fBtl5kzvBfndq2+517s2r3fDEsfOnJv1Ixz03yQ4dZXVJAKm6VWreGPf3O6kKBaSqK+J5gUkpl9E1UYjbluPf/XoI96Bw+PPvXxV8wYAoCIs0U3ks8+cdcMIpqnJe967suiva1eqP1xJ5YXpKhKOQODSRxHTvyaVvLzqWOeUVhtd3TWznjcMTeobgnLt7jb56Cf9Pt/PPtMjlWxpfnUCADDHdhsiEm1szAtITJ9jROrr3fYYj/yb/8N9zLYsefkH372qpTWDQVm750b3+vBLf7iiP+PCiePy1hOPu9frb7hJElOTcu7QgVk/nDYh3u85RjUJzINiNwou0dc7AFAMlSQAVJ0Z1YMv2YYDuBL/47FnvFLUTmBhLqGF0+f6ZNdm/7D+SuzevlFeffegvL7vsDz6oQcu6/cmU2k5d2HQvV6zvOuiv3ZFV7vU19bI2MSkW1ECAIAlT1969xj0nZ+S11/JhjTTaVv+v3/9pzn9vt7eqcuuupC31dbmfnZ7uXv03rNKSGLZxdt6rNvQIKGQIYlExm3TkUxmJBj0+4BXEmriAQCqUTqZkMHTp9zrro2bL/pr29eslZqmJpkcHnYrSlytjbfcLgeff04Ov/gHeeCf/YvL/v3P/Of/ILaVca8Pv/SC++NS+o4dlXU33HRF8wWmOUGhQrQl+HoHAC6G73oAqrqkmKYcJxKRwHxxAgp7j5y47N93qrfvqj/2tVs2iKHrMjA8mtc6Yy5O9/ZJJvdCac3yzkv+ej33/5LOiygAQDXQiu0bK3cX+dRjp6TIGelFXUklicXSm5tbOGxIU3Pokr++pta/dyQez75RUenU1zgAACxlfceOiZXJ/vvduWHTJX+9pmXfDtGNqw9FbrjpFvfPGe0771WzmCsnWHHijdcv+2M6IQngqhULIdOKGkCVoZIEgKpTdL9HJQnMg3QmI999/DfudXtLo/y7/+mbYpoXf/H9z//3/yj9w6Ny+irbbTicdhib1qyQ/cdOuWGN1csuHXaYpoYq1l6ikoRTPWJkfMK9XtnVfhUzBgCgUiibSLvy34Te986QnDg+7l7fckendC+b3Zpipid+cVJiUxk531ueIYmhwbgXdOjsihbttzwtnbZkfCzlXuu6JjU1fhu0ir7/pfK/PAEAmBM1nNC18eIhCad6xMTQoFdV4mo5bTxWXLNDTr39lhx+8QXpXL9xTr8vk07Lb/7L/+NeN3Z1yzf/4btiBC6+B/mPn35ERs/3yoXjhCQwD4qdgXMTFIAqQyUJAFVn+u73mSr3HkCUk1+/8Jr09g+5159+772XDEg4lnW0uT/39g9KKp2+6jlcvy37wnwuLT5UJ3v8ShZrVxQPSSRTKflvP3nSG999w64rmicAAJXas82u8HehUylLnn7ytHvd0hqS+x5YIdfsbLnkj7b2iPt7JsZT7o9y09vjhzc6uy/easOx90+D7lo41q6vF8Oo1M9rpc4bAICr06eGJDYVb7eRSiTkyX//b73xrvd+YF6W3mm5MV0ZYq5e+9mPZejsGff63q/92SUDEo62VavdnwfPnJZ0MnnF8wUu1s7uUgFjAFhqCEkAqDrFC0kQk8DVGZuckp/9JttDcuPq5XLjzi1z+n3LO7MhCafVxdnz/Vf9adidC0k4lSGcthuXW0nCqUZRE4lIPJH0fkxMxdy5PfPSG/Kv/91/lYPHs2+s3HfzdbJry/qrnjMAAJVVSUIq2gvPnZORkewB+z33L59zOKC1LRuScJzvnZRyc67Hn9OlKmOcPjkuT/0yu59xzoNvv+viVbTKmZ0rHT6N420AQLU4f+SIV9UhUlcnydiU9yM2Pib9J0/IG4/9TP7rVx+V0++87f7a6z7wIVl/483z8vE33nq7V9HCabtxKVOjI/LCP/2De7182zWy5c675/Rx2lavcX92Wos4fyfgqnAGDgAu2m0AqDrFUrHsD3G1fvSr38tkLO5ef+4D98/59y3vaPWuT53rkzWXaHVxKe0tTbKiq13O9F5wW248eNueS/4ey7LkzPlsuw8nEPHFv/zri/76YMCUD993m3zwnluvaq4AAFQMpfysXcHtNkaGE/Li89lD/K5lUdm2o3nOv7e1Lexd956bkvUbG6Wc9J7zQxJmQJe+8/ltQVJJy23JcejAiOzfOyRWtoiE3PfgClm9tl4qF/e/AACqjxMYuHDimHsdGxuTv37fxc9hzFBIbvvso3Lrpz8/b3No6uqW9rXr5MLxY241iT0f+dhFf/3v//t/k/hEtt3Z/X/2F3P+OK25ShKOvmNHL9laBLg42m0AgIOQBICqU6xyWIXfEIgSc6osPPvKm+71zbu2yvpVy+b8e5fn2m04TvVmgwrzUU3CCUm8se/QnEISPRcGJZkq3uojFAhIbU1EVnS2y/YNq+XWa7dLY33tvMwVAIDKqyRRuSGJXz9x2msxcd8Dyy+rrK4akjjfmx9AKLd2Gz/8zqV7dtfVBeShh1fJtmvmHhQpR/bMkETlfnkCADBng6dPSTqRKPp8IByWSH2DtK9ZK6uv2y3b771faptb5n2FN95ymxuSOPTSCxcNSTgVIN58/DH3euvd98myLdvm/DHaVq/1ri8cy1bPAK6YVewUnE0kgOpCSAJA1aHdBhaC0zLjO3/9l1f0e9et7Jbv/V9/Na/z2b19k9v648Cx025lCKeFxsWs6Gyb9zkAALBkMxIVGpI4cWxM9u8ddq9Xr6m77EoQee02zl1eSCISNWX9xgb3Ohy+9FHEqrV10jqRlrb2i+9h1AoZU1PpixYCCYUMqa0LuhU0Nm5ulM1bmyQQqPwqDPaMox2NA24AQBVoW7NW/urZF0s9Ddl02x3ywrf/UU7/6S23xUekrr5oy4y//M3zV/QxujdvKYu/K5YGO+9WQf91jaZX5mscALhShCQAVB31bjm2fliq1i7vks89fL/bRiMWT1wyJAEAAC5BN7xL29IrshzZmnX18m/+zxuu+Pe3tIav+Pd3dkXlc1+ae2noj3x83WX9+Y1Noav6u1U0rfKDHgAAVKqujZvl/j/75277j8TkVNGQBFD2lSTYUwKoMoQkAFQdQ03FKpeZ6abEwBLx3jtuLPUUAABYOpSgrZVXSaKCUhJYkmZXkgAAAIvpxkc+wYKjYthWpvDG0Sm9BgBVhO96AKpO0DQK7gOTaUISAAAAKLaJDHmXdoaX0ig1P5xjazNCEqQkAAAAUISVTBZ8XFde7wBANeBkB0DVCZqFv/Wl0kqKFgAAAFAFQoUrSdhUkkD5VJLQdS2vvSAAAAAwt5BEkIUCUFUISQCoOuFA4UoSqQwhCQAAAMyhkoTl7ydzj7BsWFTqV5yt+QfaOgEJAAAAFNtD2rZYyUTB57RwmHUDUFUISQCoOuGAf6eVeoZIuw0AAADMJSSRsfLv1KeYBErJlkBeJQkAAACg4L4xnc5/8aIcjtNuA0C1yW9cCQBVIGTOvPMvi3YbuBzf/OTD7g+wTgCAKqwkkSlUSaL83pxubArJXfd2u9fhSOE9MCp0HZWzbVtTQhJUkgAALDEP/+u/cn+AdcLVK1ZFwqGHqCQBoLoQkgBQdYKmX0RHPcpOpmm3AQAAgCICfkjCyswoyujcjVV+GQlpag7J3fcvL/U0Kl65r6MltNsAAADAHPaNyeSMR/wXMVrA31MCQDWg3QaAqhMK+Hd/qTdaUUkCAAAAc6kk4Rwm2jNabgClYmv+gbZBuw0AAABcUSUJ9fUOACx9hCQAVJ1wkXYbVJIAAABAMZppihj+PtKylJfTal9foKSVJFh+AAAAzKGSxIw2bYQkAFQbQhIAqruShPJ4MmOVZD4AAACovJYbdrpw8BZYDGosx9b8r0t9xmE3AAAAUKiShLqf1AxDNMNkoQBUFUISAKpOsXYb6XRGbO4CBAAAQDFB/459K6OEJNhDooQsNSRBKQkAAADMpZKEQqPVBoAqREgCQNUJFWm34UhRTQIAAABzqCRhpf2X0wRtUUq2FvauqSQBAACAovvGlBqS8O8e1IP+6xwAqBaEJABUnWLtNhzJdGbR5wMAAIAKoRwe5lWSAErIIiQBAACAOcgk/HYb6sG4HvZDtwBQLQhJAKg6YaWShDYjJpEiJAEAAIA5hCQyadptoITsIiEJ2m0AAACg2BayWLsNpWIeAFQLQhIAqrqShEvJSVBJAgAAAHOqJKGGJNR3rIFF4X/N2VrUuzZmlsoDAAAAcqxkonC7jRAhCQDVh5AEgKoTVCpJONRzxFTaWvT5AAAAoBIrSZj+42QksMjULzkqSQAAAGAurFSq4OO68joHAKoFIQkAVcfQNTFNvXBIIpMpyZwAAABQAZQytFSSQLmwlEoSukYpCQAAAMyhkoSybdSoJAGgChGSAFCVwko1CfUckXYbAAAAKIpKEigDMwuXZLSId01IAgAAAMVYiSLtNqgkAaAKEZIAUJVCgfyWG9MISQAAAGBOIYmU327Dtum3gdKhkgQAAADmtG9MJb1r9RWMTiUJAFWIkASAqhRSK0koqdlU2irRjAAAAFBJIYl0Wg3dEpJAaVhaWDTND+zoOu02AAAAUGTvmPRDEio9FGbJAFQdQhIAqlIooBdpt5EuzYQAAABQWSGJVMB/nEoSKBFLq8kb024DAAAARfeOibgUOhTXAkEWDUDVISQBoCrVhvxDbfVeq8l4qiTzAQAAQAWI1BZst0FIAovLr1ySUUISmqYJhSQAAABQTHpivODjRm0diwag6hCSAFCV6qPBgpUkJhOEJAAAAFBEbb13mVIrSbg5CVpuYHGoX2kZzQ/uBAzdDUoAAAAAhVpt5LXbUPaNRp3/OgcAqgUhCQBVqSESLPiNcCJeuC8bAAAAIDX+HVZW2hDbUt6Qti0WCIuekrB0PyRhGgQkAAAAUFh6cqLo0ph1DSwbgKpDSAJAVapXQhLqzVZThCQAAABQTLRO2TxqtNxAyamVJEydIx4AAAAUlh4fU0bOaxq1kgTtNgBUH15BA5BqD0mofXunkmmxLEolAwAAYDbNeRNaqSaRSSotN2i3gUWivlqxdP/r0TQ44gEAAMClK0nYynm4Hg6LHgyxbACqDq+gAVSlvEoS6hO2LVPJVCmmBAAAgEqghCTSSdO7tglJoATSmt8/mnYbAAAAKLpvnBiXQqWVjVqqSACoToQkAFSlhryQRH7v3klabgAAAKCYGv9N6XTS31OKbbFmWBxKICejVJIIUEkCAAAARWTUkITCrG9gzQBUJUISAKpSTcgUXemzobbcmIxTSQIAAABF1NYXrCRBuw2UpN2GWknCaQcDAAAAFJCemChSScLfTwJANeEVNICqpGma1EcChfaFMplIlmZSAAAAqNhKEralvnUNLIyZX2UZ3b/zz6SSBAAAAIpIT/qVJGylsrJRR0gCQHUiJAGgatUrLTd0ZWNIJQkAAAAUpdxplUqo7TYISWCx6WJptd6IdhsAAACYUyWJvJAE7TYAVCdCEgCqlhqSUCtJTFFJAgAAAHNqt+FXJhPbYs2wCPwwTkavFdEMr1Ke2kIQAAAAUKUnxgtlJMSkkgSAKkVIAkDVaigSkpiIp0ozIQAAAFRUu41kIpT3lD2rGQIwv9SvsLSmttrQ3KAEAAAAMJOVSomViPsPKPtG2m0AqFaEJABUrbq8dhs+KkkAAABgLiGJvHYbDouQBBaY8iWW0Ru9a1PneAcAAABzqCIxAyEJANWKV9EAqlaxShKTVJIAAABAMTW13ubRtnTJpEz/OVpuYBGlNT8kETA43gEAAECRfePkhDJyXsv4h+G02wBQrXgVDaBq1Uf8HtJq/97JREosm7sAAQAAMJumGyKRWm+cTvh7SmEPiQWmvkrJGE3etUlIAgAAAEVklEoStnIOrgWDoofCrBuAqkRIAkDVqlcrSahP2LZMJVKlmBIAAAAqQa3fciMd9/eUNiEJLKK02m7DyHtFAwAAABRut6GUVDbrG1glAFWLkASAqpXXbiM/JkFIAgAAAMXV+CGJVDzkP05IAgtN+RpLa83etalzvAMAAIC5tNvwGUr4GwCqDa+iAVSt2nBANOW7YF7LjXiyJHMCAABABVAOE1MJP3grtlWa+aA6223ofruNAO02AAAAUER6fKxwJYk6QhIAqhchCQBVS9M0qQ8r1STyQhK02wAAAMClQxJJpZKEbalvYQPzK/+rS5e0EpKg3QYAAADmUknCVioqG4QkAFQxQhIAqlq90nJDVzaIE/FEiWYEAACAske7DZRYRq8XTTO9sUklCQAAABSRHhtVRmpIooE1A1C1CEkAqGrNNaGClSRGJuOlmRAAAADKX4N/B38iFsprt2HPuN8fmC/q11ZKb/GuNV0TQ30xAwAAAEzvIa2MpEZH/PVQto1mo/+6BgCqDSEJAFWtrT7iXRvKBnFogpAEAAAAimhs9S5TyaDYlrKRtCyWDQtDyd+klZBEyNTdVoIAAADATKnRUbEzGf8BZd8YaG1nwQBULUISAKpaW13Yu9ZnVJKwbe4CBAAAQJFKEoaRG2iSjqvVJNhDYmGoX1lqSCJoTn8tAgAAAPmSQwMzAhJKSKLZD38DQLUhJAGgqrUrlSTUkEQ6nZHJRKo0kwIAAEBZ03RDpMF/kzqptNywqSSBRZDS/QPtECEJAAAAFJEcGvSubSUgYTY0ih4Msm4AqhYhCQBVTW23oTn/qdUkaLkBAACAYpraCoYkaLeBhaJWuksb/tdf0ORoBwAAAIWlhof8Aa02AMDDK2kAVa0mFJBoyCxYTWJ4MlaaSQEAAKD8NashCb+FG5UksBBmNnFJ6/7XH5UkAAAAMJd2G7Yakmjx95MAUI0ISQCoevktN/yNIpUkAAAAMJdKEgklJCG2xaJhQdlaUCy9wRsHabcBAACAObTbyK8kQUgCQHUjJAGg6rXVKSEJZTWGJ+NVvzYAAAAooqnVu0xM+ftJsW1x/gPmk/o1lVKqSBiGLoZaDg8AAADIsZJJSY+PKeuhVpJoZ50AVDVCEgCqXn4lCX85RghJAAAAoJhm/1AxEQuJbSsbSYtqEphnSu4mpftfeyGTYx0AAAAUlhwZyn9AeclCJQkA1Y5X0wCqXlt9uGBIYnQyLhkOuAEAAFBItFYkmNtH2rqk40H/OfaQmGdqbZK0UkmCVhsAAAAoJqW02rDdVhvZw2/NMMRsbGbhAFQ1QhIAql5euw0lJGHbtoxNJap+fQAAADCb5hwyNvtvVien/OCtTUgCC5iSSBod3nXINFhrAAAAFJQcGvAHbkgiy2xuFU3n7UEA1Y3vggCqnhuSyO0RNec/Wm4AAABgLppavcuEEpIQm3YbmF+2kpJIKyGJIO02AAAAUERSqSShHnoHWvzXMQBQrQhJAKh6AVOXppqQ/41RCUkMT8Srfn0AAABQRHO7d5mc8quTUUkCC9Vqw5HSO71rKkkAAACgmOTwkLKnVEISbX7oFgCqFSEJABCRtjr/zj9DSdWOTMZYHwAAABTW6N+BFZ/0QxJCuw0sUEwio9eJrUfzAt8AAADArB2kbUtysHC7jWCL3zYQAKoVr6YBwAlJ1PuH2lSSAAAAwJw0+4eLMaWShMOm5QbmiW0XriIRNA3R1V6BAAAAQE4mNiVWQqmSrOwbTUISAEBIAgAc7XWFQxIjk7TbAAAAQBFNfkgikwxIJmX6z1FNAgsgaXR510GqSAAAAKCI1NDgjEeUShK02wAAQhIA4GgvUkliMp6UVDrDIgEAAGAWLRgSqa2fHkly0m/hZhOSwDxRCklIyvArSYRMgzUGAABAQUklJGFrflF5PRwWPVrDqgGoerTbAIAZ7TZmFqwdppoEAAAA5lBNIjERLdwjAbgaytdSUqeSBAAAAC4tOaxUklAOvAMtbaLRsg0ACEkAgKMpGhLTyO4WNdHyqkkMT8RYJAAAAFw6JDHlV5Kg3Qbmgz2rkkS3dx0MUEkCAAAAl64kIUooItDazpIBgFNZh1UAABFd16S1Tm254W8cB8cJSQAAAKCIZj8kEVcqSdgWLdswvzJavdh6nTcOGYQkAAAAUFiy/4L/2kQpJeFUkgAAEJIAAE9Hgx+SyBWVcPWPTrJKAAAAKKylw7uMTSi9fW1bbFpu4CqpX0NJpYqEE/KeroQHAAAAqDLx+Ix2G/6+MdjeyWIBACEJAPAtb671rg2lzs6F0UkOuAEAAFBYx3LvMp0OSDoR8J+jmgTmkRqSCAcMekkDAACgoMSF3vwHNP+wO9i9glUDAEISAOBboYQkdOWmrFgiJRPxJEsFAACAWbRorUhdozdO5LXcsFgxXBU7LySxzLuOBE1WFgAAAAXFz/shCVsJSOjRqJiNTawaABCSAIAiIQnR1CpkbjUJAAAAoKBOv5pEXAlJSCbDgmHe2m2kDP/rLBIwWFkAAAAUFD9/zh8oh9yh7hVUIwOAHKWgPABUt2jIlJa6sDc2lA1k3wghCQAAAFy65UZsXK0kQUgC81NFQsSQpNHljcIBKkkAAACgsEReJQk1JOG/bgGAakdIAgAUy5trvGtDqSTRTyUJAAAAzCUkMeFXJxPbzqsEAFwO9WsnaXSKpmWDEbquSdDkOAcAAACzZeJxSQ4PFq0kAQDI4lU1ABRpuWHo+e02OOAGAADApUISiamwWBllI0k1CcyDpNJqIxwwKJMMAACAghIX/CoSLs1/bRIkJAEAHkISAKBY0eKHJHSlkkQskZKJeJK1AgAAwCxatFakrnF6JIkJteWGxYrhiqg1SBLmSu86EqTVBgAAAAqL57Xa8N8C1KNRMRubWDYAyCEkAQCK5U1KSEI0tRqZ9I1MslYAAAAorNO/0z8+7ockJJNhxTAP7Tb80siRgMGKAgAAoKD4+XNFW21o6mE3AFQ5QhIAoIiGTGmpC3tjQ9k4Oi03AAAAgEu13IiN13jXNu02cJVVJJyjm/x2G1SSAAAAQGGJvEoSakjC308CAAhJAMAsy5v9Q21DCdf2E5IAAADAHEISU2N1/uO2nVcRAJgL9WsmZbSLaNkgt65rEjS53wUAAACzZeJxSQ4PFq0kAQDw8coaAGZY0ey33DD0/HYbHHADAADgUiGJ+FRErIyykaSaBK5CwljlXYcDBmWSAQAAUHjf2OdXkXBp/muSICEJAMhDSAIAZljR4ockdKWSRDyZkol4kvUCAADALFq0VqS+MTuwNUmMR73nbMtixXBZ1NojCdMPSUSCtNoAAABAYXElJGErAQk9GhWzsYllAwAFIQkAmGF5kxKSEE2tSuZWkwAAAAAuVU0iNua3cJNMhgXDZVEr2CWVShKRgMFKAgAAoKD4+XNFW21o6iE3AICQBADMFA2Z0lqX7fnrMJQN5IVRQhIAAAC4dEhiaswP3tq028AVVpEQMSVh+P2jwwEqSQAAAKCwxHm1koQakvBfpwAAsqgkAQAFLGv27/wzlJDtBSpJAAAAYE4hiTr/cdvOqwwAXIz6tZIwlommZYMRuq5J0OQYBwAAALNl4nFJDg8WrSQBAMjHq2sAKGBFs3/nn6F8p3QqSXDADQAAgEuFJJLxkGSSAf85qkngCiTM1d51OGBQJhkAAACF9419fhUJl+YfagcJSQDALIQkAKCAlS1+SEJXKknEkymZiCdZMwAAAMyiRWtF6hunRxIf9auT2ZbFimFO1JojCXOtdx0J0moDAAAAhcWVkIStBCT0aFTMxiaWDQBmICQBAAUsVypJ6KKp1cmkj5YbAAAAmFPLDX9PKZkMa4YraLfhV5KIBAxWEAAAAAXFe3uKttrQ1MNtAICLkAQAFODcpdVaF/bGhrKRPDc0zpoBAADg0iGJUT8kYdNuA3NgK3UkMlq9pI02bxwOUEkCAAAABfaQti2xs6f9cV5Iwn99AgDwEZIAgCJWtdZ516by3fLswChrBgAAgMK6/Tv/J8bqxLZzB5S2TVACl6QUkZC4uVamj7cNQ5Og+qIEAAAAyEkND0p6QrmxT/f3jaEV/usTAICPV9gAUMT6jgbv2lAqkg2MxySWTLFuAAAAmK17lfOOtntpW4YkxqPeUzYtN3AJSkZCEuZa77omGKBMMgAAAAqaOn1yRhWJ3GG2JhJetY5VA4ACCEkAwJxCEprfys22abkBAACAgjQzINK1yhvHRvyWG0JIAnMolVwwJBGi1QYAAAAuHZIQTaki0bVcjEiEZQOAAghJAEARLbUhaawJ5QUlpp0dGGPdAAAAUNgK/26tydF679q2MqwYirKVOhK2FpSEsdIbR0MBVg4AAACz95C2LbFZlSSywmvWs2IAUAQhCQAoQtM0Wd/hH2qrLYDPDhKSAAAAwKVDEuMj/n7SqUhGUALFKEUkJGGsFk3LVo8wDF1C6osRAAAAICc5NCjpyQl/PXR/3xghJAEARfEqGwDm3HLDf3xwbEpiyRRrBwAAgNm6V4kY2Te4M6mAJKfC3lM2LTdQhJKRkLi5wbuuCZpugBsAAACYKXb6hL+fdPeMuX2jJhJa6bdvAwDkIyQBABexIS8koYl6NtkzOM7aAQAAYBbNDGSDEjlTw3X+k4QkUCQg4ZRKnhY3/WokNaFs4AYAAACYaUpptSGa/5ZfqHuFGJEICwYARRCSAICLaK4NS1NtKC8oMa2HlhsAAAAoZqXf/3dSablBuw1cso6EZkrC9O/6qwkFWDQAAADM3kHatkyd8UMSthKSCNNqAwAuipAEAFzC+na/moTaCvjswBhrBwAAgMKW+29yjw/7+0mxbYISmEUpIiFxY5WIFnSvDUOXoPoiBAAAAMhJDg5IZnLSXw/dv8EvssZv3wYAmI1X2gBwCes7/Dv/TKXdxuD4lEwlUqwfAAAAZutaJeK03RCRdDIoqamw95RNyw3MoGQkJG76B9o1QVM0tecfAAAAkBPLqyLh7Blz+0Zdk/DKNawTAFwEIQkAuIT1Hf6df7qmiXpGScsNAAAAFKKZpkj3Km88OVznP0lIAjMCEk6p5Glxc6N3XROm1QYAAAAKmzrthyREabUR6l4hetgPaQMAZiMkAQCX0FwblubakDc2lZQEIQkAAAAUtWKddzmhtNywrQyLhsJ1JDRTEqb/dVMTNFkpAAAAzOKEbNWQhK2EJGi1AQCXRkgCAC6zmoShfOc8OzjG+gEAAKCwFeu9y/ERv4Wb2DZBCahfDp64sVZEy1aPMA1dgibHNgAAAJgtOdgvmalJ/wHdv7EvvNoP3QIACuPVNgDMwQYlJKGeUw6Nx2QqkWINAQAAMFvnChEz+4Z3JhmUxETEe8qm5QamvxaUlYgFNnnX0ZApmtrrDwAAAMjJryLh7Blz+0Zdk/DqtawTAFwCIQkAmIN1SkhCF03Us0pabgAAAKAQzTRFlq32xlNDSjUJQhLIBSScUsnT4qYfkqgJZQM2AAAAwExTp04oLzz8t/pCy1aKHvRbRwMACiMkAQBz0FQTkpa6sDc2lfJltNwAAABAUSv8Urfjw43eNZUkkPtK8BbC0iKSMPxQTU3QZJEAAAAwixOyjZ055Y+VkERktd/yDwBQHCEJAJij9R3+nX+GUkmCkAQAAACKWuEfUo4P14ttTW8kbbGtDAtX5ZQiEhIzN4mWO+A2DV2Cap8/AAAAICfZf0EysSl/PZQb+sJrCUkAwFzwihsA5mh9u99yQz2vHB6PyWQ8yToCAABgts7lImbQvbQtQ+Kjtd5TVJOAkpGQWGCzd10TMkVTe/wBAAAAOVNnTvqvKdw9Y27fqOsSXrmGdQKAOSAkAQBztL7DD0noool6ZnlmYJR1BAAAwCyaYYos91soTAz6e0rJpFmxKg9IOKWSp8XNLd51TShQolkBAACg3E2eOOYPlFYb4eUrRQ+GSjMpAKgwhCQAYI4aa0LSWh/2xqZSxuz4+WHWEQAAAJdsuTE21JhXScLOqyWAaqJ+7lN6u2SMtrxKEgAAAMBMVjIpUyeP+3tKXQlJrKHVBgDMFSEJALgM25Y1e9emUkniZP+opDMWawkAAIDZ1voVAmITNZJJKlUC0lSTqFZKEQmJBbZ618GAIUHTKM2kAAAAUNYmTx4XW61Ip5Q7jm7cVppJAUAFIiQBAJdh27Im79pUvoOm0xnpGRxjLQEAADBba6dI/fQ+UpNJpeVG3gEnqoraamMq4B9o14VptQEAAIDCJo8enFFFIhuSMGpqJbR8JcsGAHNESAIALsOatnoJB7N3dWmi5bfc6KPlBgAAAGbTnLu71vtvgo/lhSQyLFmVt9qwtYAkzA3euD4cLNGsAAAAUM5sy5KJY0f8sea/xRfdtE00pfUGAODi+I4JAJfBNHTZ2l24msSJvuG8u8EAAAAAz1q/ncLYUJPYdi5sa9tUk6hC6suGuLlRRMsGIwxDk0gulA0AAACo4ufOSmZq0n9ACUVEN29nsQDgMhCSAIDLtH15c8GQxEQsKQNjU6wnAAAAZluxViQYdi+ttCnx0RrvKapJVJ+irTZCwWzlEQAAAGCGiWOHZ1SRyO4btYApkXV+ZTIAwKURkgCAy7S5u0n0XJsNXTQxlO+ktNwAAABAIZphiqzZ7I3HB/zqZHY6zaJVESceodafi5nXeNe1kUBJ5gQAAIDyN3HkUMEqEpG1G0UPhkozKQCoUIQkAOAyRYKmrO+o98amcqfX8fPDrCcAAAAKW++33Bgd9EMSYltuf2FUXxWJpNEtGaPFvXZeVtSGCEkAAABgtuTwkCQH+2dUksii1QYAXD5CEgBwBbYtK9xyo390UsZjCdYUAAAAszmVJHKHmYnJiKRi/t1edoZqEtUiv4qE3zu6JhQQI1exDgAAAFBNHPWrSNhOula5cS+60Q9jAwDmhpAEAFyBbcv9kIShaaKeZZ7oG2FNAQAAMIsWjoqsWDs9kvGBRu85Wm5UUasNpZLEVHCHd10XpooEAAAACps8WrjVRmjZCjHrG1g2ALhMhCQA4Aq01IalqynqjU0lJXGij5YbAAAAKGKtf5fX2IDScsPK5L15jqVJ/Rxn9DpJGqu9cV04WKJZAQAAoJxlYlMydfa0N6bVBgBcPUISADDPLTfODIxKMp1hXQEAADDbOj8kMTFSL1ba8Ma03Fj61BjMlHmN134lHDQkoL6oAAAAAHImjx8VsazcaEarjc1++zYAwNzxChwArtD2vJYb/t7Usmw53T/KugIAAGAWralVpKUjO7B1mVBabkg6zYpVVauNnd41VSQAAABQzITSasN2KxpnD6LNxiYJdnSxcABwBQhJAMAVWtlSK3WRbN9gTTQxlQQvLTcAAAAwl2oSY/3NeZUk7LxaA1ha/M+trYUkbm72xnXh7OsKAAAAIG8HmcnI5PEjyj7SyKsioSln0gCAuSMkAQBXyNmAblOqSajVcZ2QhEVPaQAAABSyfpt3OTrYKLalbCQztG1bqtSXB1PmVhEtG4wwDV3CAf+wGwAAAPD2jWdOipVM+gviVpLIim72X1cAAC4PIQkAmKeWG2pIIp5My/nhcdYWAAAAs3WuFInWupe2ZcjkYL33lE3LjSVLbbUxGdzlXTvV6bgDEAAAAIVMHj3s7yd15wA6G5LQw2GJrF7HogHAFSIkAQBXYWNHo5i5dITTcsNQvqsePz/M2gIAAGAWzTncXFu85QaWHiceMR2RsMWUWOAa77n6cLBk8wIAAEB5h2wnjh7yx5p/+BzZsFk0wyzRzACg8hGSAICrEDB12dTZ6I+VcmfH+0ZYWwAAAFyy5cbIQLPYVm4fadtiW7TcWMpVJGKBLSJa2L3WdE2iIQ63AQAAMFuy/4KkRpUzZreSRFbN5u0sGQBcBUISALBALTdGJmIyMDbF+gIAAGC2letFzIB7aaVNmRqm5UbVtNoIXOtd14UComt+0BoAAACYNn5o34wqErl9o65JZMMWFgoArgIhCQC4StuWNfn7U6flhnLGeahngPUFAADALFowlA1K5IxdUFpupFOs2BJttSFOq43gDm9UF8kGZQAAAIC8PaRty9i+dwpWkQivWidGJMqCAcBVICQBAFepLhKU9e3+nX8BJSXhhCTUu8YAAAAAz6ad3uVwf0t+y41MmoVaItTXA1OBzWJr2QNtp4BEXThYwpkBAACgXMV7zuS12rCVkETtdv91BADgyhCSAIB5cP2atoItNyZiSekZHGeNAQAAMNuG7fktN4ZoubHkW20Er/Oua8NBMXRabQAAAGC2sf3vFmm1oUvNdr99GwDgyhCSAIB5sHNlqxi5ChJuyw3lu+vBnn7WGAAAALNowbDI+m3eePRCi3dtp9NiK00aUJls5bNoO602An6rjcYoVSQAAABQYA+Zycj4wb0Fq0hEN2wRI1rDsgHAVSIkAQDzIBI0Zdsyv490ULkj7Oi5IUlnLNYZAAAAs23xKwuM9Df7LTect9bTtNyodJaSc4kFtnmtNnRdk9pwtooIAAAAoJo8fkQysZj/gNpqY9duFgsA5gEhCQCYJ7uLtNxIpjNy8oLfPw4AAADwrN4oEsneCWZlTJkcbMyrJoHKZc9otTER9A+0GyJB0TVabQAAAOASrTbcgESugnEoJNFNfiU6AMCVIyQBAPNkS3eThIOGe62JJoFc+w3HwbO03AAAAMBsmmGKbN7ljUf6lJYbmXTem+yoLGq7FFsLSSxwjTduoNUGAAAACsjE4zJx5JC/j9Sz582Omm07RQ9QjQwA5gMhCQCYJ6ahy7WrWr1xQPkO61SSiCVTrDUAAAAu2nJjdKBZrLR/EGqn2UNWKjXfMhnYIaIF3euAqUs0aJZuYgAAAChbE4cPuGFpj1J9rHYnrTYAYL4QkgCAeXS90nLDKSQxvYe1LFuO9g6x1gAAAJita6VIYzZsa1u6jPc3eU/RcmNptNqYDN7gXTdEQqLRagMAAAAFjO1/Z0YViewBs1FXL+HV61gzAJgnhCQAYB6tbauXptqQ33JDV1tuDLDWAAAAmMV9w3zLtd545LxfnUysjNi2xapVGDUgkdHrJG5u9sa02gAAAEAhqbFRmTp90t9T6v5beLU7rhNNGQMArg7fUQFgng+4r1vdVrDlRu/QuIxNJVhvAAAAzLbVb7kxPtwg6YTfa5iWG5VH6bQhk4HrRbRsC5Vw0JBwwG+nAgAAAEwbP7DX79nmBKk1JSRBqw0AmFeEJABgnu3Oa7mhiVJMQg71UE0CAAAAs2lNbdm2G9mRjPW1eM/RcqOyW21MzGi1AQAAABQytk9tteG/fRfs6JRgZzeLBgDziJAEAMyzzoaoLGuu8cYzW26oB6YAAACAZ4tfTWKo1w/eimWJbWVYqAqh7vdTRqekzNXemFYbAAAAKCRxoU8S/X3+nlKtIrHj+myLPgDAvCEkAQALXE1CraY7PBGT/rEp1hwAAACzbd7lldSNT9ZIYjzqPUU1icqRX0XiRu+6JhyQgMExDAAAAGYbO/Cuv5+c2Wpjx/UsGQDMM16dA8ACuHZVm1MlOfeNVhP1LPTQWVpuAAAAYDYtWiuyepM3Hult9a7tdIolq5RWG95In9FqI1iiWQEAAKDcQ7Zqqw3R/bvuwqvXidnYVJqJAcASRkgCABaAU0Z3Y2dDwZYbh3oGxKLlBgAAAArZqrTc6GsT287tI21b7EyaNaugKhIxc5NYevZA2ymPXE9IAgAAAAXEzpyU9PiYv6fUlSoSO3ezZgCwAAhJAMACuX6133LDVL7bTiVScnbA3/QCAAAAnvXbRALZigOZVEAmB/zgLS03KiskMR662buuiwTEUILTAAAAwDS1ioTtttnI7hs1w5CabTtZKABYAIQkAGCB7FjRImYuHeG03DCVQ9EDZ/tZdwAAAMyiOQGJjdd44+He9ryQhK00c0B5sZXPjqVFJBbwD7Qbo7TaAAAAwGxWOiXjhw4UrCIR3bRNjEiEZQOABUBIAgAWSDhoyjXLm71xQPmOe7R3SGJJ+koDAACggC1+y43RwSbJJAK5kU01iTJmKfmVieANTuLFvTYMTWpC059DAAAAwDd+YJ9Yibj/QF6rjetZKgBYIIQkAGABXb8mv+WGlismkclYcuDMAGsPAACA2VauF6mpy17bmoyeb/GeslNJVqwM2TNbbQRv8a7rI0HRp18IAAAAAIqRt17z95RuQCK7b9TDYYlu3MpaAcACISQBAAtoc1eT1OdK62qiSUBpufHuqb68g1QAAADA3Tfqhsj2Pd5iDJ3r8BfGssS2MixUmVH39UljhaTNFd64KRoq0awAAABQzuK9Pe6PabbzOiCndtce0UyzRDMDgKWPkAQALCBD1+SWDZ3eOOjvc2V0Mi6nB0ZZfwAAAMy24yavDFk8FpHYSK6yBNUkylJeFYnQrd51JGi6PwAAAICZRt5+3d9Pak4ZYv8tu/ob/D0lAGD+EZIAgAV207oO0XMVJHTRxFSqSbxzso/1BwAAwCxaQ7PI2i3eeKin3bu202kqkpURS2y33YbD1oIyEfSrgDTXUkUCAAAAs2XiMRnb/+6MVhtZkbXrJdimVJMDAMw7QhIAsMAaokG5ZkWzNw4o1SRO9I3IeCzB5wAAAACz7brFuxzpb5FMyq9IYKdTrFiZUDvoTQSuF9Ei7rVhaFIfzrbeAwAAAFSj777thp+zNBElJFF/w20sFgAsMEISALAIbtvY5V2bzp53upiEbcu7py7wOQAAAMBsqzeJNLRkt42WLqO9rd5TdoqQRDmwZ7TamAj5B9pN0ZBXUQ4AAADw9pC2LaNvKa02DOetuuy+0airl+jmbSwWACwwQhIAsAjWtddLR0P2jjJNNAkoh6X7Tl+QdMbi8wAAAIA8mqaJ7LrZGw/2KCV3bUvszPSdZygVNSCRNJZL0lzjjZtqaLUBAACA2aZOHZfk8KC/p9T80sP1e24WzfAryAEAFgYhCQBYpANutZqE2nIjlkjJsfNDfB4AAAAw2/Y9IrlD0kQsIlND9d5TVJMoLSceYSkhibHQHd51bTggQVPZ9AMAAAA5I2++5u8pnTYbTjjaoWtSd70fkgYALBxCEgCwSHavaZNgIPttV3eqSRh+NYl3TvbxeQAAAMAsWqRGZPMubzx4zq8m4VSSsG0qkpVDFQlLi8hkcI83bq4Nl2hWAAAAKGepsVGZOHrIG9u6H6yt2bJDzPqGEs0MAKoLIQkAWCThoCm717R746DyHbh3aFwGxqb4XAAAAGC2Xbd4l6P9TZJOBLwx1STKIyQxEbxBRMu21wiYutSGKJEMAACA2Ubeft3ZSGYHTgWJ6SoSTquNG25lyQBgkRCSAIBFdOuGTu/a0DRRiklQTQIAAAAFaV0rRTpXZAe2LiPn/OCtnU6J7TZ+wGKyZ6z6uNJqo7km7LbbAwAAAPL2kJmMjP7pzRlVJLL7xkBbh4TXrGfBAGCREJIAgEXU3VQja9rrvLHacuNgz4AkUmk+HwAAAJhtl9+beKCnQ2w7t4907kJLs4dcbJaSkIiZmyRtdLnXTjaiMRpc9PkAAACg/I0f2i+ZqUlvbOt6XhUJgrYAsHgISQDAIrttY/YA1RHQ/Ypq6XRGDp4d4PMBAACA2TbtEglHs/vGZFAmLjR5T1mpJCu2iOwZrTbGQnd61/WRoJgGRy0AAAAo0mojLyCRPRjWgkGp3bmbJQOARcQrdwBYZDtWtEhtONtHWhNNArpfTeKdU315B64AAACAu28MBEW2+wenA2f9Nm5iWWJbGRZqkaj79bTeLPHADm/cXBvm8wAAAIBZEhf6JHbm1IxWG1m1O68XIxJh1QBgERGSAIBF5txZdtP6Dm8c9PfDMjwek57BcT4nAAAAmG2n33JjcrROEmPZyhIOO5VixRbJrCoSWvZoJRw0JBJQNvcAAABAoSoSTmnh3B7SUb/nVtYJABYZIQkAKIFbNnROV1MTXTRRK/I61SQAAACAmbSmNpHVG6dHMqhUk7DTKbHdRhBYSJayyrYWlImgf6DdXBOmjzQAAABmySTiMrb3TwWrSIRXrpZQ1zJWDQAWGSEJACiBppqQbFvW7I2DSsuNY71DMhGnrzQAAAAK2OW/KT/c1yqZZLaNm4NqEgtP7Yw3HrhRbD1bzUPXNWmIBBdhBgAAAKg04/vfFSulnPfqShWJG28rzaQAoMoRkgCAErltk3/nn6mLOFXWpsv3vnOSahIAAAAoYO1mkbrG3L5Rl+Gz7d5ThCQWlp3XakOT8fDd3nNN0ZAblAAAAADy9pCWJcOv/3FGFYnsvtGoqZWarTtZMAAoAUISAFAimzobpbUu7F5rouVVk3BCEolUms8NAAAA8mjOoerOm7zxQE+n2Fbupb1tuW03sDAspYzEVGCbpI3OvEpxAAAAwEwThw9IcmjQG9tKFYm6628UzTRZNAAoAUISAFAimqbJrRu7vHHQb0UnyVRa3j11oTQTAwAAQHm75kYRI7t5TKcCMtbb4j2VV8YXC1RFQmQ0dI93XRMOSCigbOYBAACA3P5x6I8v5AcktNzbcppI3e5bWCcAKBFCEgBQQjet75BIyPSrSRh+NYm3jvdKOmOVcHYAAAAoR1pNnci2Pd64/4wfvBXLEjtDRbL5pgYkksYySQY2e+Pp6nAAAACAaurEMYn39c5otZFVu+N6CTQ1s2AAUCKEJACghMIBQ24vUk0ilkjJ/jP9pZkYAAAAytueu5zSZO5lfCoqkwON3lNWkmoS88me0WpjNHyfdx0JmlITpEQyAAAAZhv84x8KV5EQkcbb72XJAKCECEkAQIndsblLAmb227E+o5rEG8fOiWX5B7IAAACAQ2tqFdm001uM/tNqNYkM1SQWqIpERm+UycD1eVUknDZ6AAAAgCrWc0ZiZ04VrCIR3bRNgh3K/h0AsOgISQBAidWEAnLLhs6C1STGpxJy+NxAaSYGAACA8nbDPd7lxEi9xMdqvLGdoprEglSRCN0jmpatHBEKGFIXDszLxwEAAMDSMqRWkdBmVJG4w69MBgAoDUISAFAG7trcLUaugoRTTSKgVJN4/ei5vLvXAAAAAIfW3i2yZktuMTTpP9XtLYydyYhtZVioq6Tuwy0tIuOh27wxVSQAAABQSKK/TyaOHvb3lIZ/V1x49ToJr1zNwgFAiRGSAIAy0FgTkt1r2rxxUPnuPDQekxN9w6WZGAAAAMrbjX41idH+ZklNhb0x1SSujj0jJDEWulNEy66v0y6vPhK8yo8AAACApWjojy9617bTmo0qEgBQdghJAECZuGfrcucGQJehaWLqfjWJ16gmAQAAgAK05WtElq2ZHkn/ab+3sZ1Oi21brNsVcgIS0xEJWwvIWOgu77mW2rDozoE3AAAAoEiODMnYwb3+nlL3q0gEu5ZJZP0m1gsAygAhCQAoE+31Edm5ssUbh/z9s/QNT0jP4HhpJgYAAIDydtO93uVwb5ukEwFvbCeTJZpU5VOrSIwHbxFbr3evDUOXpmiohDMDAABAuRp+5SURKxdUdkK1uv82XOOd94lG0BYAygIhCQAoI/dtW+5dO9UkDOW79OtHe0ozKQAAAJS31ZtE2pe5l7aty2BeNYkU1SSugKVUkRAxZCx8nzdqqQ2JrlR9AwAAABzpiXEZffdtf0/pVpHI7hsDrW1Ss2UHCwUAZYKQBACUkeXNtbK5u9Ebhwz/8PV0/6j0jUyUaGYAAAAoV+7daDfc7Y0HejokkzK9sZ2imsTVVZHYIxk9W/HNCUc014Sv+nMGAACApWf49T+KnUnnRjOqSNx2j2jKGABQWnxHBoAyc69STcJ0qkkoN6m9cfRcaSYFAACA8rZxh0hjq3tpW4YMnen0nrJT6bw3/XFxlqhVJHQZC7/HGzXXhMSgigQAAABmyMTjMvLWa/4e3PCrSJgNjVK7azdrBgBlhJAEAJSZde31srqtzhsHlZTE0fPDMjwRK9HMAAAAUK7cu9JuuMsb95/tFCvtHMw6bKpJXAY1TzIZuFbSRjZw4hTsaK6ligQAAABmG3nrVbGSfgU3W6ka0XDLnaIZfqU3AEDpEZIAgDIsl5xXTUIX8W5Ws215nWoSAAAAKGTrbpHaevfSSpsydLbDe8pOp/LqI6AwZ438qhuajEQe9J5rrAlJwOAYBQAAAPmsVEqGX/tjwSoSejQqdbtvZskAoMzw6h4AytC2ZU3S2Rh1rzXR8qpJHDw7IOOxRAlnBwAAgHKkmabI9Xd44/7TXWJlctUkbKeaRKp0k6sQVl4ViZ2SNpZlB5pIK1UkAAAAUMDoO29KJjbljW19uqKbSMNNd4geDLFuAFBmCEkAQJlWk7hPqSYR0LPlfR3OnW1vHust3eQAAABQvnbeLBLOhm0z6YAMn2n3nrJTTvlfqknMrYqEyGj4vd51QyQoQdM/7AYAAADcPWQmI0OvvDgjIJE9yNWCQam/8TYWCgDKECEJAChTu1a1Sktd2KsmEVKqSew9fUEm4n6POwAAAMDdNzp3qV17q7cY/ae786pJOKWAcekqElOBHZIy/dBya12EZQMAAMAso+++JenxsRmtNrLq99wiRrSGVQOAMkRIAgDKlKFrcveW7oLVJDIZS149fLZ0kwMAAED5uu42kUDQvUwXqCbhVEzAxapIaDIcfr/3XF0kKOEAVSQAAACQzwkgD774e39Pqet+FQnDkIZb7mLJAKBMEZIAgDJ2w9oOqYsEClaT2He6X4YnYiWcHQAAAMqRFqkR2XFTfjWJtF9Nwk5SkexiVSQmAzslnVdFIlvdDQAAAFANv/GKpCfGvbFtmN517XU3iFnfwIIBQJkiJAEAZSxg6nL/9uV51ST0XE7CudPt5UNUkwAAAEABN9ydV01i6EyH95SdSilVE2DNqCIxEnmftyi14YBEg/5hNwAAAODIxKZk6I8v+Hts3fCrSJimNN1xPwsFAGWMkAQAlLmb13dKS+7utZnVJI6eG5S+kYkSzg4AAADlSKupE9l9pze+cEapJuGEAlKJks2t3NhKGYnJwHWSNpZ5446GSIlmBQAAgHLmBCSsRNwb24bfnq3+hlvFbGwq0cwAAHNBSAIAypxp6PLQjpX+WKkm4XjxwJnSTAwAAADlzQlJhKPupZU2ZfB014xqEpZUO8tpP+KNdBmJvN8bNUSDEg5QRQIAAAD5UmOjMvzmqzMCEtkDWz0clsY7qSIBAOWOkAQAVIDrVrdKd1ONV00ibPopibMDo3K6f7SEswMAAEA50kJhkZvu88b9Z7okk/Lf9LeTSalmTjhCbTsyHrxB0ka2LYmmibTXU0UCAAAAsw2++Hux0+ncSMu12shquPVuMaLZc1wAQPkiJAEAFUDTNHn/tau8salpYijfwV86eJq+0gAAAJht180i9Y3upZUxZOBkt/eUnU6JbWWqdtWcgIQfkTBlNPw+b9RUE5ag6R92AwAAAI7E4ICMvvuWtxiWUkXCqK2Vhlv8lncAgPJFSAIAKsTmrkZZ11HvjcOGX03iwsikHO0dKtHMAAAAUK40MyByywPeeKCnU9LxoFR7NQk712pj2ljoVskYLe61pmvSVhcu4ewAAABQrgae/62TtnWvbaf8mO6/zdZ09wOiB0MlnB0AYK4ISQBARVWTWO2NDU0TU/eDEi8dPCMZi77SAAAAmGHr9SIt2TYStqXLhRPLvKfsTFrsTPVVk1DbbNhaSEbDD3nj1tqwmGrZNgAAAEBEYufOysThA/4+0m2zkT2fDTS3St31N7FOAFAheNUPABVkdWudbF/R7I3DSgXg0cm47D/dX5qJAQAAoGxpzt1tt7/XGw+db5fkpF8pwUompJqrSIyG7hFLz1ZsMwxdWmqpIgEAAIAZe0jbloHf/8Yfz6wice9DohkmywYAFYKQBABUmPftWjUdUBZd0ySgtN145fBZSVXhnYAAAAC4hHVbRbpzVclsTfqOr/CfszJuRYlqrCJhabUyGr7PGzttNgylWhsAAADgmDpxTKZOn/T3lG4gIrtvDHYtk5prrmWhAKCCEJIAgArT2RCVG9e1e+OQUk1iKpGSt4+fL83EAAAAUNat2+SO93nj0f5miY/WVl01iZlVJEbCD4poEfc6YOrSVEMPaQAAAMzYQ9q29D+vVpHQnXJt3rj5/vdn99sAgIpBSAIAKtCD16wUM1dBQhdNgko1iTeO9UosmSrh7AAAAFCOtOVrRNZumR5J79GV/pOWJXZ66e8h1YBEWm+VsdAd3ri9PuJWagMAAABU4wf2SqLPvzHNNvy71iJr10tk/SYWDAAqDCEJAKhAjTUhuW1TlzcOKtUkkqm0vH70XGkmBgAAgPJ2+3udshLu5eRovUz0N82oJuGHCJYae0arjaHIw6Jp2b7RoYAhDZFgCWcHAACAcmRnMjLwh2f9sZ5fRaLpvvdRRQIAKhAhCQCoUPdtWy7hXDrCqSYRUqpJvHOiT8Zj1VEyGQAAAHOntXWJbLnOG/ceWyG2ndtH2rZYqVRVVJFIGKslFrjeG3c0RDjcBgAAwCyj77wpqZFhb2zr/t1qNVt3SHjFalYNACoQIQkAqFA1oYDcs3WZN3byEtPVgTOWJa8cPlu6yQEAAKB83fqASK5EcGIqKiM9bd5Tdiop9hKsJuH8nfKqSEQ/4m2eoyFTakOBEs4OAAAA5chKJmXgxefyAxLTVSQ0p4rEe0s3OQDAVSEkAQAV7I5N3VIXyR7oajOqSew/MyBD47ESzg4AAADlSGtoFtl5izc+f2KFWOncHXG2LXYyKUuNpeQ+JgO7JGmu98Yd9VGqSAAAAGCW4ddelszkpDe2c0FjR911N0qwrYNVA4AKRUgCACqY0zv5gWtWeOOALqJP5yRsW57beyLvjjkAAADAddO9IoGge5lJBWTgZFd+NQnbWlJtNvw9sSnDkQ97z9VFgm4lCQAAAECVGh2RwT/+YUZAInvwqgVMabr7QRYMACoYIQkAqHA3ruuQ1rpwwWoSZwfG5GjvUAlnBwAAgHKkRWtF9tzljfvPdEsqFvLGdiIhS4ETjVBDw6OhOyVj+O1F2usjJZoZAAAAytmFZ38tdjqdG2nZVhs59TfeLmZDY8nmBgC4eoQkAKDCmYYu79u1yhsHdE1Mr5yEyPP7TkkqnSnR7AAAAFC2dt8h4oQl3CCBLn1H/Qpldibt/qh0TkBiOiJhabUyGnnIe64xGpJwwD/sBgAAAByTx4/KxOED3mJYShUJPRyWxtvvZaEAoMIRkgCAJWDnyhZZ11HvjcNKxeDJeFJeOdJTmokBAACgbGnBsMgd7/PGI/0tMjVc542tREL8iEHlsXOtNqYNR94vthZ1r3Vdk/YGqkgAAAAgn5VOy4XfPOXvKTWnv7H/VlrTfe8TI1rDsgFAhSMkAQBLgKZp8tE9a8XZszt00SSotN1461ivDE3ESjdBAAAAlKdtu0W6V+cGmvQeWS22ndtH2pbYyaRUKjUgkTSWyXjwtrw2GwGDIxEAAADkG37tZUkOD3pjW6kiEexaJvV7bmHJAGAJ4EQAAJaIrsYauWNTtzcOGU54wi8z/Pu9J/P6MQMAAABO2Fbu+7C3cYxN1MhoT5u3MHYqKbZtVWRAQt37DkU+JlouURwKGNJcEyrh7AAAAFCOUmOjMvjy897Y1p0DVv9ttNb3PyKaUlUCAFC5+G4OAEvIg9eskLpIwL3WRJOwUk3iTP+oHO0dKuHsAAAAUI609mUiO2/2xr0nVkgm7dwxl2UnElJJnGiEGpCYDFwnicBGb9zVGM2GQwAAAADFhd/+SuxUKjfSclUksmqv3SPhldMV2AAAlY6QBAAsIeGgKQ9f52/WA7omahXhP+w/Jal0pjSTAwAAQPm67UGRaK17mUkFpP/4cu8pO5N2f1QKJyAxHZGwtYAMRT/iPdcQDUpNKBsqBgAAAKZNnjwmE4cPeGNLabOhh8PS8sDDLBYALCGEJABgibl+dZus66j3xhHTv0tuIpaUV4/0lGhmAAAAKFdaOCpyx3u98UBPpyQmot7YSiTEjx6ULzvXamPaSPgBsfRm91rXNelo8P9OAAAAgMNKp+XCM0/6e0qnxYbSVqPp3veKUZMNFAMAlgZCEgCwxDilgz+ye63XLk8XTYJK2423jvfK8ESsdBMEAABAedq2R6RrZfba1uTcIaWcsG2JnUxKuVMDEim9TUZD7/HGbXURCahl1gAAAAARGX79j5IcGvTWIttmI3ueGuzslvo9t7BOALDEcDoAAEtQd1ON3LGp2xuHDCc8kb22LFue23syr08zAAAA4IRt5b6PeBvHydF6Ge1t9RbGTiXFtq2yDkioe9yh6MdE00z3OhQwpLk2VMLZAQAAoBylxkZl8KXfe2Nbdw5S/bfOWj/wiGhuaAIAsJQQkgCAJeqBa1ZIbSTbb1kTTcJKNYkz/aNy7PxwCWcHAACAcqR1LBfZebM3PndspVhp/1DYTiSkHDnRCDUgMRXYIfHAdm/c1RgVfTo1DAAAAOT0/+5psVOp3EjLVZHIqr12j4RXrmGtAGAJIiQBAEtUJGjKB6/1SyQHdE3U6sLP7zspqXSmNJMDAABA+brtQZFIjXuZSQblwvHl3lN2Ju3+KDdOQGI6ImFrQbeKxLSGaFBqQtnwMAAAADBt8uQxGT+4zxtbSpsNPRyW5vd8gMUCgCWKkAQALGHXr2mTNe113jhi+nfPTcSS8tqRnhLNDAAAAOVKC0dF7nifNx442ynxsag3ttxqEuXTus3OtdqYNhJ+SDJ6i3ut65p0NPhzBwAAANw9ZCYjF5550t9TOi02dP8ts6Z7HhKz1j9XBQAsLYQkAGCJ95V+ZM86r42eLpoElbYbbx7vleGJWOkmCAAAgPK0fY9I18rcQJNzh5Qyw7YlViop5UINSCSNbhkN3euN2+oiElDLqQEAAAAiMvz6y5IcGvTWIttmI3tuGuzskvobbmWdAGAJ46QAAJa47qYauW1jlzcOGU54InttWbb8fu/JvP7NAAAAgBO2lXs/7G0cp8brZPhsu7cwdjIptm2VRUDC38tqMhj9pGia6Y5CAUOaa0MlnR8AAADKT2psVAZffN4b27pzYOq/Xdb6/kdEc0MTAIClipAEAFSBh3aslNpItg+zJpqElWoSp/tH5dj54RLODgAAAOVI61whsvNmb9x7bKWkE9k9pcN2226UjhONUMO+48GbJGmu98ZdjVHRp9PBAAAAQE7/757Oq4yWrSKRVbtrt4RXrWWtAGCJIyQBAFUgEjTl4WtXe+OAroladfh3756QWDJVmskBAACgfN36gEg46l5aGVPOH17lPWVn0mKnU6WtIpG7zuh1Mhz5iPdcfTQoNSE/0AEAAAA4xg8fkPGD+/w9peFUIcsGa/VwWJofeJiFAoAqQEgCAKrE7jVtsrqtzhtHTP+uulgiJc/vPVWimQEAAKBcaZEakTve541H+ltksr/RG1vJRElat1mittkQGYo8IrZe415ruiadDdlgBwAAADAtE5uSvqcf98a202JD998ma7rnQTFr/fNTAMDSRUgCAKqor/QjN6ybDkaLLpqElLYbh3oG5Pj5odJNEAAAAOXpmhtEOlfmBpqcPbxGrHSuJLFTzSERX/w2G5YfkJgKbJWp4B5v3F4XkYBaNg0AAAAQkb5nnpTM5OSMNhvZ89FgZ5fU33Ab6wQAVYJTAwCoIsuaauTuLcu8cdCQvLYbv32HthsAAACYHbaV9zwiomeDEalESPqPLy9Z2w21zYathWQo+invuXDQkOba0KLNBQAAABXUZuPAXm9sO202nEoSDk2k9eGPi+aGJgAA1YCQBABUmYd2rJT2hoh7rYkmESO/7cbvabsBAACAGbT2bpGb7/PG/Wc7JTZSu+htN2a12Qg/LBm9JTtHLRsK1p0LAAAAICc9NSV9v85vs2ErbTYabrtHwitWs14AUEUISQBAlQmYunzq5g1+2w0tv+3G4Z4BOUbbDQAAAMx0w90i7dNVyTQ5e3Ct2Ja2aG03ZrbZSJhrZCJ0pzduq4tIOGDyeQMAAECeC795UjJThdtsBNrapenuB1kxAKgyhCQAoAqtbq27aNuNZ2m7AQAAgBk0pyTxQ58UyZUhTkxFpf/EskVru5HXZkNMGYh+1iuR7LTZaKkL8zkDAABAnvFD+y/aZqPtw58SPRBg1QCgyhCSAIAqNavthjmz7cbJEs4OAAAA5Uhr6xK5yW+7ceF0t8THowvedmNmm42RyEOSNrqyc6LNBgAAAIq12Xj6CW9Mmw0AwDRCEgBQpWa13RBNQkpQ4nDPoBztHSrdBAEAAFD+bTdsXc7uXye2vXBtN2a22UgaK2Qs9B5vTJsNAAAAXH6bjQ7abABAFSMkAQBV3nbjnq1K2w09v+3G7949IbHkwpVMBgAAQOW33YhP1sjAye4Fa7uhttkQMaW/5vPOJNwRbTYAAABwRW02PkKbDQCoZoQkAKDKPXjNSumg7QYAAACuou1G38llkliAthsz22wMRx6UtJEN+dJmAwAAAFfSZqPxtnslvHwViwcAVYyQBABUOdpuAAAA4IrbbnT4bTfOOG03rPlruzGzzUbCWCVjoQe8cVt9RMIBk08eAAAA8lx45omLttlovNvfUwIAqhMhCQCArGqtk3tpuwEAAIDLbbvxYH7bjf4Ty+at7YbaZsMWUwZmtNlorQ3z+QIAAMDsNhsH9/l7UtpsAAAKICQBAHA9QNsNAAAAXFHbjfu98YXTyyQ+WnPVbTdmt9n4gKSNruzH1ESWNdWI5lwAAAAAObTZAADMFSEJAICLthsAAAC4IjfcpbTd0OTM/vViW/oVt92Y2WYjbq6X8dC93pg2GwAAALiSNhtN9zzIwgEAXIQkAAAe2m4AAADgattuJGIR6Tu64orbbqhtNiwtLANRp81G9viCNhsAAAC40jYbmmmyeAAAFyEJAMBltd149p0TV1QyGQAAAEu87cbNftuNgbOdMjlYP6PthnXZbTaGIo9IxmjNfgzabAAAAKCA9MS49D39hDe2NV1s3X/7q/G2eyW8fBVrBwDwEJIAAFxW241jvUPy7qkLrBoAAADy3XC3SMfy3ECTswfWSSaVu1vPtsWKX7zthhONsJQ2G5OBnTIZusUb02YDAAAAs/aQliW9j/+UNhsAgMtCSAIAcNltN/6w75T0j/r9/QAAAABNN0Qe/ITXdiOVDEnvwdX+wlgZt6LExdpsTMvoDTIY/Yw3DgdNaa0Ns8gAAADIM/TyH2Tq1Al/T6m22dA1af/op2mzAQCYhZAEAKCgB3fMbrvhlDh2ZCxLnnrzqKTSGVYPAAAARdtujPS3yui5bLsMh51Kip1JFwxI+G02NOmPflZsvTY7os0GAAAACpg6fVIGXnzO32s6LTac4G5O4233SGjZStYOADALIQkAQEGmocvnb9skpqF5bTecoMS0kYmYPPvuibye0QAAAIDbdmP5Wm8heo6skVQs5I2tRDxvD2mJnVdFYjR0lyQC27xxR0NUwgH/sBsAAABIT01K7y9/4rZ1c2ma2E4ViZzQshXSdM+DLBQAoCBCEgCAorqbauRD16/xxqamSTAXmnAcOjsgB84OsIIAAADIb7vxvs+IhKPu2MoYcmbverHt3D7SqRqRiGV/dvtI+wGJpLFMhiMf9sZ1kaA01/gBCwAAAMAJ3J5/4ueSnhjPb7MhuZu9wmFp//jnRVNCEwAAqAhJAAAu6pYNnbJzVYs3DhkihvKvx3PvnpCh8RirCAAAAI9W1yDy3k9546nxOuk/ttwb25mMJC/0ZttsTD+mBaS/5kuiaaZX2ay7KSradM83AAAAQESGX3tZJo8f8feWThhC8w8sWz/4CQk0+y3fAACYiZAEAOCinEPpT9y4Xpprs3fwabm2G9Nn1emMJU+9ccT9GQAAAPD2kWu3iOy5yxtfON0tU0P13jhx/pzY6bQ3How8ImmjK/ebRVY014rp9JUGAAAAcmLnzsrA73/jrYet62I7lcxy6m+4VWq372K9AAAXxWkDAOCSIkFTHr19kxi5Vhu6aBJW2m4Mjk/J8/tOspIAAADId9uDIl0rcwNNTu9fL5lkwK0e4VSRkGTCbbsxGbhWJkO3e7+tvT4i0RDlkQEAAODLxOPS+9iPxbZyN2tpWraKRE6ws0uaH/wgSwYAuCRCEgCAOVnZUifv37XaGwd0TYJKUGLvqQtyuGeQ1QQAAIDH7QP9/s+KhCLuOJ0Myqm9a8TvsWFLakKXgeinvd9TGw5Ia22YVQQAAIDHtm05/9QvJDU64j1muQGJ7PmkFgxK+8cfFT0QYNUAAJdESAIAMGd3bu6SrcuavHHIEFFyEvLbd47LyGScFQUAAIBHa2gWeeBj7rVTPOLo8YCcPtSSG2vS82qnpPfuc8emocuyphq35RsAAAAwbfTt12Xi8AFvbBuGiOa/xdX6gY9JsK2DBQMAzAkhCQDAnDmH1Z++ZYM01gSzY9EkEvAPsFPpjPzqzSOSzuRK3gEAAADOvnHjDpFdt8jwREwSqbS8+3aLjAxE5MKhTomPRiX1+gti9Z2TZc01blACAAAAmBbv65ULv/2VN7Y1XWzd8Ma11+6Rul27WTAAwJxx8gAAuCw1oYB87tZNXlBbd4ISph+UuDAyKS8dPMOqAgAAIM+prbfLab3GqyCxb3+D9B9vzT5p26K/+CuJ2GlWDQAAAB4rmZDex34sdiaTe0QT2/TbbATa2qX1/R9lxQAAl4WQBADgsq1tr5eHdqz0xgFdk4DSd+Pt471y/PwQKwsAAADXRCwpT797Wt7cfI+kjYBXlaw/lnZbcBi6JmZsQs4/+Qu33zQAAADg7Av7fv2EJIcGvcWwlICEFjCl/eOPih4MsVgAgMtCSAIAcEXu27ZcNnY1eOOwIaIrraOfefu4jMcSrC4AAECVs2xbfv3WUYknUzIZbZR3198mhpbdOCYytoylMhIOZMslTxw5KCNvvlriGQMAAKAcjL37toztf8cb24YhXnlbEWl56MMS6uwu0ewAAJWMkAQA4IpomiafuWWj1Eb8OwGjStsNp9f0r948KhnLYoUBAACq2KuHe6RncMwbDy7bIL3dm7xxfU1ENKWndP/vnpb4+d5FnycAAADKR2KgX/p+86Q3tjVdbGXPWLN9p9TtvrlEswMAVDpCEgCAK1YfCcpnb9k4XeFOdE2TiBKU6B0al5cPnmGFAQAAqtTp/lF59UhPXpu2oK7J4Y23uFUlWuvCUhsO5vWVdvpNn3vsR5KJx0o4cwAAAJSKlUzIuV/8UOxUyntM3S+azS3S+vAn3Ju4AAC4EoQkAABXZVNXo9t6Qz34Dhj+C5Q3j/XKwbMDrDIAAECVGZ6IyVNvHBGxbXfstGYLm7mDbyMgw3c8LB3N9blfreX6S2elhofk3C9+JLaVKcXUAQAAUCK2bUvv4z+T5EC/95hlOpVss+eNmmFIx8c/L0YkwucIAHDFCEkAAK7agztWypr2Om8cNrKH4NN++6fj0jcywUoDAABUCaf12uOvH3Z/nuZUHHP+c4SDhnzivbdK6/s+6v8mp4Sy02c6Z+rkcen/3TOLO3EAAACU1OALz8nEkYPe2N0fav5bWc3veb+Elq0s0ewAAEsFIQkAwFUzdE0+f+smiYayd/85h9/RgCbTFe8yliWPv3ZYJuJJVhsAAGCJs2xbfv3WMRkej+UFJAylHPKnbtogzbVhqbv+Rqm95lrvcVs3xdb9o4rh1/8oo++8tYizBwAAQKmMH9wngy/93hs7+0Jb90O00c3bpP7mO0s0OwDAUkJIAgAwLxprQvK5WzdOV74TXTT3MHzaZDwpT7x2WNIZixUHAABYwl4+eEZO9g1746ChuS3Zpt25pVt2rGxxr50+0q0Pf1wCbR3e87Zhiq0EKvqeflxiZ08v2vwBAACw+OJ9vdL7xM+8sbMfdPaF04eNZnOLtH34U+7+EQCAq0VIAgAwbzZ3N8nD1672xqamSVgJSjgtN377znG3tyAAAACWnoNnB+SNo+e8salrEvJv/pPN3Y3yAWW/6NDDYen8zFdE9/pKa2IrfaftTEZ6fv4DSY2NLs5fAgAAAIsqPTkhPT/5nthpv1WbGpDQgkF3v2hEa/jMAADmBSEJAMC8umtLt+xe2+aNg7omAcMPShw6OyBvHe9l1QEAAJYYNxD7p+Pe2CkeETGzrdgcbfUR+fxtm9xWbTMFWlql4xNfyP4mlyaWmW3l5shMTsq5n35frFRqEf4mAAAAWCxWOi3nfvYDSY+P+Y85gVkt9/aVJtL+sc9JsL2TTwoAYN4QkgAAzCun5N3Hb1gvq1prvcfChoih/IvzwoH8EswAAACobBPxpDz+2mHJWNnWak4V5GjAiUdkQw/hoCFfuWuLRIJ+8GGmyLqN0vLgh/wHNF0s9w5CvwTz+Sd/TlUyAACAJcKpNnvhmSck1nPGf8zZ/00HJESk+b73Sc3m7SWaIQBgqSIkAQCYdwFTly/esUXqo0F37ByOR0zNPSx32bb86s2jMjQRY/UBAAAqXDpjyROvHZbJeNJ7zNn76bmAhPOTU0GivX66nUZx9TfdLnXX3+g/oBti636/jvGD+2Toj3+Y578BAAAASmHkjVdk9J23vLGt63l7v9prrpWG2+/lkwMAmHeEJAAAC6IhGpQv37lZTDP7T41zSB41/dLKyXRGHn/1kMSTfq9BAAAAVN7df79957jbamNa2NTE9NKxIg9fu1q2dDfNuSpZ6wcekfCqNf7HMAyxlbsJB55/ViaOHJy3vwMAAAAW3+SJY3Lh2V97Y2e/51aRyAl1L5fWD33S3R8CADDfCEkAABbMypY6+eSN672xoWUrSkwbmYzLU28eEcuy+SwAAABUoLeO98qhswPeOGBoEtT9/d7utW1y15buy/ozNcOUjk99ScyGxulHxDZNsZUD8t5f/lQS/X3z8DcAAADAYksODcq5X/zIrTbr0rL7PbcEmXOGWFsrHZ/+sujBbJVaAADmGyEJAMCCun5Nm9y7bZk3DuiahAz/gPtM/6i8cOA0nwUAAIAKc7JvWF444PePNnSRsF8dWVa11srHb1h/RXf/GTW10vGZL4sWCOQe0fLuLLRSSen5yfckE5u6ur8EAAAAFlUmHpeen35PrETce8xy93nZPaNmGNLxqS8rgVkAAOYfIQkAwIJ7785VsnWZX2LZCUmYyh2Gbx/vlf2nL/CZAAAAqBBDEzH51ZtHvbv/nByEUzHM+c9RHw3KF+/YIoFc67UrEepaLm0f+bT/gKaLZU6HJkRSoyPuHYh2JnM1fxUAAAAsEtuypPeXP5Hk4EB+QEJprdb6wY9LeOVqPicAgAVFSAIAsOB0XZPP3bpROhoi3mMRU0TJSciz756Qc0PjfDYAAADKXDyZlsdfPSTJtB9OiJqa6LmAhGnq8uU7N0tD9OrLI9du3yWNd73Hf8DtVe2Xq5g6dUIuPPurq/44AAAAWHgDz/9WJo8f8cbuvk7393YNt9wpddfewKcCALDgCEkAABZFOGjKV+7aIpFQtkyyc5dhNKC5dx06LMuWJ14/LOOxBJ8RAACAMuXs2Z5684iMTPrlkZ0KEobSUuOTN66XlS118/Yxm+55UGq2XuONbd0UW/ePM0befE1G3n593j4eAAAA5t/o3j/J0CsvemNnP2crAYnIhs3S/J4PsPQAgEVBSAIAsGha6yLyhds3eRX0nLsNnUP1abFESh5/7bCkKJkMAABQll44cFrO9I/mtVELKOXB7t22TK5f0zavH1PTNGn76Gck2NnlPWYbpthKMOPCM0/K1OmT8/pxAQAAMD9i585K369+6Y2dfZyzn3Nuo3IEWtuk/WOfF02pGAYAwEIiJAEAWFQbOxvlQ9et8campklYCUr0j07Kr9446t6lCAAAgPLx9onz8vbxXm9s6pobkpi2dVmTvHfnqgX52HowJB2f/ooY0ZrcI5rYZsA7WHf6W5/72Q8kMdC/IB8fAAAAVyY5PCQ9P/me2Jm095ht+Ps4PRyWjs98RYyI36YXAICFRkgCALDobt/UJTeu7/DGQV2ToHLAfqJvWJ5997jYNkEJAACAcnCoZ0Ce33fKGzvFIyLZLmqujoaIfO7WjaIrVSXmW6CpWdo/+QURr9WGJpbpTyITj8nZH/6TpMb8ShcAAAAonfTEuLs/y0xNeo9ZTtB1uiKYJm4FiWBre+kmCQCoSoQkAACLzimZ/MietbKm3e9VHTKydyNO23+6X14+dJbPDgAAQImd7h+VZ94+JpILsDpn2tGAJs5/jkjIlK/ctUXCQSU1sUAia9ZL6/s/6j+g6WK5pZqz0uNjcvaH35ZMbGrB5wIAAIDiMom4nP3xdyQ1Muw95u7bpvvwikjzex6W6MYtLCMAYNERkgAAlIRp6PKlO7ZIY03QHTuH7M7diIbyL9PrR3rcss4AAAAojb6RCXn89cNeKzQnIFFjaqLnAhLOGfejt22S1rrFK49cv+cWqb/hVv8B3cgLSiQH+92SzlYqtWhzAgAAgM9Kp7Ot0Pr8cz3b2a/phjeu3Xm9NNx6F8sGACgJQhIAgJKpDQfka3dvlXDQ8IISUefQXanS7JR1dso7AwAAYHENT8TksVcPSTqd8R7L7tX8zdrHb1gnm7oaF/1T0/LeD0l001b/Ad0Q2/AP3WM9Z+TcL34kdsafOwAAABaebVly/vGfytSpE/5jzl5NCUhE1q6Xtg990q02CwBAKRCSAACUVFdjjXz1rq1imroflHDKN0+/RrJtt7yzU+YZAAAAi2MynpRfvHJQYgm/GoOzRzOUg+z37lopN63vLMmnRDNMaf/4oxJascp7zNbNvMP3yWOH5fyvfil2rk0IAAAAFpaz77rw26dk/NB+ZY+m54VZg13LpONTXxbNXPhWbQAAFENIAgBQcmvb6+XRWzc6CQmXU77ZKeM8fQbvlHd2yjw75Z4BAACwsBKptBuQGJtKeI9FTE1MJSBx++YuuW/b8pJ+KvRgUDo/+1UJtHV4jzkH8M5B/LSxvW/LwPO/LdEMAQAAqsvQy8/LyJuveWNbcwISThgiu480m1uk83NfEz0cLuEsAQAgJAEAKBPbV7TIJ25c742dMs5OOedpTplnp9yzU/YZAAAACyOdseTx1w7LwNiU91jI1CSg9EPbuapFPnz9mrIoj2xEa6Tr0W+I2TDd8kNzD+KdA/lpQ398QYZee7lkcwQAAKgGI2+/IQN/+F1+QMKtFpHdMxo1tdL1+W+IWVdfwlkCAJBFJQkAQNm4aX2HW7Z5mlPO2SnrPM0p9+zc1TgRT5ZohgAAAEuXZdvy67eOSs/gmPdY0NAkpAQkNnQ2yGdv2VgWAYlpTkCi8/NfFz0SyT2iuQfytjLH/md/LWP73y3ZHAEAAJay8cMHpe/px72xsw9TAxKaUwHs81+XQEtrCWcJAICPkAQAoKw4ZZud8s3TnLLOTnnnaU7Z58deOeiWgQYAAMD89Y9+7t2Tcqx3yHss4AQk/PbRsry5Rr50x2YxjfI7Sgi2d7qtN7TAdG9r52A+IF7/NhE5/+TPZfLEsZLNEQAAYCmaOn1Seh/7sbOhzN+HTQckDEM6P/1lCXWXtlUbAACq8jvZAABUNeeuRKd8s1PGeZpT3tkp8zzNKf/slIF2ykEDAADg6r16pEf2nurzxqauSdhwjraze7DWurB87e6tEg5OhxDKT3jlGmn/+KNO37bcI5pYSg9sO5ORcz/7gcR7e0o6TwAAgKUicaFPen76fbEz/s1MllJBwvmp7ZHPSmTdxtJNEgCAAghJAADKMijhlHF2yjlPc8o8O+WepzlloJ1y0E5ZaAAAAFy5d0/1ySuHznpjp1BExPQDErWRgHz9nq1SFwmW/TLXbN4ubR/8hP+ApucO6rOsVFLO/ug7khwaKM0EAQAAlojU6Iic/dG3xUrEvccst5KX/7ZTy0Mfltrtu0o0QwAAiiMkAQAoS04ZZ6ecs1PWeZpT7tkp+zzNKQftlIV2ykMDAADg8h3tHZLfvXvSGztFGJxWZ9MBiVDAkK/fvVVa6yIVs7x1190ozfe/b0ZQwin5nJWJTcnZH35b0hPjpZkgAABAhUtPOfupf8rbT1lGfkCi8c77pOHmO0o0QwAALo6QBACgbDnlnJ2yzk55Z4dzWO+UfXbKP09zykI75aEBAABwec46lbnePOr1j9Y0kWhAEz0XkDAMTb58pxNara24pW24/d78Q3knKOG23lDufPzhtyUT9+98BAAAwKVZyaT0/NipzDXoP+bss3T/7aa662+Upnvfy3ICAMoWIQkAQFlzyjo75Z2dMs/TQQmn/LNTBnqaUx763ZN+D20AAABcXP/opDz+6iHJWFb2AScgYfoBCeenbPuzxopt39b80Iek9ppr/Qd1Q2wlKJHod3pof0+sdKo0kwQAAKgwdiYj537+Q4n3+jcs2Ybh7rOmRTdvk9YPfMzdjwEAUK4ISQAAyp5T3tkp8+yUe/aDEppbDnra7/aelANn+ks3SQAAgAoxNB6Tn79yUJLpjPeYE5AwlIPsj+5ZK7tWtUolcw7m2z76aYms2+g9ZrtBCf8QP3bmlJz72Q/ESqdLNEsAAIAKCkg89mOZPHE0f2+l+yHU8Ko10v7xR0VT9lsAAJQjQhIAgIrglHl2yj07ZZ8dzl2OTjlo7yzftuWZPx2Xg2cHSjpPAACAcjY0EZOfvLxfYgm/eoITPjWVgMR7rlkht23skqVAM0zp+NQXJdS93HvMOci3lXLQk8ePundEOgf/AAAAmM22MtL7+E9l4vABZU+l54VPgx2d0vGZr4geyFaDBQCgnBGSAABUDKfcs1P2eboKtBOUqDHzgxJPv31MDvf4PREBAACQNTwRk5+9fGBWQCKglOe6aUOHPLhjxZJaMj0Uls7Pf10CzX5lDKftRl5Q4thhghIAAABFAxI/k/GD+2YEJJwKEtl9pNnY6O63jEiUNQQAVARCEgCAiuKUfXbKP0/TNc0tD60GJX791lE5co6gBAAAwLSRybj89OUDMhlPeo+FZwQktq9olkf2rFuS/aONmlrp/MI3xKiryz2izQpKTBw95JaQpqIEAABAlm1Zcv7JX8j4gb3ekthafkBCj0al8/PfELO+kWUDAFQMQhIAgIrjlH9+766V3tiYEZSwbVt+9eZROdo7VLpJAgAAlIlRNyCxf1ZAIqgEJDZ2Ncjnb90khvLYUhNoanEP8J2D/KJBicMH3FLSzh2TAAAA1cw5Xzv/1GMytu+d/ICEqQQkwmHp+vzXJdjWUcKZAgBw+QhJAAAq0v3bV8gDSinoQkGJp944IsfPE5QAAADVa2wqIT95+YBMxIoHJNZ3NsiX79wiAXPpHxGEOrul6wvfFD0SKRqUcEpJOyWlnTsnAQAAqpFzrtb3q8dkbO/bxQMSoZDbYiO0zL+RCQCASrH0T0AAAEvWA9eskPuvWT4rKJF7rea+oHvyjSNyom+4dJMEAAAokfFYwq0gMRFLFA1IrOuol6/cuUWCpiHVItS1XLq+8GfunY95QQlNCUoc2Cvnn/w5QQkAAFCdAYmnH5fRd94qGpDQgkE3IBFesbqEMwUA4MoRkgAAVCynX/ZDO1bKvduW5QUlapygRI5l2fLE64cJSgAAgOqrIPHSAffnYgGJ1W118tW7tkooUD0BiWmhbico8c38oISZH5RwSksTlAAAAFUZkHj7jYsGJJwWG+GVa0o4UwAArg4hCQBAxQcl3rdrldy1tTs/KBGYHZSg9QYAAKgGo1Nx+cnL+2VsKu49FioQkPj6PduqMiAxzSkN7dwB6ZSKvmhQ4gmn9UamZPMEAABYzBYbFw1IBEzp/OxXJbxqLZ8UAEBFIyQBAFgSQYmHr10td27JD0pEZwUljsjR3qESzRIAAGDhjUzG5Scv7ZdxpYJEyNAkpAQkVrbWytfu3irhKg5ITHNKRDtBCeeOyKJBif3vSu8vfyp2hqAEAABYmmzLkvNP/uLiLTbcgMTXJLJmfQlnCgDA/CAkAQBYMkGJD163Wm7b1OU9Zs6oKOEk4p9644gcOTdYolkCAAAsnOGJmBuQmIgl81psOCGJactbauQb92yTSNA58Ia7RivXSOfnviZaIFA0KDF+cJ+ce+zHBCUAAMCS41TMOv/Ez2Vs79vFAxKmKR2f+YpE1m4o4UwBAJg/hCQAAEsqKPGR3Wvk1o2ds1pvaJoflPjVm0flUM9A6SYKAAAwz4YmYvLTlw/IZDw/IKG22FjeTECimMjqdZcMSkwcPiDnfv5DsdJpvn4BAMCSCUg4FbPG9r9z8YDEp74k0XWbSjhTAADmFyEJAMCSC0p8dM9auX1zV37rDTM/KPHrt47JwbP9pZsoAADAPBkaz1aQuFhAYkVLrXzz3u1SE5oOAWAmp3R0wdYbuhKUOHooF5RIsYAAAKCiOa3Ezv3ix27FrIu12Oj4zJclunFLCWcKAMD8IyQBAFiSQYkPX79G7tzSXTQoIbYtT799XPafvlCyeQIAAFytgbEp+cnL+yWWSBUNSKxqdQIS2yQaosXGXCpKdD36DdFDodwjmthGflBi8thhOffTHxCUAAAAFR6Q+JFbKetiAYnOz35Nous3l3CmAAAsDEISAIAlG5T44HWr5d5tyy4alPjNn47L2yfOl2yeAAAAV+r88Lj89BIBidVtdfKNe7ZJJEhAYq7CK9dIpxOUCIeLByVOHJWeH31HMvE4X8AAAKCiWMmk9Pz0ezJx5KD3mLPPyQtIBIPS+bmvS2TthhLOFACAhUNIAgCwpIMS79u1Su7bvrx4UEJEnt97Ul46eMZtwwEAAFAJTl4YkZ++fEDiybT3WGRGQGJdR70bkAgTkLhs4RWrpesL37xoUGLq9Ek58/1/kPTE+NV9MgEAABZJempKznz/H2Xy+NH8gISRH5Do+vzX3VZkAAAsVYQkAABLPijx3p0r5YEdK/KCEjWB/KDE60d65LfvHBfLIigBAADK24Ez/fLLVw9JOmPlBSQCSkBifWeDfPWurRIKGCWaZeULLVspXV/6c9Gj0aJBiUTfeTn97b+T5NBgyeYJAAAwF6nRETnznb+TeG9P0YCEExDtevSbEl61lkUFACxphCQAAFURlHhwx0p5aOdK7zFdskEJ5b0E2X+6X554/bCkMpnSTBQAAOAS3jx2Tp55+1heBaxoID8gsbHLCUhsISAxD0Jdy6XrC38mRrRmRlDCyHvD4fR3/j7vDQcAAIBykrjQNyvY6exnZgYknJZj4ZWrSzhTAAAWByEJAEDVeM81K+T9166aFZQwlH8NT/QNy8//eFBiSb+3NwAAQKk5oYg/7DslL+w/7T3mVMVy9jKmUh5rc3ejfOXOrRI0qSAxX0Jdy9yKEkZtbX5Qwn1TISszNSlnvvePMnny2Lx9XAAAgPngtAg7/d3/ntcizDZmBCSiUTcYGl7un5sBALCUEZIAAFSVe7ctl0/ctG76NaBooknU1MRU7r7sHRqXH7+0X8ZjidJNFAAAICdjWfL0W8fkreO93po4Wxc37KkEJK5f0yZfvnOLBExe6s+3YEeXdH/lWxJobs27+9JSghJWKik9P/6ujO1/l69dAABQFsYPH5CzP/y2WIm495izf7F1fw9jNjRK95f/QkLL/Fa1AAAsdZycAACqzk3rO+XLd2wWM/cGghOUiJgiAcN/k2F4PCY/enGfDI3HSjhTAABQ7VLpjPzy1UNyqGcgLyDhtNhwqmJNu2trt3zmlg1iqiWyMK8CLa3S9dV/LsHu5f6DTlDCDHhDO5OR3l/+RIZee5nVBwAAJTXy9uty7uc/FDuT9h6zjIC7f5kWbO+U7q9+y/0ZAIBqwukJAKAqbV/RIt+8Z5uEg4YflDA0CSpBiYlY0g1KnBvyyxECAAAslqlESn7y8gE53T/qPeZkIGpmBCQevm61fPC6NaIpVSWwMMzaOun+0p9LZO0G/0FNzwUl/PXvf/bX0v/737htUgAAABaTs/8YfPH30vfrx52B97i7X9H9t4TCq9ZI11f+wq0kAQBAtSEkAQCoWmvb6+Wfv+caaYgGvcfChiZh0z/gTqTS8rM/HpDj54dLNEsAAFCNRqfi8uMX98mFkQnvMac9mNMmzPnPoeuaWz3i7q3LSjjT6qOHwtL5ua9JzTW7ZgQlTBElqDL0xxfk/JO/cKtLAAAALAbbsuTC00/IwAu/U/YpWjYgoflvB0U3b5POR78pRiTKJwYAUJUISQAAqlpXY41864Ed0t4Q8R4L6k77Df+AO5Ox5InXD8v+0xdKNEsAAFBN+kcn5Ucv7JORSb93tFPtymkPNh2QCJi6fOWuLbJ7bXsJZ1q9NNOU9o99XhpuvmNWRQlbCUqM7X1ben76fbGSydJMFAAAVA0rnZJzv/iR22Zjml0gIFG3+ybp+OQXRQ/4LcMAAKg2hCQAAFWvqSbkVpRY1VrrrUXAuVMzoOWVKvzNn47La0d6KJsMAAAWzNmBMfnJS/vdVhvTQk6lK8OvIFETNuXP79suW7qb+EyUkNPepPmhD0nz/e9THxXbDUr4xy2Tx4/ImR/8D0lPTZVkngAAYOnLxONy9offlonDB7zHnP2IPaMlWONd75HWhz8umpFtPwsAQLUiJAEAgPNmQyggf3bfdtm6zH+zwdQ0t+e32t775YNn5Pl9pwhKAACAeXfk3KD8/JUDkkz77RmcNmBOSGJaU60T7twhq1rr+AyUSVCi8Y77pO0jnxLRNSUoYYqt9PyOnzsrZ77zd5IaHSnZXAEAwNKUnhiXM9/97xI7c8p7zNmHOPsRLyChibR+4KPSfO9D7v4FAIBqR0gCAICcoGnIF+/YLHuUstVGLijhnXmLyJ9OnJen3jwq6YzF2gEAgHnxzsk+d39hWbb3mNP+y2kDNq2rKSrfes8Oaa/324ShPNRde4N0fvrLogWcNyMcmtiGE5Tw79JMDg3K6W//nSQu9JVsngAAYGlJDg1k9xf9ffkBCcMPSDhVI9o/8ajU33BbCWcKAEB5ISQBAIDCNHT51M3r5d7ty5V/LLOtN5SbOOXouUF57NWDkkilWT8AAHDFnJZeTqWq59494Qzcx5yb+5y9h9P+a9q6jnr5i/uvkYZokNUuU9FN26TrC38uejSaH5RQylk7d3qe/u5/l6nTJ0s2TwAAsDTEzp2V09/++7xKVc6+wzb8Fht6OCydn/+61G7bVcKZAgBQfghJAAAwg1N28P27VsmHdq+ZHZTQ83uG/+CFfTI8EWMNAQDAZUulM271iNeO9Cj7EJGoqbltv6btWNkiX797m0SC01UKUK7CK1dL95f/QsyGRu8xWzfFcu/mzLISTs/wf5KRt98o0SwBAEClG937Jznz3X+QTGzKeyxbxcrfcxh1ddL1pT+XyNoNJZolAADli5AEAABF3Lm5Wz5320YxciUkNCcoYWoSUEpKjEzE3KDEqX76SwMAgLkbm0rIj17a71an8l6ga+K2+XLafU27ZWOnPHrbJgmYvHyvFMH2Tun+6rfcnz26IZZ7V2eWnclI369/KX3PPOleAwAAzIVtWdL/3DNy/omfiZ3xq5taM9p8BZpb3f1IqMuvlAoAAHycsgAAcBHXrW6Tr961VYIB3QtKhA2RoBKUSKbS8otXDsmbx3rdktkAAAAX0zPoVKPaKwOjk95jTrUqJyDhVK+a9uDOlfLInrWiK203UBmcShJdX/kLt7KER9fFMv3y146RN1+Vsz/6dt5doAAAAIVk4nHp+cn3ZOiVF/Med/cXSkAitGyFdH/tWxJoamEhAQAogpAEAACXsKmrUf7ZfddIbTigBCU0iZjKGxa2LS/sPyW/+dNxSWcs1hQAABS099QF+dkfD0gskfIec8KXTrUq57/cZkM+duM6eeCaFW4bMFQmIxKVzi/8mUQ3b/Mf1HSxAgGxlc/r1KkTcuof/1YS/X2lmSgAACh7yaEBOf1P/1Umjx/xHnP2E5YZdPcX0yLrNkrXF/9MjJraEs0UAIDKQEgCAIA5WNFSK9964Bppq494jwV0zb3jU33v4sCZfvnJy/tlIp5kXQEAgCdjWfLcuyfk2XeOi2X5lafCZjZ8OR2QcNpqfOmOzXLLBqVVAyqWHghIxye/KPV7blEe1cQ2A2Lr/pFManRETv/T38nEkYMlmScAAChfk8ePyqn/8d8kOeS3aXP2Ec5+Qj2Uqt21Wzo/91XRQ+ESzRQAgMpBSAIAgDlqrYvIv3xwh2zubvQec3qGu73DlX9R+4Yn5Ad/2Ct9IxOsLQAAkFgyJb945aC8c9KvFOCcZ0cDmgSVVhpNtSH51gM75JoVlEZeSjTDkNaHPyatH/io23Ij96jYRkBswy+NbaWS0vPT78vgS7+nhRsAAHD3A0OvvSRnf/wdsRJxb0Wc/YNtmH4LL02k+cEPSttHPi2a+zgAALgUQhIAAFyGSNCUr961Ve7a2q38Y5otkR0w/Dc5JuNJ+fGL++Xg2QHWFwCAKjYwNuWGJ88OjHmPOVsGJ2RpKnf+reuol3/14E5Z1lRTopliodXfcJt0feGbokej3mO2bmb7iCsG/vA76X3sx2IlqUwGAEC1stIpOf/kL6T/2afdFq/e44bp7h+mAxJ6OCydn/2aNN56F23aAAC4DIQkAAC4TLquyQevWyOfuWWDmLlghFMiO2Jobslstaz2028dlRcOnBZLeUELAACqw/HzQ/LDF/fJ2FQir12XU0HCCVlOu2Vjp3zjnm1SG85/sxxLT2TNeln2jX8lwc4u/0FNd4MSTl/xaeMH98np7/69pMZGSzNRAABQMumJcTnzvX+Usb1v+w9qWjZYqftVqAKtbdL99X8p0Y1bSjNRAAAqGCEJAACu0O617fLP7r9G6qNB77Fg7o0P5Yxb3jx6Tn756iFJpNKsNQAAVVIa+dXDPfL4a4clnc54j4cMTSKmE63M3fmna/LRG9bKx25YJ6bauwtLWqCpRbq/+i2p2XqN/6CW7Stua/7XQaLvvJz6x7+VqbP/f/buAk6ShLwb/9PjtrPuemu3t+fufodLcAIkQPJyAYIHAi+QQMILJLzwR17cSdDghxxw7rLnsrfu7juz49L/T9XIzuzOrI5uf79Q19XVNd01VTXVvV2/ep51g7OgAMCAq9+8MX3/r9+0oXNa8vkgDUh0+ZxQOm9BTLnhvVE0boKtBADHwLcwAHAcZo4bkZbGnjGuonNaUjo7KaHdpcV4rN22J/7nnmdi97466xsATmBNzS1x06Mr4oGl67tNLy3MpCGJDmXFBfG2a0+Ny+Z3qShAzsgrKo4Jr31zjL76uV2mZiJbkJTQ3n+FaEttTWz46Q9iz+OPDMpyAgADp+qZJ2Pdj76XVpLokHwuSD4fdLTXSIy87OqY9Ia3RH5pqc0DAMdISAIAjtPIsqJ4x3Wnx7knje/yBtsWlCjokpRIAhI/u+eZWLt9j3UOACegpK3Gz+9bHCs27eyclnwUSD4TFHYpMzV5dFm87/lnxtyJIwdpSRkKMplMjL7meTHhtW+MTGFHq5VMZPML0n7jHbItLbH1z7+LrTf/MR0HAE4s2dbW2H7HzbH597+KbMv+KqTJ54Hkc0FHQCJTUBDjX/H6GPvcl0Qmz6kdADge+//VDQAcs8KCvHj9JfNi6ujyuPGxNRHZ5J+wSUntbDS0ZKKxJZvO19jUHL99cGlcdsqMOHv2pPTLcQBg+Nu4syr++MjyqGto6pyWdNAo69JeI3H69DHx+kvmR3Hh/moB5LaKU8+KwjHjYuuPvxPNe9rDtHn50ZrJRF5zcqKk7XPknkcfisad22PKS18V+aVlg7vQAECfaKmvj82/+2XUrFrebfqB7TXyR1TGxNf9XZRMm2nNA0AfEDcEgD6SBB6uXjg1brh6YZQUtZ34SE6KlLT3H++UzcY9i9fGXx5fmZbkBgCGr2w2G0+u2Rq/fuDZbgGJovzMQQGJ554xPd58xQIBCQ5SPHlaTH3rP0XJrNn7Jyb9xwsLI9slVFu7dnVbn/Itm61FABjmGnZsi3U//Ha3gETyvt9aUNQtIFE8bUZMfev7BCQAoA8JSQBAHztlyuh47/POjPGV+3tDFua1td/oWjhi6YYd8dO7n44dVbW2AQAMQw1NzWn1iDueWh2trW1X+ydKCtpCkh0BiaTi1JuuODmed8YMVaToVX55RUx+09tixPkXd5maiWxBYWS7lNRu2rsnPaGy+5EH05AOADC8JO/fe554NA0+Nu7csX96Xl76vt/1y6OKs86LyX/3jiio1KYNAPqSkAQA9IMJlaXx3uedEQumjOqclp9pC0okpbc77N5XFz+75+l4au1WX3IDwDCyZXd1/Piup2Ll5l2d05Lvs8sKM1GUt/+L7dEVxfHu554RZ84YN0hLynCSyS+I8S95dYx78SsiOoMRmcjmF0Y2f3+LlmxLS2y75abY9OufRUt93aAtLwBwdFoa2tprbP3TjZFN22q1Sd7ns/lJd/T2z5GZiDHPe2mMf/nrIq+w0GoGgD4mJAEA/aS0qCDectXCuGrhlC5vvG2lt5MS3B1aWlrj9idXx02PrkivSAUAhvaVf4+u3BQ/v3dxVNc2dE5PQpBJGLKgy5V/cyZWxvued2ZMHV0+SEvLcFV5wWUx+Y1vjbyyss5p2byCtv7kXVq47Fu+JNZ+7+tRt3H9IC0pAHCk6rdsSqtHVD/7dJepSXuNpGrU/oBEXklJTHrDDTHq0qtUIQOAfiIkAQD9KC8vEy8956R43SXzoqA9GJGU3k5KcJcd0H5jxaad8ZO7no6te/bZJgAwBNU2NMWNDy2Nexav61YBqjh5Xy/IpGHIDhfPmxhvvebUqChx5R/HpnT2vLT/eNHESfsnZvKitbAwsl36lDdV7Y11P/5e7HzgHpXJAGAISj437n74gVj3w+9E0+79Vciy7e/ryft7h8Jx42PKP7w3yuafMkhLCwC5QUgCAAbA+bMnxDuuPz0tud2hoIf2G1W19fHze5+JR1du9iU3AAwhG3ZUxU/ueirWbttzUHuNJCSR/C+RhCJfdeGcePWFc6Og65s8HIPC0WNjyg3viYrTz+4yNRPZgoJu7TeitTV23HlLbPz5j6K5tsa6BoAhoqWuNjb96qex7dY/pe2yurXXKOjSXiMiyhacGlNueG8UjZswSEsLALnDNzYAMEBmjhsR73/+WXHGjLFd3ojbrjxNTq50aG3Nxj2L18bvFi2LusYm2wcABlFrNhsPLtsQv3rg2aipb+ycXpCXOai9xoSRpfHe550Zl8zrcuU/HKe8ouIY/6q/iXEvfU1kCpOTKYnM/vYbXfbBmtUrYu13vxa1a1db7wAwyOo2rIs13/t67Fux9JDtNTL5+TH2BS+Lia/7+8gvLR205QWAXCIkAQADqKy4IN50+cnxigtmR36X9hvFPbTfWLN1d/z4zqdi484q2wgABsG++sb49f3PxoNLNyR1kjunFxcc3F4jqRr1vuedGVNGl9tW9LlMJhOV510UU//hfVE4fmKXB/LaT7Ts/3qnuWZfrP/Zf8WOu2+PbOv+K1YBgIFrr7Hz/rti3U++H83VVYdurzFmXFo1auTFV6Tv9wDAwOi4BAEAGCDJP3ovmz85Zo0bEf91z7LYXlXXpf1GRH1zRHNr24mY5IrVX97/bFw0f1qcN29K5PkHMwAMiDXb9sRfHlsZ9V2qOuVlIkoLMpHf5f24qDAvXnX+nDhvtrLI9L+iiZNj6lvfGzv/8OuofvTB9qmZyOYXRjbTEnktzW2TkpMz990ZtevXxOQXvTwKK0faPAAwAJr3VcfmP/w6ates6jY9m1/QHmrc/zmy4oxzYtxLXhV5xSW2DQAMMJUkAGCQTBtTEe97/plx7knjD26/UdDl6oFsNh5Yuj5+88Cz6RWtAED/aWltTdte3fjgkm4BiY72Gl0DEknViH96/lkCEgx8+42XvTYmvOoNkSkq6vJAfltViS77aN36tbH2+0mZ72W2EgD0s5o1K2PN97/ePSCR6Wivkb+/vUZhQYz/q9fE+Fe+QUACAAaJShIAMIhKCvPjDZfOj/mTRsYvFq2KpubWdHpxXiaSFtO1zdnO6t4bdlTFT+56Kp5z1pyYOWGU7QYAfayqtiFuenR5bN29r/v7dUEmipIyEl1cOn9SvPSck6KwwLUHDI6KM86N4qkzYuvPfhCNmze2TczkRTb5ENnSHJnWts+VLXV1sfGXP47R518c46+8Lu17DgD0naS91Y577ohdD9zTrUVbUjkiqSDRtXpE0YRJMeE1b0xvAYDBIyQBAEPABXMmxoy0/cbS2Ly7Np2WXKlaURhR16X9Rl1DU/z2wSVx7twpcdHJ0yK/S/9pAODYrdi8K255YlU0NjUfsr1GSVF+vPaiuXHmjHFWN4OucOz4mHrDe2Lnn2+Mqgfu7r39RkTsXnR/1G1YG5Nf8sooGjVm0JYZAE4kTVV7Y/ONv4i6jeu7TW9NwhFp9Yj9Rpx3UYx9/ssir2slKABgUAhJAMAQMWlkWbz3uWfGbx5dHfct25JOy6TtNyIaWyPqm/dfjfDIik2xYWdVWlVidEXpIC41AAxvjc0taXuNp9du6za9MD8TJflt78UdZo6riL+57OQYW6FvNENHpqAgxr3w5VE6e15s/9WPo7W+fn/7jUwmMklVifarWus3b4q13/9GTLju+VF56pmR6RIAAgCOXDabjX1LF8fWv/w+rdrUOT2ThBUL0upOHfKKi2PcS14dFWecYxUDwBAhJAEAQ0hSsvtVF8yJeRNHxs8eXBH1jS3p9KTEd35aVSIb7UUl0lLgP77rqbj45Olx1uxJkedLbgA4Kuu3741bnlwV1bUN3aYn1SMKD2ivcfXCqfGCM2dEQb4qTgxN5aecHkVv/0Bs+/l/R8P6NQe032iJTGvb58rWhobY8offRPWSxTHpeS+OgooRg7vgADDMNNfWxNY//z72LXu22/Se2msUT5kWE179xigcqwoZAAwlQhIAMASdNXNcTBtTkbbfWL+zrS96Uuq7vDCiviWiqaUtKdHS0ppe/bpyy6647szZqkoAwBFXj1gXT6/d2m16kosoK8xEXpcvtstLCuL1l8yPU6aMtm4Z8gpHj4kpf/+PsfvWP8Weu29tn9p2RWtyZWvX9hs1K5fF6m9/JSZc9zxVJQDgCKtHVC95Jrbd/Idu1SN6a68x8pIrY8z1L0qrPgEAQ4t3ZwAYosaNKIl3Pef0+MMTa+OOxZvSaUnJ79L8iIJMEpbIRnvl5Ni8q1pVCQA4juoRRfmZKD6gvcbciZXxhktPjpFl+kYzfGTyC2LMc14UJSfNje2//FG01Ozb334jLy8yzUn7jdZ0UmtDvaoSAHAEmmv2xda//OHg6hFJ1aa0vcb+z5B5paUx/mWvi/JTTrNuAWCIEpIAgCEsKen90nNOirkTR8aP71setQ1tV/8lJcCTat/J3ab2/huqSgDA4apHrI2n1247qHpE0l4jqdjUKRPx3NOnx3NOmx55B7TdgOGibN6CmPr298f2X/4w6lataJ+aSdtvZFtbeq4qce3zovK0MyOjjRsAHGH1iKQV2/7PiyUzZsWEV/1tFIxShQwAhrJMNnmX70N3LlocS1e3Xe2amD9rSpy9cHZfvgQA5KQ9NQ3x4/uXx/Ite7tNT0ISXatKJPLz8+Lik6fFWbMnR54vuQHIceuS6hFPrIp9dYevHjG6ojhed/G8NKAIJ4Jsa2vaemPP7X+ObEtL10e6VZXoUD5nfkx63oujoGLEgC8rAAzX6hFJ8nbUFdfH6KueE5n87m03AICjd+NtD0VdfWPn/edfflZMnzwu+opKEgAwTIwqL463XXtq3L9ia/z20dXR2NR6mKoS62LF5t1x/VmzY3RF6SAvPQAMvIam5rj32XVHVj0iIi6ZPylefPasKCn0xTYnjkxeXoy+8vooO/nU2P7rn0Tjpg0dj6gqAQC9VY949unYdssfj6h6RNGkyWl7jeIp06xPABgmhCQAYBhJSh9fMm9SLJg8Kn724IpYtrmtqkReZKK0IKKgNbpVldiyuzp+fNdTqkoAkHOOtnrEay+aG/MnjRqEJYWBUTxpSky94d2x557bu1eVyMuP1ry8blUlWhvqY8sffxPVSxfHxOe+KApHVNpMAOSEo64ecfl1MfrqpHqEUy0AMJx45waAYWhMRUm89ZpDVJVoiWhqUVUCgNxztNUjLp0/KV6kegQ5IjmBc7RVJdZ856sx4drnReVpZ6aBXQA4EakeAQC5RUgCAE7EqhL5EQUZVSUAyC29VY8ozs9EUQ/VI/76orkxT/UIctAxVZVY8kxMfN6LVZUA4IRz1NUjrrg+Rl91veoRADCMCUkAQA5WlVi+eVdcf9acGFNROshLDwDHT/UIOPaqEuULTovtv/pxNByuqsSq5apKAHBCUT0CAHKXkAQA5GBVia2798VP7nwqLpg/Nc6ePTkKkjQFAAxDq7bsjjueXqN6BByjoomTY8pRV5V4OiZc/4IoGjXGegdgWGqq2hvbbrkp9i1fcgTVI/Ji1BXXqR4BACeQTDaJS/ahOxctjqWrN3Xenz9rSpy9cHZfvgQAcAjJW/sDaVWJNdHQ1P4ld/KldmS7VZXoMKqiNK48bWbMHD/KegVg2NhTUx93PbM21mzd3W16XiaitCAT+V2/2I6IS+dPihefPSuKC/MHeElh+GjcuvmAqhLtDqgq0VGJYsyFl8aYiy+LvILCgV1QADhGrc3NsXvRfbHz/rsj29TU/bEkHJGXXESy/3Nk0aTJMf7lr4viydOscwAYQDfe9lDU1Td23n/+5WfF9Mnj+uz5VZIAgBOwqsTFSVWJKaPjpw8sP2xViT376uK3DyyJOZPHxBWnzowRpcWD+wsAwCE0tbTEIys2xSMrNkdLa9vV7R2K8zNRlJ98rb3/i+3RFcXx1xfNjXmThAHhyKpKvCf23nt77L7tT4esKpFtaY6d990ZVc88EROufV6Uzz05/RwKAENV0jpq2y1/isbdO7tNVz0CAHKPkAQAnKBGlxfHW6859aCqEoV5mSjIi7SqRGOXqhIrN++KNdv2xAXztOAAYGhWSlq1dXdaPaK6tqHbY0nXqJJ81SOgL2Ty89OS4mUnnxrbf/2TaNi4vuORyBYURjatKpF8rmz7HNm0d09s/NVPo3z23Jhw3QuiaLQWHAAMLcl71bZb/3RQa43kva01P1/1CADIQdptAEAO2F3T0K2qRIeWbDatKtHS/ULcGFleEleeNitmTXDVLQCDb/e+ujQcsXbbnm7Tk4vWk3BEEgDsSvUI6BtJJYmDqkq0PRKZlpbItLYcFLAYfcElMfbiKyKvUAsOAAZXa3NT7H7wvtj5wN2Rbe7eNiqblx/ZJCDRpQJZ0mojCQqOvur6tK0UADB4tNsAAPq0qsSNj62J+sa2L7STfu3lBZloas12a8Gxt6Y+bnxwSZw0aXTagmNkWYmtAMCAa2puiUUrNsWjKzdFa+v+6keJovxMFB/QWiMZvXT+pHjxWbOiuDD50hvok6oSC05rqyqxYV3nH1s2vyCySQuOJCzR2YKjJXbdf3dUP/NkjL/muVEx/xQtOAAYFPtWLIttt/0pmnbv6qG1RvIhMq/b9KLJU2P8y14bxZOnDfCSAgCDQRwSAHJE0iP64nmT4vTpY+N3j62Jh1Zu63ystxYcq7fsjnXb98Z5c6fEuXOmREFSzxwABqC1xsotSWuNNbGvrrHbY8lbUWl+JvKSMhJdzBxXEa84f05MH1th+0AfK5owKabc8J6ofuTB2HXz76K1trbtgeREU0FeWwuOpKpEe+K2qWpvbPrN/0T5SXNiwnXPj6Ix42wTAAZE455dsT1prbFiWS+tNboHafNKSmL0dS+MyvMviUye7zwAIFdotwEAOWrN9qr4xaJVsXFXzRG14KgsS1pwzIyTJo4e2AUFIOdaa9zx9JpYv33vEbXWKC8piBedNSsunDPBFeswAFpqa2L3rTdF1aJ7k64bXWTT9htJZYmDWnCcf3FbC46iItsIgH5rrbHrgXti1wP3RrblCFprRMSIcy6MMc95UeSXC9kCQK612xCSAIAclpQuv2/FlvjjE+uirqH7lwgHtuDoMGtiWwuOUeVacADQt601Hlq+MR5btfmoWmu84IyZUVasSCIMtIZNG2LH738RDevXdn8g29qtBUeHghGVMSFpwXHyQoEmAPq0AlnNiqWx7dY/RdPePUfWWmPKtBj3oldEyfRZtgQA5GhIwjdJAJDD8vIycdn8yXHWjHHx+8fXxoMrt3ZeEdhbC441W3enV/eeO3dynDt3ShSmV2MAwLF/sb1i866465m1UdPlH7+drTUKMpF3wFV/s8aPiFecPzumjXHVHwyW4inTYspb3h37Hnsodv3l99FSs++QLTiaq6ti029/HmWzZseE614QxWO14ADg+DTu3hXbbrkpalYtP7LWGqWlMeb6F8WIcy/SWgMAcpxKEgBAp7U7quOXi1bF+p3tX3IfpgXHiLLiuHzhzJgzabQrAgE4ajuqauOuZ9bEhh1VR9Rao6KkMF589sw4f7bWGjCUtNTVtrXgeOieI2vBkZcXo8+7KMZcfEXkl6hOBsDRaW1siF0P3psO2QPeY3psrZGJNBgx5roXaq0BAMPEjdptAAADKSlx/sDKrfGHx9dG7RG24Jg4uiIuXTAjpo2rtLEAOKy9tfXxwNINsXTjzs6rzA/XWuPykyfH88+YEaVFCiLCUNWweUPs/P0vo37dmoNbcCRhidbuidv85Ireiy6PUeecH3kFhQO7sAAMO63NzbH3iUdi5313RUttzRG11iieOj3GvfiVUTx1xgAvLQBwPIQkAIBBUdPQlAYl7l+xvwVHIhvZg1pwdJgxflRcesr0GD+yfGAXFoBhobahKRYt3xhPrd2ahvK6KsjLRElBHNRa46QJI+KV58+JKaO9t8BwaaGz7/GHY9dfboyWfd2rk0Vra+S1Nh8UjioYURnjLrsqKk87MzIHlEYHgGxra1Qtfip23nN7NO3d032FZDLRmtdDa42yshhz/YtjxLkXqnwJAMPQjf1cScIlOABAj8qLC+PVF86Ni+ZOjF8tWhVrd7R9yZ1c2VuSH1GYF2lYornLSa512/ekw/ypY+Oik6fHqHLlkwGIaGhqjsdWbYlHV22O5ubuJZGTjhrFPbTWGFFaGC85e1ace9J4X2zDMJLJZGLE2edH2YLTYs/tf4q9D94d0fF5MS8vWvMKD2rB0VxdFVtuujF2PXRfjLv8mqiYf4q/ewDS4F3NyuWx465bomH7toPWSFI5ImmvcWBrjcrzL43R1z4/8suEbAGAnmWyySeNPnTnosWxdPWmzvvzZ02JsxfO7suXAAAGWPJx4cGV2+L3j6+JmvruLTias0lliWy0dK+enH6xfdrMCXH+vKlRUVI0sAsMwJDQ3NKaVo1YtHxT1Dc2dXss0xmO6N5aI6mQfMXJU+J5p0+PEq01YNhr2LIpdv7hl1G/ZtUBj2TToEQSmDhQyeSpMf7K66Js5kkDtpwADC21G9bFjjtujrqN6w96LAlGpK01DqhAVjx9Zox70SujeMq0AVxSAKA/aLcBAAypFhw3PbEu7l2+pVsLjkRTEpZoznZeKNihID8vzpo9Oc6dMzmKCxWxAsgFrdlsLNmwIx5YuiH21TUcFI4oys9E0QHhiMSciZXxivNnx+RRrvqDE64Fx5OPxK4/3xgt1dUHPhqZlubItLYeXNnspDkx7orromTS5AFbVgAGV8O2rbH9rlujZuWygx7L5uVFNq+g7QNlF0nFiDHPfXFUnH2BSkQAcIK4UbsNAGAoteB45QVz4pJ5k+IPj6+NxRt3dz5WmMlEQWHSfiOiviXb2Wo6uYr44eUb4+m1W+O8uVPijFmT0uAEACfmidDVW3fHfUvWx67quoMeT8IRxfkHhyMmjCyNF5w5I86YPtYX23CituA487woX3Ba7L3vjthzz+2RbezoLZuJbH5hZPNa29pwdAlL1KxemQ4jFpyatuEoGjN20H4HAPpX057dseOe26Nq8VPJh8oewhHJh8ju3yVkCgti5CVXxchLr4n80lKbCAA4YtptAADHbNW2qvjdY2tizfbuVwRmIxtNrZG24TiwsVdFaVFcOH9anDJtfOQd0H8egOFr486qNByxedeBV4m3V47Ij8g7IBwxqrwonnv6jDh/9oTI954AOaOlZl/sufPmqHro3si2HNBuI9va1oYj272yRCYvL0aecU6MvfTKKKgYMbALDEC/aa6tiV333RV7Hn/4oPeEbCavra3GAeGIyMuLyvMujlFXXh8FlSNtHQA4Ad3Yz5UkhCQAgOO+aviZjbvTyhJb9tR2fyyy0djSFpY40OiK0rh4wfSYM2m0q4YBhrEdVbVpOGLN1v3VhToUtleOODAcUVZcENedOi0umz85CgtUF4Jc1bR7V+y+7abY98TDB7VyawtLNCdfXHWbnCkoiNHnXRxjLrw08ktKBnR5Aeg7LQ31sXvR/bH7ofujtWn/CZBENpNpa6uRd/DnxIrTz47R174gCsf23UkSAGDoEZIAAIaF1tZsPLJme9z05LrYva97//nW9rBEYw9hiYmjKuKSU6bH9HGu/gAYTvbW1scDSzfE0o07DyqJXJDXFo7IP6BfdBKIuHLBlLhm4dQoLSoY4CUGhqqGLZti9y1/jNqlzxzwSDb5kBl5rS0HHWfyS0pjzEWXxqhzL4y8pOcbAMNCa3Nz7HlsUey6/+5oqet+oUVkMtGatNVIwxHdP0eWzlsQY657YRRPmTawCwwADAohCQBgWGluaY17l2+Jm59eHzX1zQeFJRpaIpp6CEtMGTMizps3NWaOH6myBMAQtqu6Lh5ZuSmWbNiRVhPqKj8vojg/EwUHhCOS9koXz50Y1582PUaWFQ3wEgPDRf3aVbHrL7+L+nVreg5LpGXYDzjulJenlSVGnX1e5BerLAEwVLU2NsaeJx6O3YseiObqqoMez+YXRLaHcETxtBkx5voXRenseQO4tADAYBOSAACGpfqmlrjj2Y1x+7Mbo7Gpe0/plmxbWKK59eCwxLiR5XHenCkxd8qYyDvgJBsAg2frnn3x8PJNsTJpq3FAOCIv0xaOKExGDnD2rHHxgjNnxLgRpQO4tMBwlYSv6pYtjl03/z4at2458NHItLZE5oCe9Ym84pIYdc75Mfq8i6KgrHzAlheAQ0uqRex+5MHY8+hD0VJXd9Dj2fz8yCbVIw4IRxSOG59WjihbeIYLKQAgB91420NRV7+/JdfzLz8rpk/uu3ZbmeyBl/4cpzsXLY6lqzd13p8/a0qcvXB2X74EADCM7Ktvir88vT7uW74lWg6oINGchiWy0dI9Q5EaWV4S586ZEgumjYuC5NJkAAZc8s/FDTuqYtGKTbFhx96DHs90hiOSr7W7f7G9YMqoeOFZM2PamIoBXGLgRJFtbY19Tz4au2/9YzTv2X3go2lQIglMHChTUBAjzzgnxlxwSRSOHDVgywtAd01Ve2P3ovtj7xOPRmvT/hMcHZJgRBKQODAckV85MsZc87yoOOv8yKSPAwC56EYhCQDgRLBrX3388Yl18cia7QdWSU7DEo29VJYoLymKs2dPitNmToyiAl+QAAxUOGLllt3x8IpNsW3PvoMe76gcUdBDOGLmuIp40dmzYu7EkTYWcPzHo+bmqFp0X+y58+Zoqdl3xJUlkn72laecFmMuuiyKx02wJQAGSOOuHbHrwXuj6pknI9vD8bm3cEReaWmMuuL6qLzwssgrLLS9ACDH3SgkAQCcSDbtrok/PL42Fm888IrAtjYcSViiqYewRHFhQZx50sQ486RJUVrkCxOA/tDS2hpLN+6MR1Zsit37Di6HnBT2KcrrORwxYWRpvPDMmXH69DFKIgN9rrWhPvbed0fsuef2yDY29hKWSMqTHfw5smLuyTHm4sujdMo0Wwagn9Rv2Ry7Hrg7qpc9e1BrtkO11cgUFsTIS66KkZdeE/ml2rMBAG2EJACAE9KqbVVx0xNrY8XWqoMea42kDUdE0wHtORJJ641TZ0yIc+ZMjhGlxQO0tAAntqaWlnhm3fZ4dOWm2FfX2GM4Iq0ckfTXOMCYiuK4/rTpcf7sCZGflJgA6EdJNYk9d90SVYvujWxT8wGPZiNaWyMvacPRwwm6spknpZUlymbOFuYC6KPqY3Xr18SuB+6JmtUrD54hk2mrHJGXd3A4oqAgRpxzYYy68vooqFSBDADoTkgCADihrdlRHbc8vSGe2bCrx7BEUlmisYewRF5eJk6eOi7OnTslxlS42gTgWNQ3NseTa7bG46u3RH1j00GPF+Rlojg/Ir+HcMTk0WVx7cJpcdbMccIRwKCEJfY+cHdUPXBXtNbX9xiWSKtL9BCWKJk0JcZceGlUzD8lMumJOwCONhxRs2Jp7HzgnqjftOHgx9vDEUnro4PaahQXR+UFl0XlxVdEwYhKKx4A6JGQBACQE7bsqY1bntkQj67dHtmkUnIX2Y6wRGv24IsCM5mYM2l0nDd3SkwcVTGQiwwwbO2rb4zHVm2Op9Zui+bmg3tFF+ZnojgvIq+HcMSs8SPiutOmxcIpo12JDQyJNhxVi+5PW3G0VB9YoSybVpTItCRhiQM+YEZE0ZixaVii8tQzI5OflIAH4FCyLS1R9ezTsevBe6Jxx/ZewhEFyYfIg8IR+eUVMfKSK2PE+ZdqqwEAHJaQBACQU3btq4/bnt0YD65MTty1HhSWaGqNaGjpISwREdPHj4yzT5oUMyaM6vHEHkCu21FVG0+s3hLPbtgera0HH0iL8jNRlFz0d8CX2okFU0al4Yg5E5RDBoae1qam2Pf4oth7z+3RtGvHwTNkW3sNSyRXMo8+76IYefpZkV9aNjALDDCMtNTXR9XTj8fuhx+Ipr17Dno8m8mLbBI2yxxcnadg1OgYedk1aWuNvMLCAVpiAGC4u/G2h6Kufn9L2OdfflZMnzyuz54/k01qY/WhOxctjqWrN3Xenz9rSpy9cHZfvgQAkAOq6xrjrqWb455lm6M+KSPRQ1giacPRwzm+qCwrjtNmToxTZ4yP0iJfwgC5raW1NVZs3hVPrdkam3ZVH/R4kikrzOslHJGJOGvGuLj21KkxbYxqPcDwuMq55pknYs/dt0bjlk09zNDehqP14LBEpqAgRiw4LUafc36UTJ46MAsMMITVb90cex57OKoWPxnZpoNbs2Xz8traavQQjigcPzFGXXFdVJx+VmTyCwZoiQGAE8WNQhIAQC6ra2yOe5dviTuXbIp9dU0HhSWSYhNJG46Wg7/njvy8vJg7ZUycOWti2oojo7oEkEOq6xrSdhrPrNsWdQ0Hf6mdHBLTyhF5SRbigHLI+Zm4YPaEuGbh1Bg3onQAlxqgbyTXBNUtXxJ77r4l6tesOqqwRKJk8pQYddb5MWLhaZFXIHQL5I7W5qaoXrI49j7+cNRtXN/jPIcKRxRPnxmjLr8uyhac6t/gAMAxE5IAAIiIpubWeHDV1rh98cbYta/hoHXSnM1GUnCiuafSEhExbmR5nDFrYpw8ZWwUFug5DZy4JwXXbd8bT67dGqu37kkmHDRPflI5Ij8ThT2EI4oK8+KSeZPiqgVTY2RZ0QAuOUD/qV+3OvbcdUvULl3cw6PZtrBEmrjt4ZhZUhqVp58Vo846L4rGjLWZgBNW057dseeJR2LvE49GS11tj/MkwYi0rUYPrdlK556cVo4omTVHOAIAOG5CEgAAXbS0ZuPxtTvi1sUbYvPug7+4aY22sERTa7anc4NRVFgQp0wbF6fPmhhjKlwdDZwY6hqb4tn129PKEXtr6nucp7C9akR+D1V1yooL4ooFU+Ly+ZPTcYATUcOWTbH37ltj31OP9pCHyEa0JtUlWiOT7bm6RPlJc2LU2edH+Zx5kUmuoAYY5rKtrVGzekXaUqNm1fIeA7bZTFI1Ii8iGXpozVZ+6pkx6rJro3jq9IFbcADghHejdhsAAD1fLb140+645ekNsWZ79cGPH6YVR2LauMo4Y9akOGniqLQ1B8Bws3XPvnhyzdZYtmlntPRwsMtLqkbkZaIwPyKvhyv+RpUXxdWnTI0L50yM4mQmgBzQtGtH7L3n9qh+7KHINjf30oqjrR1HTwpGVMaos8+LkWecEwXlFf2/wAB9rLm2NqqeejQNRzTt3XPULTUy+flRceZ5MfLya6Jo3ATbBwDoc0ISAACHCUus2VEddy/dHE+uT04SHnzlS0s2G01JYKKHxxLlJUVx2swJceqMCVFRorw8MLQ1tbTE8o0748m122Lbnn09zlOQl4mi/LbWGge21EjMGj8iLp0/Kc6aMS4K8oXEgNzUXF0V1Q/fH1WL7ouW6qoe5uioLtESmR6urk5PEs4/Ja0uUTpthvLywJD/t3P95o2x59GHonrJM5FtOTgIlk0qjiUtNXqqGpG0IKqoiBHnXhyV518SBSNHDdCSAwC56EaVJAAAjkx1XWM8sHJr3Ld8S+ypaeyxukRHWKK1h7xEJpOJOZNGp604po2t9EU3MKTs3lcXT6/dFovXb4+GpoOvfM60V40o6qVqRFFhXpw7a3xcOn9yTB1dPkBLDTD0JScKa5c+E1UP3Rt1K5f1MlN7WKK15xJlxeMnpGGJylPPiLyi4v5dYICj0NrYGFWLn4o9jy+Khq1bjrpqRKJk1pyovPCyKD/ltMjka80GAPQ/IQkAgKPU2pqNxRt3xb3Lt8SSTT2XDm1Oqku0RDT1lJZIS9CXxMnTxsXJU8el4wCDIQlDLN+8K5Zu2BEbd/Z0lXNEUgiiKC8TBXk9V42YOLI0rRpx3kkTorTIl9oAh9K4Y1tUL7ovqh99MFrr63uYI7s/LNFDdYm8wqKoOPmUNCxRNuOkyGjpBgxS1Yi6dWuiavGTUb10cbQ2NPQwVyay+e3hiB4+Q+YVF0fFWedF5QWXRdGESQOy3AAAHYQkAACOw47qujQs8dDKbVHbcPCV161JdYmWiMbWbE/fc6cmjq6IBVPHxbwpY6OsuND2APpVc0trrNm2J5Zu3BGrt+5Og189KczPRFFe0lKjhy+18zJx+vQxcdn8yTFngso4AEertbEhap56LPY+eE80bt7YSyuO9sBEtufqEgXlFTHilNPSwETxxMmqlAH9Hoxo2L41qp95MqqefTptKdTjfJkkGJGXfGDsMRxRNGlyVJ5/aVSceW7kFbtgAAAYHEISAAB9oKm5NR5buz0NTKzbsa/HVhzNSSuO1my09Pw9d/rF9swJo+LkqWNj9qTRUZifXHED0Ddfam/cVZ1WjEgqRzT20E4jkZeJKMrPRGEvVSNGlhXFxfMmxUVzJqbjABz/8blhw9q0FUfN049Htrm5l1Ycbe04elM0dlwalqg85fQoHDXaZgH6TFPV3rSdRvXiJ6Nh+7Ze50sqRqThiB5aamTy86P81DOj8oJLozipgtNDCBcAYCAJSQAA9LF1O6vjvuVb4pE1O6I5SUb0Ul0iacXRywXcUVCQH3MmjY4F08bF9LEj06u2AY7WjqratGJEMuyra+xxnuQ76sK8tmBET1UjEvMnj4xL502OU6eNiXzHI4B+0VKzL6ofeyiqFt0Xzbt29lJdojWtLJG24+hF6dTpaWBixIJTI7+0zNYCjv54VF8X1UsWp+006tav7XW+JBSRVI7orWpEwahRbVUjzrkwCipG2BIAwJAhJAEA0E9qGppi0aqkusTm2FHVU8/piJZsNppa2wITvbXjKC0ujJOnjksrTEwYWe6qG+CQqusaYtnGnbFk447YWVXb63xJMKIgP6Ig03PViJKi/LhwzsS4ZN6kmFBZaq0DDGB1ibrlS9LqErXLnkmzEb0GJtpDEz3J5OVF+ey5UXnqmVE+d37kFWjrBvSutbkpalYuT6tG1KxcFtmWlmNqp5EonbcgrRpRNn9heiwCAMi1kERBnz0TAMAwU15cGFedMiWuXDA5lm3ZG/cu2xxPb9yVVEzulFy1nXTVKM5PAhNJWCKiKRnpoq6hKR5ftTkdRlWUxoL2wMTIcv1bgTYNTc2xYvOutGLEhp3VyRm2HldNfl5EURKO6KWdRmLamPK4dP7kOGfWuChKUhQADKikDH3Z/FPSoWn3rqh++L6ofuTBtNJEl7ki0tL2+WmoIpNtaa8ysf/4n21tjX0rlqVDXlFRVMxfGJWnnh5lSal7Jy2BjlDW+jVR9cxTUb10cbQ29BzuzybVxjqqRvTQTiORV1YWI86+IK0cUTi2704wAAAMR5ls8kmrD925aHEsXb2p8/78WVPi7IWz+/IlAAD6TXVdYzy2dkc8vHp7rN/Z9Yvu/bKRjaRLRxKYaO6tH0dETB4zIq0wMXfymCgrdmUg5JrmltZYu31PLN2wI1Zv3RMtvZRez0/aaeS3BSPyeglGjCovinNnjY9zZo2PKaPL+3nJATha2ZbmqFuxNPY98UjULHkqsk3NvczYXl0ifU/o+XNkUvJ+xCmnReXC06N44mRVyiDHJF/XN2zfGtXPPh1VzzwZzdVVvcyZaWunkdd7MCJTUBBlJ58aFWeeF2XzFqT3AQCGA+02AAAGybaqunhk9fZ4ZM322Fnd8xU7rZ2BiWy09NZ6OpOJSaMq4qSJo2L2pNExpqLUl91wgqptaIrVW3fHqq27Y932vdHSy4EhLxNRkJeJovzegxFJO40zZ4yL804aH3MmVDpuAAwTyZXeNYufin1PPhJ1K5f23o4jqTDR2tIemOhZwYjKqJh7clTMnR+lM06KPCc44YSUtM6oXb8m9i1fmrbSaNq7p/d502BEfvrvzB7baWQiSk+aGxVnnBtlC8+M/FJt2QCA4efGfm63oZIEAMARXMmzdue+eHj1tnh87Y6oqW/uNTDR1NIWmDhEgYmoLCtJwxJJaGLKmBGRr5wyDOvjw659dbFqS1swYuueml5baSTfYxe2t9IoSL/UPlh+fiYWThkd5540PhZOGROFycwADFvNVXuj5unH0goTDZs29DJXtr0VR0eFiZ7lFRZF2Ulz0tBE+Zz5UVBW1m/LDfS/lrraqFm1IvatWBo1q5ZHa+P+kwAHStpopBUj0n879vw5smjS5LRiRMXp50TByFH9uOQAAP1PSAIAYIiVz1+6ZU9aYeKpDbuiOSkj0YOWbDZtx5EEJg7V3KyosCBmTRgZJ00cHbMmjIriQuVPYahL2mZs2lWdBiOSqhFVtQ2HnD+pGFGYBCOSSsi9fKk9Z2Jl2k4jqRxRVuw4AHAiaty+NQ1L7Hvi4Wjes/vQgYn20ESvMpkonTo9rTBRPndBFI0Zq+IQDAONu3bGvpVL04oRdRvXp3/vhwpGJKGINBzRy2fIJAyRVIwoP+OcKJ40pR+XHABgYAlJAAAMUfVNLfH0+p3x8OrtaXCil7bSaWAiyVI0Jy05DhGYyGQyMXVsZcyeOCpOmjQ6RpaV9NuyA0enoak51mzbk4Yi1mzbG4299Zo/oGJEfqb3YMSkUWVpxYgkHDG6vNgmAcihKkQNSVn9JDDx9GPRWlt7XBUmEkWjx0b5vJOjYs78KJ02PTJJKX5g0GVbW6N+04a0WkQyNO7ccej5k1BEeziit2BEXklJlJ92VhqOKJk1R0AKADgh3ajdBgDA0Le3tjEeW7sjHlmzLTbsrOl1vqQlR1tgoi00cShjR5SlYYkkNDFxVIUvv2CA7a2pT0MRSRuNjTur05NavUnCEAWdwYiev9BOjCwrinNmjU/DEVNGlfm7Bshx2ZbmqF22JPY9+XDULn0msr2G8LJpO6e0wkQamDjEe1JJaZTPmZe25Ujac+QXC97CQEraZtSsTtpoLIualcvSthq9y0Q2LxPZTH5EXvIZsufPkZn8/Cg7+dSoOOOcKJ2/MPIKC/tt+QEAciEkoY4rAEAfSE58XnXKlHTYurc2HlmzPQ1N7Kiq7zZfXmSiKC/SIflqOw1LZNsCEweef91ZXZsODy/fGKXFhXHSxFExc/yotNpEWbEvxaA/2uls3l0d67fvTYMRu6rrDjl/RygiGZK/7d4k7TNOnz4mrRgxZ8LIyEu/AAeA5MRnQZSfclo6tNbXR82zT6YVJupWrzigDH9SmigT2fy8yCZFIrLt7TjSShPdP0S21NdF1TNPpkNyYrV0+sy2wMSMWVE0boKAHvSxJEibVIioXbc6DUXUrl0d2ZaW3udPArUdFSPScG0vnw3zMlE6a06Un35ulJ96RuSXltl2AAB9JJM91OVQx+DORYtj6epNnffnz5oSZy+c3ZcvAQAwbGyrqounN+yKZzbsilXbq3q96C8bba04OipMHKbIRFplYuq4ypg2tjKmjh0RpUVCE3AsoYgtu/fFhp1700oRSUCi9RB/fMl32GkwIr3tvY1GYnxlaZw2bUycOm10zBpXGfmCEQAchZa6uqhbsSRqlzwdtcsWpwGK3rW35WhvzXEoyUnWJCxROmNWW2hi7HihCTiWUMSunVG3bnXUrlsTtevXREtNzaF/pr2Fxv5gRPTaSqN03oIoX3BalM5dEPll5bYPAJCTbtRuAwDgxLCvvime3bQ7DU0s2bw7GptaD9mWo6k9MNFy6O+6U2Mry9oDE0ITcMhQxJ59sXFnVWzYUZUGJFoO0+M9r1sbjUMEIzIRcyZUxqlTk2DEmJhQWWpDANBnLTnqkxOxSWBiyTPRtGvHoeZury7R0Zbj0JITsElYIg1OTE9CE+OEJuDAv6psNpp272wLRKxbE3Xr1kRzzb7D/+12hCLy8nqvFpFUJxs1OsoWnBZlC05NK0ckFWYAAHLdjUISAAAn5snaFVv3tlWZ2Lgr9tTs76/WU2AiCUq0teY4uC3HoUIT08ZVxpQxKk2Qm44lFJHIT1toZKLwMG00igrz4pTJo9OKEadMHR3l2uAAMBAna7dvbQtMLH0m6tev6bVSWfpANtsWlkjbcxz+Q6TQBPQQili/Npr3VR/+7zOpEJHJS8MRh2yjERHF02dG2cmnpkPRxMnCSQAABxCSAADIgS/hNu6uiWc2JlUmdsaGnYcu1dqazUZze2uOliMJTWQyMW5EaUwbNzKmjR0RU8ZURkmRq5M4MUMRW/ck7TOq0mDE5iQU0XKEoYhMJr09ZLWIiBhVXpRWikiCEXMnjIyC5IcAYJC01OxLwxLpsHxJZJuaDlNlItvWkuNIQxPl5VE2va3SRNnMk6Jw9FgnczlBQxG72gMRbcGIowpFpNUiDh2KyBQWROnsk9sqRsw/JQoqR/bxbwEAcGK5USUJAIDcsqe2IRangYldsXzLnmhuyfZ9aKKyLKaOGRETR5XHhJEVMaqiJPIO0RsXhuKX2TX1TbFt777YtrcmNu3aF5t3V/d5KCIxfWxFZzBiyqgyJ4cAGJJam5qifvXyqGlvy9FSXXWYnzj60ERBeUWUzpgVJZOnRsmkKVEycXLkFRX12e8AA/W30rB1c9Qnw6aNUbt+TTQf9u/l6EIRifyKirZqEQtOi9LZ8/2tAAAMoZCESwgBAIaYUWXFccm8SenQ0NQSSzfvicWbksDE3ti1r+Gg+ZNwQ1Emoii9oD2TBiVaDhWayGZjx96adOhQUJAf4yvLYsLIcsEJhnwgYuuemvS2ruFQV8seeyiipCg/5kwYGQunjk6H5G8SAIa6vMLCKJu/MB2yL85G46YNaWCiftWyqN+wLuKgllOZ9ERvNtqqImWPIDTRXLMvqp99Oh3aniITRWPGRcmkyVE8cbLgBENOa2NjNGzb0haI2LIpHRp37kj/TXTkoYjkbyX5OzlMqDwvE8VTZ0Tp7HlpOKJ42kzhWgCAIUpIAgBgCCsuzI8zZoxNh8SuffWxcltVGphYsW1v7O4hNJGfnAw+mtBE8oV3c0ts3lWdDh0EJxisQMS++sbYvrfmmAIRxxOKmDsxGSpjyqjyyEuvDgSA4SmTSU7WTk+HuPb50drYEPXrVkf96hVRt3plNGzsm9BE8sGycef2dIhnnux48SgaMzZKJk6J4kmT02oTSYgir0jokAEKRLSHIeq3bI7GXUcWiDimUEQm0lBEyUlzo3TW3CiZeVLkFZf0zS8DAEC/EpIAABhGxlSUpMP5syd0hiZWbN0bK7ZWxfKte2NPzZGHJtKhNZt8992jwwUn0mFUeYyuKNWqg+MKRCQhiG3tYYijDUQk32F37ONHF4qobA9FjBSKAOCElwQUyuYuSIdEGppY2x6aWLOiPTSRPYrQRPttT9LgxI62q/UXdwlOjB6TVpoobm/TITjB8QYi6rdtiYZjDESku2rSNiOTObpQxJTpbaGIk+ZGyYzZkVciFAEAMBwJSQAADGNJYOKCZJgzsTM0kYQl2oITSWhif9+2Dh0nlNvvpF94J4GJ1mMNTuTnxfiR5TF2RFmMriiJUeUlaXCisrTY1fjsb5fR0BR79tXH7pq62FNTH7uq644rEJF8j53eHu7L7PZQxOzxbaGIeZOEIgAgDU3MW5AOidaG+rbQxJqVUbd6eTRsWn9koYnkhHQamMgePjixa2c6xOKnugUnkjYdSeWJpG1Hcls4ekzkuxqfLmGIxt1t+05Tug/tiPqtW44vENExHGkoYtacNBRRPGN25JeW2jYAACcAIQkAgBMsNHFhMsyZmJ6Y3rmvoTMwsXJbz6GJ5Kr7gq7fER5tcKKl9aDgRPq8mUyMTAIT5SUxqiK5LU1vkxBFeXGh/rwnoIam5jQAsXtffexpD0Mk47tr6tOAzdE41kBER5uajkoRcyZWxrTRFQI7AHAISYuAsvmnpEOitb4+bc+RVJmoX9Uemjjos2DHyeb9Dx1zcOIABeUVUdglOJGEKQqT8VGjI5Ofb1ueYLItLdG0d3c07toVjbt3dAlE7IzmfdVH/3zHEojoCEVMntZZKUIoAgDgxCUkAQBwgkpCCuNGlKTDRXO7hyZWb6+K9bv2xZa9tUmL6T4PTiSS19uzry4dYmv3x5K2HUl4oq3yRGlnBYpkKC70EXUoS0IxVbUNaQiiLQCRbOO2IMTRVIU4VMuMvKMIRCRGVxTH9DEVMXPciJg7sTKmjq6I/ORJAIBjkrQQODg0sSrq1qyMxo3romHThnRaD+/qxxecaNdcsy8d6tavPWDB8qJw5KgoGp0EKDqGcWn1iYIRlUK4Q1jyb4OWmn1trVjaK0OkYYjdO6Npz+7ItrYe2/MeayAiDQcVR9HkaVE8ta1aRMnMOSpFAADkCN9AAwDkaGgi0dTcGhv31MSGXfvSYd3OYw9OtIUn2oITh6t8m1QV2L63Jh0OVFZcmLbrqCgpivKSwihPbouLoiwZLy5MpxcWuIKwP7S0tkZtQ1PU1Dem7TFq6jvGG2NfXVNaGaKqruGoSht3lbZ7TodMWqj7eAIRyTBtbHl6m+wXAEB/hyYWpkPHCe/mXTvTChPJ0JjeHmNw4sAQxaG0tkbT7l3pULNqefdXKSxMwxOFo0ZHQUVFFFSMiILyEe3jlZFfURH5pWWCFP0VgKiri5Z91W0Bl+S2y5BUiWjatStamxqP/TW6hCCSveaoAxElJW2BiCnJMD2KpkyLwrHj7Q8AADlKSAIAIIcVFuTFrHEj0qHDgcGJ9TtrYvPemiMLTrSPJF98dwQn0iH5TvsIAxTJSfpkOPRy57eHJwq7BCn2Byo6phUJU3RWf+gefuhymwQhGhrTx4+1EsSBOoMQaYWIjvtt+8vRSAIRM9IwRHsoYky5QAQADJHwbeHYcelQcfrZ3YMTmzdEw8Z1Rx2cSJ+j41630MT+ChSHkm1qioZtW9Kh1+XOz4/8svK2AEXn0BaoyC9vD1Yk42XlTp53hh9q06BDGoDoCEFU7w9DdAQjkpYZx61LAKJ7ZYj2feYIHRiISCpFFIwZZ5sCANBJSAIAgCMKTmzaU5O26OgITiQVJ1p76beRnAxPWyd0+y7z4ADF/ioUh27dcaCm5pb9rTwOIWnrUdEenigqzE9DE8lQmJ+XBi3S8eQ2Pxk/YFr7fAX5eYP6hWoScGhK+jQ3t0Zjc0s6JL9/U8sB95PxZN72aclQ2x6AqG9s7vPl6hqE6DYcZRCiw5j2ChECEQBwAgQnTjvr4OBEUnFiY1vliZ6DE+mzdDzZAZUn2v+bhiWSh5Lb1rbx9iDFkUhO5DdXV6XDIX+XvLz20ERSfaI88oqKIq+o+IDbosgUFkV+cXK/OK1kceA8yfMMlqR9RWtjY7Q2NkRrU1Nkk9uO+12mtzY07J/WlExvjJbamrYARBJ+OMY2GL3rCEB0hCKSdhnt2/wYPkcmgYgkDNEWihCIAADgyAhJAABwRMGJmeNGpENPwYkte2pje3VdbKuqjz01DYd8rq4Biv1NEtq+EG09oAJF+pV3e4iiY/xoJG099iRDTW9fxB+BTCaK2kMVPYUn0iXvCAekF7u1TWv77rf9C/50uTsqabT9Ysnv2v7/dFryO3YEHhqb28IOSTiityBKf+ryvXUahEjG2ypBtG23zt/tGJQVF8SEytIYP6I0JowsVSECAHIxOLE7adWxIRo3b4imHdujaee2aNq5I7LNhwp2Hlh9Ir+HAEVH+46OaR2BiqP7PJUEA44kTHE4mYKCg8MVhUWdFRLSIO6hxpPPwB1VNA4xnoYb2gMOHaGHQ6/LftRZDSK90769jr49xkFPm5+ftscoHDc+CsdOiKJJU1SIAADgmAlJAADQZ8GJRHKif0d1fWyrqkuDE9ur6tsDFHVR23DoL2uToEFej9+d7q9CsT840SVE0VmVOds53mfawwvJMNwdKvzQNq19/Bi/vO5QUJAX40eUxIQRpTE+DUSUxISRZelt0hYFAMjx4MSYcekQ7cGJzvDEnt3RtGt7W3BiRxKcaLtt3rPrMMUiem7fkT5v17GOD4lpmKLjQ+P+UEVbVYq+kwQVWpKhtiaGvUOFH9ofP9YAxP7XiCgYOToKx01oC0SkoYgJaTAimT6YlTkAADixCEkAANCnkkoLU0aXp8OBahqaYntVW2Bie3V923gSpKiuj+bmw5fyTU7ed3wv272Vx/45DgpTdAQo2h5om95tvEtFh74OWPRj0GH/eObg6QcFIvom/NB9QSLGVZTEuCQAkQQh2qtDTKwsjZFlRXo+AwBHH54YPSYdYs7J3R5L2kIk1SfS0MT2rWnVibT6xI7taUuII/7w0nlCv+fMRbeqFOlo221ajaJzhq7jbSP7AxfDIeiQjrTf73yg23g6T1+GHw6QX1beVhHigDBEwZhxkVcoUAsAQP8TkgAAYMAkVQTKxxfGrPGV3aYnVw7uqW3srD6xc199VNU2RlV9U+ytbYzq+saobzzySg7dwhTtt4eauycdQYu25Ts4XNF93gNHjmghD3rl7uGHbhfqdf5eA6UgPxMjSotiZGlRVLYPo8qK0hDEuGSoKElbjgAA9LfkxHnRhEnpEKec3u2xlrq6aN61PRq3t1WeaKneGy37qqO5KrmtSseP6jPaAWGKxOF+PNvbva7Bia4Bi/2vdMhnOmi5DjnnAR8uO36HXl6p3ySfv8srIn/EyMivGBEFyW3lyPYwRNJ6ZXwakgAAgMEkJAEAwJC4cnB0eXE6nDx5VI/zNDS1pGGJvXWNaYBib11T2/0kTJFMq2t77GjCFIdcpq6RhEH6jrk/JK0w2oIPhZ3hh8ouYYjkdkRpYZQVFagGAQAMefmlpZE/dUYUT53R4+PZlpa02kQSnmiuroqW9qE5CVAkQYpkPJm2r+oowxS9yfQcVOjhM+QwqD3RPfxQURn5IyqjYETbbXK/oHJk+237tPKKyOT7yhkAgKHNJ1YAAIaF4sL8KC4sjXEjSg85X1NzaxqW6BqgqK5vivqmljRo0dDc5ba5JZ3e2D6tuWV4fFWdyYsoLsiPkmSdFCTrpe22qH1aMiTjFSWFUVlSmLa/6KgKkTyWhFIAAHJBJj8/PZGfDMWHmC/b2toepqhqD1RUp7et9XXR2lAf2cbGaG1siNaGhvQ221DfeT95bLjIFBVFXnFx5BUWRya5LS6JvKKitvGi9vslJd0CEPvDD2mNNgAAGPaEJAAAOKEUFuTFuBEl6XC0mltao7G5tTNIUd8RqGgPUiT3O8IWrdlsOnRUUe64n1wSmE5vbyOSTEpCCXmZiLz2PhrJbdpOI7lNJ2UiLy8TxQV53UIPyW3JAQGIZDxphSHoAADQdzJ5eWmFhGSImHZUP5t+5usWoGi/7TKtLUxRH61NTe293NqGJJzR7X76YTKZ1l59Ii+vrb5ZZv+QLGsquU0+ZxYWRqYwCTi0hRzaghBJ+KG4S/ihODLJfWFZAAAQkgAAgA4F+XnpUFYsSwwAwJFJg69JKKH46EO6AADAwGuPHQMAAAAAAAAAnNiEJAAAAAAAAACAnCAkAQAAAAAAAADkBCEJAAAAAAAAACAnCEkAAAAAAAAAADlBSAIAAAAAAAAAyAlCEgAAAAAAAABAThCSAAAAAAAAAABygpAEAAAAAAAAAJAThCQAAAAAAAAAgJwgJAEAAAAAAAAA5AQhCQAAAAAAAAAgJwhJAAAAAAAAAAA5QUgCAAAAAAAAAMgJQhIAAAAAAAAAQE4QkgAAAAAAAAAAcoKQBAAAAAAAAACQE4QkAAAAAAAAAICcICQBAAAAAAAAAOQEIQkAAAAAAAAAICcISQAAAAAAAAAAOUFIAgAAAAAAAADICUISAAAAAAAAAEBOEJIAAAAAAAAAAHKCkAQAAAAAAAAAkBOEJAAAAAAAAACAnCAkAQAAAAAAAADkBCEJAAAAAAAAACAnCEkAAAAAAAAAADlBSAIAAAAAAAAAyAlCEgAAAAAAAABAThCSAAAAAAAAAABygpAEAAAAAAAAAJAThCQAAAAAAAAAgJwgJAEAAAAAAAAA5AQhCQAAAAAAAAAgJwhJAAAAAAAAAAA5QUgCAAAAAAAAAMgJQhIAAAAAAAAAQE4QkgAAAAAAAAAAcoKQBAAAAAAAAACQE4QkAAAAAAAAAICcICQBAAAAAAAAAOQEIQkAAAAAAAAAICcISQAAAAAAAAAAOUFIAgAAAAAAAADICUISAAAAAAAAAEBOEJIAAAAAAAAAAHKCkAQAAAAAAAAAkBOEJAAAAAAAAACAnCAkAQAAAAAAAADkBCEJAAAAAAAAACAnCEkAAAAAAAAAADlBSAIAAAAAAAAAyAlCEgAAAAAAAABAThCSAAAAAAAAAABygpAEAAAAAAAAAJAThCQAAAAAAAAAgJwgJAEAAAAAAAAA5AQhCQAAAAAAAAAgJwhJAAAAAAAAAAA5QUgCAAAAAAAAAMgJQhIAAAAAAAAAQE4QkgAAAAAAAAAAcoKQBAAAAAAAAACQE4QkAAAAAAAAAICcICQBAAAAAAAAAOQEIQkAAAAAAAAAICcISQAAAAAAAAAAOUFIAgAAAAAAAADICUISAAAAAAAAAEBOEJIAAAAAAAAAAHKCkAQAAAAAAAAAkBOEJAAAAAAAAACAnCAkAQAAAAAAAADkBCEJAAAAAAAAACAnCEkAAAAAAAAAADlBSAIAAAAAAAAAyAlCEgAAAAAAAABAThCSAAAAAAAAAABygpAEAAAAAAAAAJAThCQAAAAAAAAAgJwgJAEAAAAAAAAA5AQhCQAAAAAAAAAgJwhJAAAAAAAAAAA5QUgCAAAAAAAAAMgJQhIAAAAAAAAAQE4QkgAAAAAAAAAAcoKQBAAAAAAAAACQE4QkAAAAAAAAAICcICQBAAAAAAAAAOQEIQkAAAAAAAAAICcISQAAAAAAAAAAOUFIAgAAAAAAAADICUISAAAAAAAAAEBOEJIAAAAAAAAAAHKCkAQAAAAAAAAAkBOEJAAAAAAAAACAnCAkAQAAAAAAAADkBCEJAAAAAAAAACAnCEkAAAAAAAAAADlBSAIAAAAAAAAAyAlCEgAAAAAAAABAThCSAAAAAAAAAABygpAEAAAAAAAAAJAThCQAAAAAAAAAgJwgJAEAAAAAAAAA5AQhCQAAAAAAAAAgJwhJAAAAAAAAAAA5QUgCAAAAAAAAAMgJQhIAAAAAAAAAQE4QkgAAAAAAAAAAcoKQBAAAAAAAAACQE4QkAAAAAAAAAICcICQBAAAAAAAAAOQEIQkAAAAAAAAAICcISQAAAAAAAAAAOUFIAgAAAAAAAADICUISAAAAAAAAAEBOEJIAAAAAAAAAAHKCkAQAAAAAAAAAkBOEJAAAAAAAAACAnNDvIYlsf78AAAAAAAAAAHBCyB4YMshkhnZIIj+/+1O2trb29UsAAAAAAAAAACeg1gMyBgUHZBCGXEiiIK/7U7a0CEkAAAAAAAAAAId3YMYg/4AMwpALSRQVFXS7X9/Q2NcvAQAAAAAAAACcYBqbmqPlgEoSB2YQhlxIorKirNv96pq6vn4JAAAAAAAAAOAEs++AfEEmEzGirHRohyRGHhCSqK1rOCjpAQAAAAAAAADQVXVtfbf7FWWlkZ+fN7xCEtmIqDngFwEAAAAAAAAAOFQliZEVfVtFol9CEkk/kJLiwm7T9lbX9vXLAAAAAAAAAAAnkL37umcLKkd0L9IwJEMSiTEjK7rd37htZ3+8DAAAAAAAAABwAmhpaY3N23d3mzamsnv2YMiGJGZNHd/t/qatu9JfCAAAAAAAAADgQFt27I7m5pZDZg+GbEjipGkTu91vam5JfyEAAAAAAAAAgAOt37yj2/3J40dFWWlxDIuQRHlpcUwaN6rbtJXrtvTHSwEAAAAAAAAAw1hdQ2Ns3LrzkMUZhnRIIjF7+oRu95PeIUnbDQAAAAAAAACADk8sWR3NLa3R1exp3TMHQz4kMX/mlCgtLuo27dHFK6O5pXsPEQAAAAAAAAAgN23ftTfWbtzebdq8mZP7pdVGv4YkiooK4sIz53abVlPXEE88uyay2Wx/vSwAAAAAAAAAMAw0NDbFoqdWdJtWWJh/UNZgWIQkOtIdk8aN6jZtxbrN8fizqwUlAAAAAAAAACCHAxK3P/hUVNfUdZt+/qlzoqykf6pI9HtIIpPJxKXnnJzedrVszaZ47NnV0dqqogQAAAAAAAAA5JK6hsY0ILG3urbb9DEjK2Lh3Gn9+tqZ7AD0vli+dnPc8dAzceArjaosj3NPnRPjRlf29yIAAAAAAAAAAIMoKaSwct3meGrZ2mhqbun2WEVZSbzoqnOjsqJ0+IckDhWUSMyaOiFOnj01RlaUHVR1AgAAAAAAAAAYvlpbW2PLjj1pOGJPVc1Bjw9UQGJAQxKHC0okRpSXxvTJ42L6pHFROaIs8gQmAAAAAAAAAGDYaW5pie0798b6LTti49Zd0djU3ON8SUDixVefm+YFBsKAhiQSm7btjnsfXRK7e0iHdJWXl4ny0pIYUV4SFWWlUV5aHPn5+ZGfn6faBAAAAAAAAAAMtmw2WrPZaGlpTdtn7Kuti3019VFdWxd19Y2H/fG5MybFRWfOi7LS4hgoAx6S6Cil8fTy9fHIM6sO6jPSF/IyEUUFeVHYPhTkZ9KqFElhCs08AAAAAAAAAGC/JDSQJAdaWrPR1NwaTWnooW28PwIFo0eWx6VnL4gpE0bHQBuUkESH2rqGeGLp2li1fmvU1DUc13Pl52WiorQgHUqL8vtsGQEAAAAAAAAgF7W2ZqOmoTn21TVHbX3LcQcmJowdGfNnTo4Fs6dEXl5eDIZBDUl0SBZh2869sWrDtqMOTCRVIsZVFqfhCAAAAAAAAACgfwITe2qaYnd141GFJZJgxOxpE2L2tIlRUV4y6JtmSIQkukoWp76hKfbuq42qfbWxt7o29u6ri/qGxrSPSXNLS1rmI1FaGFFSGGkbja7y8/KickRFlJaWRGlJ21BQkJ8mUTIHzgwAAAAAAAAAOSybzUZLa2s0NjZGXX1D1NXVR21dXVTvqzlo3pbWiNrGiKaW/efn8/PbhoqykhhZURaVI0rbbivKorBgaHWCGHIhiSPR3Nwcjz+zJKqq93VOS8IP48eOiQnjxsTo0aPSDQEAAAAAAAAAHJuGxsbYvnNXbNu+s9v5+cSs6VNj9szpw27VDruQRE8BibGjR8Xck2amlSMAAAAAAAAAgL6TzWZj5649sWL12qhvaBjWQYlhFZJozWbj0Sef6QxIJNUjFsybHRPHjxvsRQMAAAAAAACAE1pLS2ssW7U6tm7b0Tlt7qwZMWPalBguhlVPio2btnQLSJx68lwBCQAAAAAAAAAYAPn5ebFg7uyYNGF857RVa9dHbV39sFn/ecOp18mqdRs678+fMyvGjR0zqMsEAAAAAAAAALkkk8nEyXNPijGjR3Z2hFi+ak0MF8MmJLFqzfpoaWlJx0dWjuiWTAEAAAAAAAAABi4oMW/2rPQ2sXP3nnQYDoZFSCIJR2zdsbPzfteVDQAAAAAAAAAMrNKSkpgxbUrn/c1btw2LTTAsQhJJ4qS1tTUdH1U5IirKywZ7kQAAAAAAAAAgp02ZNKFzfMeuPZ3dIYayYRGS2NalisT4cWMHdVkAAAAAAAAAgIjioqIYWTkiXRVJ4YPh0HJjyIckstls7Nq9t/P+uLGjB3V5AAAAAAAAAIA2E8aN6dIlYv+5/aFqyIckmpqaorm9JEdJSXGaRAEAAAAAAAAABl/liLZKEom6uroY6oZ8SKK2vqFzvLSkZFCXBQAAAAAAAADYr7SkuHO8tr4+hrohH5LomjQpKxWSAAAAAAAAAIChoqCgIAoLC9LxxsamaGnvFDFUDfmQRH1DY+d4SfH+BAoAAAAAAAAAMPhKu3SF6HqOfyga8iGJltbWzvGCgvxBXRYAAAAAAAAAoLv8/P3n8lu7nOMfioZ8SKLrCsxkhvziAgAAAAAAAEBOyctkOseFJI5Xdv9ol/UKAAAAAAAAAAwBmS4n87uc4h+SlGYAAAAAAAAAAHKCkAQAAAAAAAAAkBOEJAAAAAAAAACAnCAkAQAAAAAAAADkBCEJAAAAAAAAACAnCEkAAAAAAAAAADlBSAIAAAAAAAAAyAlCEgAAAAAAAABAThCSAAAAAAAAAABygpAEAAAAAAAAAJAThCQAAAAAAAAAgJwgJAEAAAAAAAAA5AQhCQAAAAAAAAAgJwhJAAAAAAAAAAA5QUgCAAAAAAAAAMgJQhIAAAAAAAAAQE4QkgAAAAAAAAAAcoKQBAAAAAAAAACQE4QkAAAAAAAAAICcICQBAAAAAAAAAOQEIQkAAAAAAAAAICcISQAAAAAAAAAAOUFIAgAAAAAAAADICUISAAAAAAAAAEBOEJIAAAAAAAAAAHKCkAQAAAAAAAAAkBOEJAAAAAAAAACAnCAkAQAAAAAAAADkBCEJAAAAAAAAACAnCEkAAAAAAAAAADlBSAIAAAAAAAAAyAlCEgAAAAAAAABAThCSAAAAAAAAAABygpAEAAAAAAAAAJAThCQAAAAAAAAAgJwgJAEAAMARec0b/yFmLjw/fv7r3x31GrvhnR9IfzYZPvqJ/xyya/wt73h/53IeOMw/69K47Dkvjfd+6GPx9OKl/bKeDvTJ//vF9Ln+6cMfH1bb6boXvzp9jj/+5bY4UbdNrjvWdelYMLQMtb+J4bJ/9BfHOQAAGBhCEgAAAPSrRY8+EX++9Y7O+0uXrRyya3zx0uW9PtbQ2BjrN2yKX934x3jpa98Yf7n1zn5fnlvvvCe9vfaqK4bVdnrOtVeltzffducJu204eo4FnCj7R39xnAMAgIFRMECvAwAAQI5KqiF0tWT5ihiKqqr3xYaNm9LxD7z77fHmN7ym87GmpqbYuHlL/Ph/fh0//Nkvo7m5JT7y7/8R1151WeTn5/fL8qxZuz5WrloTRYWFccWlF8Zw2k7PuebK+Mo3vxe33XlvtLS0HPc6GmrbhmPjWMCJsH/0F8c5AAAYOCpJAAAA0G/+8Odb47EnnkrHr7v68vS2qqo6Nm/ZOuTW+rNLl3WOn7JgXpSXl3UOo0aNjFNPOTk++bEPxVWXX5LOs237jli2YlW/Lc8td9yd3l50wblRUV4ew2k7nXn6wpgwflzs2bs3Hnrk8RNu23D0HAs4UfaP/uI4BwAAA0dIAgAAgH7R1NQcn/n8V9LxSy86P97yptd3PrZkWd9cIZzNZqN6375obW097uda/Oz+E/HzZs/qdb6Lzj+nc3zX7j3R3yGJa69qO2E4nLZTJpOJ66++os9abgy1bcPRcSw4er/87R/iP/+/Lx/RvM3NzfGuD3w0FvVBIGkwDLf9o784zgEAwMARkgAAAKBf/PCnv4g169anJ8w//P53x8nz5nQ+tnT5sfeaT04I/vCnv4yXvOaNMffMi+O0C66OuWdeEi985d/E9/77p+kJt+PpBV9cXBzTpk7pdb7k9+kwetTI6A97q6pj0SOPpeNJ24jhuJ2uv6YtJPGXvghJDKFtw9FzLDg6i5csiw989BPx1W//ID7/5W8ect6knc27PvAv8ds//Dn+7h/fF3v27B12u+hw2z/6i+McAAAMnIIBfC0AAAByRHLF7pe+/p10/K9e9Lw4beHJ6fj4sWNi+85dx3x1cH1DQ9zwzvfHnfc8cNCJwqcXL0mHP992Z3zvq5+P0tKSo3ruZ9tPxM+ZNSPy8nq/puDhR59Ib8eMHhXz586O/nDnPfdHc3NLLJg/N6YfIhQwVLdT4pKLzo/ysrJYv2FT+jzJ73KshtK24eg4Fhy9hQvmxxte84r4wY//J77w1W9FUVFh/OMNbz5ovqQqwvv+98fjD3++Jb3//ne9NW0/M5wMx/2jvzjOAQDAwFFJAgAAgD731W9+P213UFxUFO9/19s6p89rP3G99BhPfH3hK99MT3oVFhTEB9799rj35htjxRP3xwO3/T4+/P53pZUG7n/w4fjMF9pKtx/NFcfLVqxKx+fOOanX+e6578G49c570vGkJHxBQf9ce3DL7XcNSKuN/tpOieQ5r7zs4uOuJjHUtg1Hx7Hg2PzbR94fr33lS9Pxz3zhq/HtH/z4oPYR//zRT8Rvfv+n9P5H//k98cbXvXrY7Z7Dbf/oL45zAAAwsHxjAAAAQJ/avGVrfPeHP03H3/SG18S0qZM7H5s/d07c9+DDsWLVmvSk0NGcyK6pqY0f/Ojn6fhHP/jeeNPr958QnDxpYvzD3/1N2mIhKVP/o5/9Kt79tv91xFdVr1q9NhoaGtLx6dOmpq/VobmlJTZu2hx/+POt8a3v/TC9evu5110VN7z5DdEfkiud77j7/nT8uqsvH3bb6cCWG3/8y61x8213xrve+vfH9BxDadtwdBwLjl3SeuLTH/9wNDQ0xq9/d1N84j8/H4WFBWkQIglIfPjjn46f/+b36bxJCCAJBvWH5G+qrq7+uJ6jrKy0Wyuc4bx/9Nc6cZwDAICBJSQBAABAn/rsl74W9fUNMWrkyINKxHe0QGhsaopVa9YdVUuEe+5/KGrr6mJERXm8/tUv73GeV/7Vi+I/P/+V2LFzV9x+933xshc//4ie+5klyzrHv/LN76VDTxaePC/+9vWvjte+4qU9nvTrC4sefTz2VlXFuLFj4qzTT43htp26uvbKy6KgID+eemZJbNm6LSZNnHDUzzGUtg1Hx7Hg+CStZT73qY9FY2NT2lLjY5/8bBQVFsaSZSvjxz//dTrPu9729/GOfzi4FUdftoB4wSuOL3R0z82/7bFt0HDcP/prnTjOAQDAwNJuAwAAgD6TnDz61Y03pePJibuRlSO6PT5/3pzO8aXLj66M+uL2k+VnnXFaekV1bycVzz37jG7zH+lyH4nV69bH088siYbGxugvt9x+d3p79RWXpr/PcNtOXY0cWRnnn3t2euX7ze0tRI5lWYfKtuHIORb0jfz8/PjiZz4R1199Rfp39KGPfSq+/6OfpY8lFRH+6Z1vHZa75XDdP/qL4xwAAAwslSQAAADoM5/67JfSUuTTp01Jy8IfqOvVwMnV0Edx8W56xW+ia0n2nkybMrnb/Edzgiq5ovmJ+2/p9lhS6n3jpi3xs1/dmFYx+OHPfhlV1dXx/z77yegPt97RFpJITooOx+10oOdcc0Xc/+DDacuNv3ntK4/654fStuHIORb0neRE/1c+/+l4zkteG2vWre8MUX34/e/q913y1FNOjrWLFx3Tz85ceP4Jt3/01zpxnAMAgIGlkgQAAAB94u77Hoy77n0gHX/J858TS5YtjyefXtxtWLtufYysrEznWbrs6K4OTsqnJ8pKSw/b6z1RU1t7xM/9bPuVxKeeMv+gxwoKCmLmjGnxz+95e7zoedel0353082xecvW6Gur16xLS8sXFxXF5ZdcGMNxOx3o+muuTG/vf/CRqN6376h/fqhsG46cY0Hf+/b3f9QZkGhbxw/ETTffNix3y+G8f/QXxzkAABhYKkkAAABw3JIrgpMrgzt85VvfT4dDWXKUJdST10gcrgVFfl5+epttzR7R827bviO2t19JfNrCBYec9+ILzovf/+mWtOx9EmaYPGli9KVb2qtIXHzheZ0n8IbbdjrQ9KlT4pST56VXSt959/3xoudff8Q/O5S2DUfGsaDvfev7P4rPfOGr6fjrX/3yWL12Xdz34MPxzn/6SHzjS5+Ja6+6fNjsnsN5/+gvjnMAADDwVJIAAADguP3qxj8edV/3DRs3x76amkFf+12X+3An4jOZTOd4fn7f/5O6IyRx3dWXn1Db6fpr2lqH/OW2O4fttuHIOBb0rR/8+H/i/3zmC+n4a17x0vjkxz4U3/3q5+OCc8+OpubmeNt7PhR33nP/sNk9h/P+0V8c5wAAYOCpJAEAAMBxqW9oiM996evp+GUXXxA/+s5XDjn/z3/9u3j/R/49veJ/2YpVcc6Zpw/qFujoBZ84beHJh5z3gYcf7bxCec5Js/p0OfZWVcfDjz6ejl975WVxIm2n51xzZXzpa9+J2+++L5qamqOwsGBYbRuOjGNB3/rJL34TH/vkZ9Pxl7/kBfEf//bhNAxUWloS3/v65+Nv3vLOePTxp+KGd30gDU5cetH5Q3pXHe77R39xnAMAgIHn0goAAACOy3f/6yexacvW9OTdB9/7j4edf+7skzrHj7bXfH9Y3H4ivqK8PE6aOaPX+e6694H4/U03p+PXXHlpjB83tk+X446774vm5pZYuGB+TJk8KU6k7XT6qafE5EkToqqqOh5sDzMMp23DkXEs6Du//O0f4sMf/3QaEHjR866Lz37yX7u1j0j+Jn7wjS/FGaedEvX1DfH3//i+eOjhx4bUrvqet78lHSpHjDgh9o/+WCcJxzkAABh4KkkAAABwzHbv2RNf+/YP0vEXPvfaOOO0hYf9mblz9l/lv2TZykFf+4ufbSv9Pn/e7Kitrev2WF19faxesy5+d9PN8aP/+WXa6z45Af/xD79/WLXaGArb6bqrr4j//skv4ubb7kqvIh9O24bhsY8dr6GyvyXtFz7w0U+kr/Hca6+KL37mE5Gfn3/QfJUjKuKH3/pyvPbNb0t/JglK3P3n38SoUSNjKHjvO244ofaPvl4nQ22/AwCAXCIkAQAAR+iZZ5fGr393U9x7/0Oxeeu2qKmpjTFjRsfsWTPi2qsuj1e97MUxsnL/lYHH4v6HHonXvumt6fg9N/82pk+dMijPcbQ+/+Vvxhe++q2DpicndZKTOHNmz4orL7s4Xv/ql8fYMaNjsHUs77Qpk+PeW24c7MUZ1r741W9HVfW+KCjIj/e/621H9DMjKipi4oTxsXXb9liyfMWgl39fvXZdOp6UrV94/pWHnP/sM0+Pz3/6433+d9Xc3Bx33n1/On7dVZefkNspabnRFpK4M/7tI+8fNtuG4bOPHY+htL8l1WTe9PpXx+q16+PLn/tUFBT0/vXdyJGVaduKv37z2+KNr3vVkAlInGj7Ry7sdwAAkEuEJAAA4DDq6urjX/7PZ+IXv/l9Wva6qy1bt6XDfQ8+HF/55vfiXz743rRvOG1aWlpi95698fCjT6TD93/4s/jWlz8b5551Rr+sIuGHgbV23Yb44U9/mY6/5uUvjZNm9d4O4UBzZ89KT3wNdgn1ZctXpvtpT5LS9hXlZTF50sT0quek5P0Vl17UreR9X1n06BOxt6oqvUL4SK6wHo7b6eILzosRFeWxcfOWNHR26iknD4ttw/DZx47HUNvf/vVD74vGxqYoKio87LxjRo+KG//nB1FcVBRD0Ymwf+TKfgcAALlCSAIAAA5hX01N/O0N74pHHnsyvX/2GafFG1//6jj3rNNjxIiK2LxlW9x2573xze/9MHbt3hPv/dDHYsfOXXHDm9+Qs+v15ht/FlMnT+q8QnLV6rXxrR/8KP58yx2xc9fuuOGdH4g7//SrtJ86w9vMGdNixZNt1Q+O1o+/+9Wj/pmk5HwyHEk5855KmvckOfG0dvGiGGy33H5XenvtVZdFJpMZ1tupN4WFBXHV5ZekZeOTlhuHC0kMlW3D4TkW9I8jCUh0GKoBiRNl/+gvjnMAADA4hCQAAOAQPvJv/9EZkPj7N74uPvqBd3e7gm/0qFFpWexXvfzF8bo3vz1WrFodn/rsl2LhyfPisksuzMl1W1paEuXlZel4cpu01zj/3LPihnd9IA1KJCGS3/zuT/GG175i0JZxKJwYga5uveOefmu1MZS84x/+Li46/9yY0h6kAgAAABho6rMBAEAv7r7vwfjN7//UeXX3v37wvb2WOJ44flz84BtfjLLS0rQlxwf/9ZO9lk/OVW/9+7/tHH/4sScGdVlgqLnjpl+mVROuv+bQ/eiHuwXz56YBqWuuvHSwFwUAAADIUSpJAABAL771/R+lt0np+4996J8Ou56mTZ0c/+tNr4svfe07sWHT5rjpL7fFi55//UHz3fvAovS5H3vi6ahvqI9pU6bEC597bbcQweEcz3M8sOiRtDd48rPbd+yM/Pz8GDNmdEyZNDEuvej8dJmTHuB9bc6smZ3jSduNnrS2tsYDix5NWw8kFTxWrV0XtbW1UVFeEfPnzo4XPOea+OtX/VWUlJR0+7n7H3okXvumt3beT9b/zIXnd5vnlX/1wvjcpz6ejn/+y9+ML3z1WzFtyuS495YbD9lHPWkVcs99D8XmrVsjPy8/pk+bEtdeeVlaWSSpkgEAAAAADB9CEgAA0IOamtq45/6H0vGLLjg37ad9JF7z8pemIYnEn265/aCQxFe/9f34z89/pdu0pEXHF7/27bjp5tvin961/0R/b47nOb78je/F//3iwf29azfWxYaNm+KhRx6LvVVV8bH/ffhQyNFKKmx0qBxR0eM8t9x+d7zlne8/aPqevXvTZUuG//n17+KH3/5yvwcUfnXjH+OD//J/orGpqdv0JctWpMMPf/ar+M5XPpe2EgEAAAAAhgchCQAA6MFjTzzV2S7jwnPPPuJ1lFSTSCqXIaeCAACtBElEQVQybNqy9aCWErfeeU9nuOHkeXPiQ+97Z5x5+sLYt68mfvvHP8f/+9p34hP/+YVDPv/xPMeKVWvic//v6+n4JReel1admDv7pCgpKY4tW7fFmrXr4w9/vjVKiov7ZZ9YuWZt53jyuj3JL8hPq1m84DnXxsJT5seEceOirKw0tm7bHrfdeU98579+EouXLIv//fFPxTe/9H87f+6Cc8+KxYvujK9883vxlW99P6ZOnhQ33/izbs9dUHjk//xJqm3804f/La1sMWvG9Pjn9/5j+hpNTU1x+133xWe+8NU0uPHmt70n/vTrn6Tb/XCS/am+viGOR3l52XH9PDA8OX4AAABA3xGSAACAHqzbsLFzfO6co2s9MW/uSWlIYvOWbdHc3BwFBW0fuz/5f7+Y3s6YPjV+/t/fipGVI9L7SUWEd73172P61Cnxng/+6yGf+3ie4+57H0hP+o8bOyZ+8I0vRVFRYedjyc+fesrJ8cLnXddv+8M3vvPf6W1BQX685hUv6XGepI1FMhxozOhRccrJ8+K5110dz/2r18Zfbr0zVq9ZFyfNmpE+nrQMSQIEhYWFnS1SjidQ8LFPfjZdV5MnTYhf/ujb6Trr8PrXvDzOOuPUeNlf/11U76uJ//j//l98+XOfOuxzPvTI491aghyLtYsXHdfPA8OT4wcAAAD0HSEJAADowd6q6s7xyhFtQYQj1XX+PXur0hPsjz3xdKxctSad9p63v6Uz3NDVy178/Pj+D38Wjz/1TI/Pe7zP0dLa2hk46BqQ6Gt1dfVpu5JEfUNDrFqzLr79gx/Fn2+9Iw0zfO5TH48pkycd03PPnT0rTjtlQfr73ffgw50hib70xFPPpO00Eu9++1u6BSQ6JIGS1736ZfG9H/4sbXGS7C89bY8Tzc9+8I3BXoRhYbDX02C//lBm3ViX9jd/Eyc6xzkAADg8IQkAADiMpCrBsc6fzWbT20cef7LzseuuvqLXn33udVf1GpI43udYuGB+ertsxaq0XcQNb3p9jBo1Mvra9S95TY/Tp0+bEt//+hfToMOhNDY2xS9+87v40y23x7NLV8SePXujsanpoPlWdWnf0ZcWPbq/Tcrzr7u61/le+Nzr0pBEc3NL2p7lqssvOeTzXnzBuSpBAMfE8QMAAAD6jpAEAAD0oGtVgKouVSWOxN6qqs7xUSPbQggbNm5Kb8ePG3vIigNzTuo9QHC8z3HJhefFdVdfHrfcfnd85Zvfi29897/izNNPjQvOPSsuPO+cuPjC86KkuDj6y/oNm+LTn/tSfP0Ln4nCwp7/KbJ1+454w9//YxrkOJyk1UV/2Lhpc+d6PlSIZP7c2Z3jmzZv6ZdlAQAAAAD6lpAEAAD0YNrUKZ3jK9pbXBypFSvb5p80cUJnGKC2rj69LSstPeTPlpf1/nhfPMfXPv+faeuLH/3sV7Fh0+Z45LEn0+Fr3/6vqBxREW96w2vinf/w98fVjuOem38b09vXX1IF4slnno3Pfulr8cRTi9OAxue//I345/f+Y48/+94P/msakCgsKEiX5dqrLo+Z06dGeXl5FOTnp/P87T+8Kx5+9IloaWmO/lBTW5felpeVHXK+8vL9j+9rby9yKC0tLVFf33Bcy9b1NYHc4fgBAAAAfUdIAgAAenD2madFXl5etLa2xoMPP3rE62jjpi2xsb2qwHlnn9E5vay0JL2trWs7AX+4E/Q96YvnSMIPb3/Lm9IhCX8seuSxuP+hR+L2u+9LK2Z86WvfiZWr1sZXP//p6AtJJYYrLr0ozj3rjHjJa5LXXB1f/+5/x6te9uI4adaMbvOuWbs+7n1gUTr+iX/55/jrV72sx+esPYJAwvHoCJnU1NYe8XquOILwwkOPPB6vfdNbj2vZ1i5uWz9AbnH8AAAAgL4jJAEAAD0YUVERl150ftx934PxwKJHY92GjTFj2tTDrqv/+dWNnePPve7qgypTbN+xM6qq96VVG3qycnXvVSv64jm6mjt7VjokYYS6uvp4/0f+LX7/p1viD3++JZavvCHmzTkp+kpSAeGTH/tgvOaNb02viP7CV78VX/zMJ7rN8+yy5Z3jL3ze9T0+T1NTc6xauy7607SpkzvXc1IJo7eWG11bgkyd0vYzg2nmwvMHexGAEzzc5DgDw9+JdEwCAIBjJSQBAAC9+Pu//es0JJFUk/jEf3w+vvXlzx5yXW3avCW+9f0fpeNTJk2MFzznms7HkkoKiWw2Gzffdme84qUv7PE5/nLrnb0+f188R29KS0vS6hJJSKIjaNGXIYnEReefG1dedlHcec8D8bubbo73veMfYuaMaZ2PNzY2dY63trT0+Bx/vuX2Q7asKCho+ydOS2vPP38kzj/nrM7xm265Pf76lX/V43w3/eXW9tfMj7POOO2wz3vxBef264kJJz3gxNXfx48jNRSWAQAAAI5X3nE/AwAAnKCuvuLSeOFzr0vH/3LbnfHJ//vFNDDRk23bd8Qb/+HdaYuGTCYTn/63D3eesO9o3zFn9qx0PKmisLeq+qDn+M3v/xSPPfl0r8tzvM+xes26Xpc/sXb9xs7x0b1UTzhe7377W9LbpJrEV7/9/W6PTZ/WVikjccsddx/0szt27opPf+7/HfL5O5Z71+690dzcfEzLeMZpC2PB/Lnp+Be+8q30dQ/07NLl8cOf/jIdf/7118TIyhHH9FoAAAAAwMASkgAAgENIwg5nnr4wHf/m934YL3/9/4ob//iXtP3G7j17YsmyFfGVb34vrn/JazvbL/zze94eV11+yUHP9ZEPvDu9Xbd+Y7zqb94St915b+zavSfWrd8Q/+/r340PfOTfO1tq9OZ4nuPL3/xuXPn8l8f//eLX4p77H4rNW7bG3r1VsWrN2vjBj/8nPvSvn0znmzp5UpxzZlvVir6WVMO4/JIL0/Ff/faP6TJ0OPO0hZ1BiY9/6rPx3z/9RWzYuDkNoPzupr/Ey173d7Fz9+6YdojWFqefuiC9bWhoiC9+7dvpzyZhiWQ4VEDkQP/2kfdHXl5ebNm6LV7x+v8Vf/zLbWlYIlnen/z81/HXb35bNDQ2xoiK8vjg+94RQ81r3vgPaVn8n//6d4ec7y3veH86X0/D/LMujcue89J474c+Fk8vXnpMy3Hdi1+dPley/vrTDe/8QOdyf/QT/9ln6+dwkuBU8jz/9OGPH/HP9NVrD+b6O9H0xd/BUNyuw92xrtO+3J/74xiWC/ub96C+WT8AAED/0W4DAAAOIakQ8JPvfi0+8u//0Val4Ymn4p1PPNXjvKNGjox/+eB74pV/9aIeH7/2ysvig+/9x/jPz38lli5fGW9+23u6PT539knx/ne/Nd767g/2ujzH+xxJuOLL3/huOvRkzOhR8dXP/0cUFvbfPxWSahJJG5PGpqY0ePKx//1P6fT8/Pz4zCf+Jd701vdE9b6a+Oi/dz+5VVRYGP/ff3w8reCwYdPmHp/7zNNPjbPPPD3dTl/62nfSocMr/+qF8blPffyIW4N87lMfiw/+y/+JNevWx9vec/D6HFlZGd/5yudi+mGCLUPZ4qXLe30sCYGs37ApHW7845/ja5//z3jOtVce1fM/59qrYvnK1Wl7mK7tZ/rSokefiD/fekfn/aXLVsZAufXOe9Lba6+6IoarwVx/ufJ3wPDdn/vjGGZ/G7h1caK/BwEAAMdOJQkAADiM8vKy+MJ//nv87uf/FX//t3+dtmJITpAnJ+0njB8XF194Xnz0n98Td/3pV70GJDq8/S1vih9/96tppYnkOUpKimPOSTPjH294c/z2Z99LgxaHc6zP8aH3vTM+/x//Fi9/yQti4cnzYtzYMVFQkB+VlSPi7DNOi/e944a47Q8/j7POOLVf94nzzzkzLr3o/HT8J7/4TezctbvzsUsuPC9+85PvxvOuvzptnZGs4ymTJsbLXvz8+PVPvhcvfv5zDvv8P/j6F+J/vfF1aWuS4uLiY17OZD3dfOPP4g2vfUWcNHNGup7LSkvT7f/2//XGuP2Pv4jzzz0rhquq6n2xYeOmdPwD7357LF50Z+fwxH23xB9/+cN4w2tekT7e3NySBoWSNilH4znXtJ3QSiqeHO3PHk01h66WLF8RA2HN2vWxctWadB+94tK26ijD0WCtv1z6O2D47s99fQyzvw3sujiR34MAAIDjo5IEAAAcodMXLkiH45UEBDpCAge6+IJzY+3iRf3yHOPHjU1P/CdDX3vvO25IhyOVhDx6s3DB/PjGFz/T6+M/+8E3DvncI0dWxr988L3pcLzLO2vm9Pjkv34oTkTPLl3WOX7KgnlpGKirUaNGxic/9qG0ascdd9+Xti5JWsqccvK8I36NpFVNEiRKfvahRx5P982+9Ic/35pWDUlcd/Xlccvtd0dVVXXaFmXypInRn2654+709qILzo2K8vIYjgZz/eXS3wHDd3/u62OY/W1g18WJ/B4EAAAcH5UkAAAActDiZ/efoJo3e1av8110/jmd47t27zmq18hkMnH91W2tKJJy532pqak5PvP5r6TjSWDoLW96fedjS5b1/5W8HSGJa6+6PIajgVh/2Ww2qvfti9bW1sjlvwOG7/7c18ewobC//fK3f4j//P++fETzNjc3x7s+8NFY9Mjj0de8BwEAAINJSAIAACAHdfSCT1qSTJs65ZAnCTskLVCO1vXXtJ1g/EsfhyR++NNfxJp169Pl+/D73x0nz5vT+djS5f3bE35vVXUseuSxdPzaqy6L4ai/1l9yUvWHP/1lvOQ1b4y5Z14cp11wdcw985J44Sv/Jr733z9NT2bn4t8B/as/jwd9eQwb7P1t8ZJl8YGPfiK++u0fxOe//M1Dzpu0p3jXB/4lfvuHP8ff/eP7Ys+evX22HOmyeA8CAAAGkZAEAABADnq2/QTVnFkzIi+v938aPvzoE+ntmNGjYv7c2Uf9OpdcdH6Ul5XF+g2b+qxCQVKd4Etf/046/lcvel6ctvDkGD1qVIwfO2ZAKkncec/90dzcEgvmz43phzjROVT11/qrb2iIv3v7e+Mj//4f8cRTi9N11HGy9enFS+Ljn/5c/M0N74y6uvrItb8D+k9/Hw/68hg22Ptb0s7qDa95RTr+ha9+K77yze/1OF9S/eV9//vj8Yc/35Lef/+73pq2v+hL3oMAAIDBJCQBAACQY5Kr/ZPe7om5c07qdb577nswbr3znnQ8KV9fUFBw1K9VXFQUV152cZ9Wk/jqN7+flqBPnvv973pb5/R57ScTl/ZzSOKW2+8a1q02+mv9feEr34w773kgCgsK4gPvfnvce/ONseKJ++OB234fH37/u9Kr5+9/8OH4zBfa2iLk0t8B/ae/jwd9dQwbKvvbv33k/fHaV740Hf/MF74a3/7Bjw9qk/PPH/1E/Ob3f0rvf/Sf3xNvfN2r+3QZvAcBAACDTUgCAAAgx6xavTYaGhrS8enTpkZNTW3nkLSSSEqy/98vfi3+7u3vS68ofu51V8UNb37DcZerv7kPQhKbt2yN7/7wp+n4m97wmpg2dXLnY/PntpXYX7FqTXoSrj8kVRHuuPv+dPy6q/s+JJGs767b41iG5CTnQK+/5HV/8KOfp+Mf/eB74x3/8Ob0uQsLC2LypInxD3/3N/F//uWf08d/9LNfHVXp/v5aJwP9d0DfG6jjQV8cw4bK/pa08vj0xz8cL3vx89P7n/jPz8cPfvw/6Xjyd/Lhj386fv6b36f3k7BTEtToa96DAACAwebyBwAAgBzzzJJlneNJufXeSq4vPHle/O3rXx2vfcVL0xNrx+raKy+LgoL8eOqZJbFl67aYNHHCMT/XZ7/0taivb4hRI0fGP97w5m6PdZSlb2xqilVr1vVLW4RFjz4ee6uqYtzYMXHW6af2+fMnJehf8IrjOzF6z82/7bUNSH+tv3vufyhq6+piREV5vP7VL+9xnlf+1YviPz//ldixc1fcfvd9nSdpB2udDPTfAX1voI4HfXEMG0r7W9Lq43Of+lg0NjalLTU+9snPRlFhYSxZtjJ+/PNfp/O8621/n4ad+oP3IAAAYLCpJAEAAJBjOnrBH87qdevj6WeWRENj43G93siRlXH+uWenVynf3N6q4liX+1c33pSOJyfvRlaO6Pb4/HltV44nli7vn5Ybt9x+d3p79RWXpicah5P+XH/JVfCJs844La0e0ZNkfZ179hnd5s+lvwP61kAeD/riGDbU9rf8/Pz44mc+EddffUX6e33oY5+K7//oZ+ljSeWXf3rnW/vttb0HAQAAg00lCQAAgBzTcYIqufr6iftv6fZYUpZ+46Yt8bNf3Zhe6fzDn/0yqqqr4/999pPH9ZrPueaKuP/Bh9Ny9X/z2lce03N86rNfSsvQT582Jd74ulcf9HjXK8WTK6KPsFDBUbn1jraQRHJisT+cesrJsXbxomP62ZkLzx+09ZdUh0h0bXfQk2lTJnebfzDXyWD8HdB3Bvp4cLzHsKG4vyWBpq98/tPxnJe8NtasW98ZAPvw+9/Vr6/rPQgAABhsw+uyFwAAAI7bs+1X8Z96yvyDHisoKIiZM6bFP7/n7fGi512XTvvdTTfH5i1bj+s1r7/myvT2/gcfiep9+4765+++78G4694H0vGXPP85sWTZ8njy6cXdhrXr1sfIysp0nqXL+r6SxOo169Ky/cVFRXH5JRfGcNLf6y9ptZEoKy095HxlZW2P19TWRi7+HdA3BuN4cLzHsKG6v337+z/qDEgk7r7vgbjp5tv69TW9BwEAAINNJQkAAIAcsm37jtjefhX/aQsXHHLeiy84L37/p1vSUuxJOGDypInH/LrTp06JU06el15BfOfd98eLnn/9Ef9scrV4ctV4h6986/vpcChL+qHdxi3tVSQuvvC8zpP9w8FArL/kNRKHa0GSn5ef3mZbs5GLfwccv8E6HhzPMWyo7m/f+v6P4jNf+Go6/vpXvzxWr10X9z34cLzznz4S3/jSZ+Laqy7v89f0HgQAAAwFKkkAAADkkMXtVzMfycm6TCbTOZ6ff/z/fLz+mrYWFX+57c6j+rlf3fjHbst9JDZs3Bz7amqiP0IS113d9ycO+9NQWX9DyWD+HTB89+djPYYNxf3tBz/+n/g/n/lCOv6aV7w0PvmxD8V3v/r5uODcs6OpuTne9p4PxZ333N/nr+s9CAAAGAr86x4AACCHdPSCT5y28ORDzvvAw492VgeYc9Ks437t57SXq7/97vuiqan5iH6mvqEhPvelr6fjl118QaxdvOiQw2c/+a/pvMlV2MtWrIq+sreqOh5+9PF0/NorL4vhYqisv6FmMP8OGL7787Ecw4bi/vaTX/wmPvbJz6bjL3/JC+I//u3DaTijtLQkvvf1z8c5Z50eDY2NccO7PhD3PrCoT1/bexAAADAUCEkAAADkkMXtJ+sqysvjpJkzep3vrnsfiN/fdHM6fs2Vl8b4cWOP+7VPP/WUmDxpQlRVVceD7ScCD+e7//WT2LRla3oC74Pv/cfDzj939kmd40uX9V3LjTvuvi+am1ti4YL5MWXypBiK3vP2t6RD5YgRQ279DaV1Mth/Bxy7wd6fj+UYNtT2t1/+9g/x4Y9/Og2OvOh516VBkq5tcpJl/ME3vhRnnHZK1Nc3xN//4/vioYcf67PX9x4EAAAMBQWDvQAAAAAMnMXPtpV9nz9vdtTW1nV7rK6+PlavWRe/u+nm+NH//DJaW1vTk3Qf//D7++z1r7v6ivjvn/wibr7trvRK8EPZvWdPfO3bP0jHX/jca+OM0xYe9vnnztl/5fWSZSsjl1ptvPcdNwzZ9TdU1slQ+Tvg6A2V/flojmFDbX9LWl184KOfSF/juddeFV/8zCciPz//oPkqR1TED7/15Xjtm9+W/kwSlLj7z7+JUaNGHv8yeA8CAACGACEJAACAHCpVv3rtunT80cefioXnt5WO783ZZ54en//0x2P61Cl9tgxJufq2E4x3xr995NAnAb/41W9HVfW+KCjIj/e/621H9PwjKipi4oTxsXXb9liyvG8qITQ3N8edd9+fjl931dANSQzV9TfUDIW/A4bv/nw0x7Chtr8llXDe9PpXx+q16+PLn/tUFBT0/rXgyJGV8aPvfCX++s1vize+7lV9EpAYCutiOL4HAQAAfU9IAgAAIEcsW74yWlpaenwsKbdeUV4WkydNTK/QTsqwX3HpRd3KsPeFiy84L0ZUlMfGzVvimWeXxqmnnNzjfGvXbYgf/vSX6fhrXv7SOGlW7yXqDzR39qz0BFVftYtY9OgTsbeqKr26+0iuXh8KhtL6G2qGwt8Bw3d/PtJj2FDd3/71Q++LxsamKCoqPOy8Y0aPihv/5wdRXFTUJ689FNbFcHwPAgAA+p6QBAAAQI5ITjytXbxoUJehsLAgrrr8krS0fFKuvrcTVDNnTIsVT7ZVbzhaP/7uV6Mv3XL7XenttVddFplMJoaDgV5/Sdn+ZDiS9he9tcDIpb8Djs5QOh4c6TFsKO9vRxKQ6NBXAYmhsi6G43sQAADQ94QkAAAAGFDv+Ie/i4vOPzemTJ40LNb8rXfcM+xabQD9Z7gdw+jO9gMAAIQkAAAAGFAL5s9Nh+HijpvaSq4DDMdjGN3ZfgAAgKaaAAAAAAAAAEBOEJIAAAAAAAAAAHKCkAQAAAAAAAAAkBOEJAAAAAAAAACAnJDJZrPZGMKWrVwTGzZvScdPmT8nJo4fN9iLBAAAAAAAAAC0e/rZZbFj1+50/JwzTo1RlSNiqFJJAgAAAAAAAADICUISAAAAAAAAAEBOEJIAAAAAAAAAAHKCkAQAAAAAAAAAkBOEJAAAAAAAAACAnCAkAQAAAAAAAADkBCEJAAAAAAAAACAnCEkAAAAAAAAAADlBSAIAAAAAAAAAyAlCEgAAAAAAAABAThCSAAAAAAAAAABygpAEAAAAAAAAAJAThCQAAAAAAAAAgJwgJAEAAAAAAAAA5AQhCQAAAAAAAAAgJwhJAAAAAAAAAAA5QUgCAAAAAAAAAMgJQhIAAAAAAAAAQE4QkgAAAAAAAAAAcoKQBAAAAAAAAACQE4QkAAAAAAAAAICcICQBAAAAAAAAAOQEIQkAAAAAAAAAICcISQAAAAAAAAAAOUFIAgAAAAAAAADICUISAAAAAAAAAEBOEJIAAAAAAAAAAHKCkAQAAAAAAAAAkBOEJAAAAAAAAACAnCAkAQAAAAAAAADkBCEJAAAAAAAAACAnCEkAAAAAAAAAADlBSAIAAAAAAAAAyAlCEgAAAAAAAABAThCSAAAAAAAAAABygpAEAAAAAAAAAJAThCQAAAAAAAAAgJwgJAEAAAAAAAAA5AQhCQAAAAAAAAAgJwhJAAAAAAAAAAA5QUgCAAAAgP+/vbsAj/M68/5/m0EGmZmZHTNDbIeZHKamTSFtd7ftbv/dvvv23d1uedOmSdqG0yQNO4kTJ3bixMzMMjMzs/2/fkc642dGM6OZkRRLmu/nunRpbGmeeeA8oHPf5z4GAAAAAEA6IEkCAAAAAAAAAAAAAACkBZIkAAAAAAAAAAAAAABAWiBJAgAAAAAAAAAAAAAApAWSJAAAAAAAAAAAAAAAQFogSQIAAAAAAAAAAAAAAKQFkiQAAAAAAAAAAAAAAEBaIEkCAAAAAAAAAAAAAACkBZIkAAAAAAAAAAAAAABAWiBJAgAAAAAAAAAAAAAApAWSJAAAAAAAAAAAAAAAQFogSQIAAAAAAAAAAAAAAKQFkiQAAAAAAAAAAAAAAEBaIEkCAAAAAAAAAAAAAACkBZIkAAAAAAAAAAAAAABAWih7uVcAAAAARcdXM2bY9DlzrX/PnjZq6JCE33fu3Dn79dPP2IULF+zfnvieVShf3ooardvcRYtt2apVtu/gQTt79qz7/3/65jetWtUqlg72Hzxoz7z8ilXJqGz/8vjjVlQF29NPv/+ElS9X7nKvEoqwhcuW2SdfTLQObdrYnTfecLlXB8WwHTz53PN29Ngx+/aDD1jd2rWtKFq8YoXbxr379tvpM2fc/z08+i5r2qiRpQPdF37156fd658+8T0rV4TvC749fffhh6x2zZqXe3WAr0Vx+FsAAAAACCJJAgAAACG79+513+vXrZPUXtmzf7/rFK2ZmVlkO0U/+2qSLVi61EqXLm11atVygXcFWdIlQSJ4fOvVqWtF2d5AeyJBouAoSejEyZPWs2sXq1qlSrFZdl52792X0nXr4sWLtnrdeluWtcp27N5jx48fd9eHKhkZllm9mjVr3MTatWqZK2iuRCMlHMmoIUOsf6+eUZe/cOky+2TiRKtft6596/77wn72+vtjbMPmzbneU7FCBff7fa7obu1bt7aiYtzEibZg6TK7fuQI69m1qxVFqbYDtVsFtMuWKVNkA9oz5s6zL6dPd691/1I7kXp1ktvW4mz3vn3unNUxKsoJEidPnnLtqUyZMu4eBkTasn27bdi8xRo1qG9tWrQoNssuzL8F9h044J7RN23dZoeOHHEJF5UrVbKqGRnWqEEDa9GkibVr3cpKlSoVes/0uXPtq+kz3Ou6tWrZtx96MG6Cla4fjz9wf9h1c3lWlo359LNc79H9oEZmdWvfuo0N7NObZ1EAAIASiiQJAAAA5AoyJRt4uRR8L5oBmwMHD7nOVwVWHrv3HhdkSke79uQkwRTR4+QdOXbMatWoYS2aNrncq1JinD9/3r6YOtV979ezR7FZdmFdfxTEfO+TcbZ1xw73bwWdFXy9qOvFoUPuS4Gmk6dO2lVDh4bed+bMGfczL2vduphJErv27ol5vu3asycU2KmQE/BWdRtVudm0dav70udejv0Z6fDRo7Z4xUqrXrWqde/UyYqqVO9D/n1KhlGSTFGjqhFT58xxwcH7b789ba+Lu/cU7ecM7+CRw+7+VatmjSLZnnD5LVq23JasXGlXDxtm1qL4LLuwrsGTZ860aXPmuiQGJRfpXqN78tHjx23H7t3uS8kM7du0jnof9QkaetavWSMz6t82ftmRiXA7c5aRUblyKKlJv3vw8GHbu/+A7d0/x9Zv2mSP3D3avR8AAAAlC0kSAAAAcE6eOmVHjh5NaTRtqhUovi5Z69e57+1btUrbBAnZVcSPk9euVSv3hYIdha0khhrVq4dGoReHZSf6+VI/wQopZ86etb+/+56rBqGpCq4cNDBsygKNhFXyxJIVK61xgwa5ziEFUJRwVaZ0adu2c6cdO37cVZ+ItNMnJdUNX68jR4+56gVy1003hQV1jp84YW99NNa279xpk2bOdJU5LveoeU3BpOOr0bRFOUiUbDvIb3Lg10UBOiXQNGnYMG0TJMLuX0X0OHkN69Wz7z3y8OVeDRRhO3fvLrRnscJcdmH8LeCn+atUsaINHzjAJeKVLXupq1pVJVasXm37D2RXb4qW+FutShWXXKukxQG9e8VMWKxbu1aue5hfxhVdOtuVAweG/l/3eVWqmDRjZnaSxurV1q1jx4S3CwAAAMUDae0AAADI92jaUJnzIjqNw/adu9z3po3TY+72WHbnjJiLDNqi5CupQZlDhw/b6dOnXVnuRKfOmb1ggUuQUMLUA3fcHpYgIbr+NWvc2G66+irr2LZt1IBKvdq1rW3LltlTdqxfn+sz9P97YgSMfMCmfPnyrpx3kEaz+kCNAuP+2nq5KHFu8YoVVq1qVbuic2crqlJpB7kDe0Xzusj9K6IySxE9TkAizp49Z3sPHCiUhJ/CXHZh/C2gag2aSkhG33yT9erWLSxBQjKrVbOBvXu7+3FkhR1f1WlQ3z7uu5IkovEVJ6Kt16Wfhe8vVe4Z3LevVa9Wzf17W07VKQAAAJQsVJIAAAAohjS/7sJly2356iw3ukojo2tmVrfO7TvYgF49Y472VRBp1oIFtnLNWhdUqly5snXp0N6G9uuX5wiwjVu22uyFC2zbjp1uVHHD+vXdqC+Nbo31Xv2e1nPlmjW2Z98+16mpkeZVq1SxZo0buQ7RZKtWrF633hYtX+4CsydOnXJBxbYtW9iQfv1yjeb+699fc5/rjZv4pfuSHl262A2jRib0mQqozl+8xNZt2uSChqXLlLFG9evZgN69rWXTpmG/O2bcp27EmUazjRw8OOryTp06ZX9+6WVXveOb991rDerVC/u55mReuGyZm1taI8vLlyvnArlD+vXN9bvBeZk16nzE4ME2c/58W6VjfOSIZVSqZP/0rW+65ah0sUalR86TrjnUV65d46YX0P46duKEa2NVq2RY6+bat32jjpR/9+OPbdXadXbjVaNceeS5ixe7gJ62q0rlytaxXVsbPmBAUiPhFVz+9Z+ftrPnztm/PP6tsM/9+PMv3LEfNWSIXdG5k81asNCy1q21g4ezt1NBa1UF8NMXJEoj+1dp+7dsceeTKgOcv3DBdc6rvPOgPn2izq/95xdfcp38j913rztGs+YvsA2bN9vxkyfde3t27Wp9rugeNoe2LF25yj4cP97atGxho2+6yW2TymPv3bffnbuNGzZw26iS7fk9/9ds2GBvffhR6N86Xv/5v0+G/q02c/3IkSnth2SW7Ss0qGT2sqzV7vw9dfq0O76xzt/g9s5bvMSWrVrlpqOoWKG8tWvV2kYMGngpKJNEgkbW2uxAihIgkq2MEAqo1K3rRvUvXbXKstatd8c6cn51teFowRe/jHp1audqG1K9WtXQ63Pnz4f9TNcEXYu25lSwUDULXQO1Ph3btrFO7dpZQZo+d567jg8qwCoSOse135auXGl79u137UAjgTu0aWOD+/aJef7mpx2opPrMefPctVX3ISUD6rqma0as+5fWU+1V56uqGOi6pvavdtqkYQPr3rlzrkojedE0KguWLnMBN11nK1WsYC2aNLWh/fvnKhOvc0vnmKdgog8otmzWzO6//baEp5aZt2SJrVm33t0TfNUMXZsi28uX06e7z+jUrq3dfv31UZen8/gvr/7d3RfvuvHGXCXwta/VRrWtGt2toKfulwp0toi4X4r28ZhPP7NWzZq5QOmcRYtsedZqt3x91k+f+J5re3tiHGMlE61au9bdv3bu2W3Hjp9wx1j3oOZNm7hrS+Q9Tz6fMtUlTCkQ2q5VS3c/2ap77smT7jlFbWPE4EEu8SYZz7z8ilv3R++5O6x9TJ09x00r0Lt7dxs1ZLDNXbTYPSvod9WuWjRpYqOGDol5HYzltffec89nd95wgzVq0MBmLZhva9ZvcMddz1qd27d351Vk8DmV5xt/DXrl7XfcVEHffuhBN8pfzytKINM5o2vRyMGD3DNisnS8FejWfVTPEnpm0fVBVQaaNmro7kHRnoFS3bf6vF/9+Wl3jfvxd75tBw8dtjkLF9qmbdvc83LNGjVsQK9e7jm5IM7p3z37F3cd8X7zzLOh1zo+/9/3n3D3hFT2Q6LLTvU8zc/fArGsWb/etZnM6tVyJSvmxd9H9eylBD5VfFBlJ9/uE6nqpKmk/D6LlXylpLvDR47kuhfrmVrXqnUbNrpkDT0rVa5Y0TIzq7vjr6myLkdlLQAAACSHJAkAAIBiRsG3dz/+xPbuzw6o1srMdJ13+w4ctEkzZrhOVQVPIgNaet8/xnzggiR+Xt6TJ0+6gIiCD5UrZwcC6kUZafXltOk2Y152cEbBLAXE1SGskvU3XXWV67xVICHYManA++tj3redu/e4jlmV4teXAnsKxKsztVXz5gknSSgQ/MGnn4VGbSvoUbtSpewO/iVLXfLEI/fc7QK6ntZJHbg7dmVXklAnrte8SWJlyxcuXWbjJ092ATp1UGt91bGqgIy+rh85IixAWrdObbPVq93xiRd4VMesghfBjm51OH/8xRcuKCdVMiq70e4HDx1y260gxj233pIrcOFHt5ctU9b++urfXYd6nZo1XZBdnc+RI+AjA7NLVq20zydPce2iakaGa1N+lN78JUts3caN9q3777OKFStG/VyVg1fiTbmyZa1WzvFUp/vsBQvdnM733XarJcoHl7XtkQENP5JYnerPvPKqC2Jo/6hNal0VCDx24rjdeeONloxZ8+e7zm4FCbT97tw4dcq1LZ0fGzdvcQGvYIUV7R8lSOj/1L4mTJ7iOuvVxrPPxwM2YfJkt68VjAzfjuz9Vr1qNXv5rbdt+65d7jPVdjUKVAEuBX2/89CDbpn5Of9Pnz7j2r0+U8FEBYeDiQ7B8yDZ/ZDMspWA8c7Ysa5stfaJtlVBzP2ujS21NRs22jfuuTtXcEPXC1239Bl6n463PmvB0qVuSorWLVokPVWCzg/RdStZl5Ik6ljr5s3dvlKQScsKBkT8uaFzUBUjgnRNjDfaVoFKr3qgKsLkmbNs6uzZ7rUSIxSk1HVRv68gsfZPQSZJaLlK4CnIKhI6nu9+8olt3b7Dra/2T+VKFW3/wUMuuWvtxg32yOjRua41+WkHSqwYP2mSu25ov6ld79u/3yUh3HLNNaGR17o2errevz32Y3dtE7XX6nXquAC6zgedfwqqJZokoaDnZ1995YKponuotuHAwYO2LCvLJUOoqkkwsKx2o/NLgT+tu6Zx8Oe12l4idN/4aPwE1z51biroq+CeppPRl64zSizz6tXO3n9K2IpFAXHtAyVzRSZI+CQLf//VNirAqHulgqsaDR5Zst6fK7rev/CPN93zgdpF7Ro13LHWOabPU1vX7+gYBq3fvNk+HD/BXZf0c12Dz547awcOHXb30rUbNrpENv1/0O6c+4mOpRINfbvy9y+1fV2vHrv3noQThNQmdS/SsoLtKfh5uk4ogVPPYrpPZ1ar6u6Tage6Huo6GC15KhafIKR7319efdW1lVo1a7igre5R0+bMcffUq4YOyffzTdi1rWbNUCKPnsc04l7nla6Hr733vn334YdyXc/zomP/3ifj3PbrOOs80X1C26EkPN0nHrrrzlznXar7VvdTPXfpGUL3oSmzZllG5UpWrUpVO3j+vHtO/eCzz6xChfIuaSY/57SSY7V/dS3T9mj7gsk7eu3XLdn9kMyyUz1PU/1bIJF7sSpgaH8mU8XuUrJhHXd+tmnRwiXf6ZqnBOzgcfLJ0rmqOu0OVHWKuD4En12yt/dS8qKuC2+8P8Y9H+n6pPuDvuueqXubEkiUsAoAAICijyQJAACAYkQdcK+/P8Z917y9GuXoAwYKHimItHnbNpu9cKEbDeapI+/NDz50Hcc9unR21QbUIS4alfvBZ+NDowwjOxHnLFzkOkUVNL3p6qutVfNm7v8VaHln7McuqB/tfROnTXPBwHatWtm1V14ZVgJdCRTLslZZowRHGqoTWwFWdeDqPerA9cGMEydPuvXQ6MZPJ35p9wYC8g/eeYcLYCvwog7QR+4endT+Vpn5TyZOdB2oCqZ1bt/OdeIqCKGqAdrG8ZMmuwCdOotFgWLZtz878BZJx27uokWuU/fKgQPCfqYqFwrqNKhb1+0zBaFEAYwvpk6zeYsX2ydffGHff/TRsA5vP3pPlRw0AlDb6UsEq4M4vFR57iCiOte1rxSQCwaDNCLvvXHjXKfvwuXL3YjKyCQBUYKERnBqNKEPFKtiyRdTprogo4ImiSal+G2JTNbRdvjA3SQ3YrSbq4DiR537dqwAgtq7b9+J0AjGTu3bubYV3K8KzL39UXZgX6M6g1Mv+PWUiVOn2XUjrnRBBbWP7ODJJBfAVaAtMknCB3T0M03r8P1vPBrqoFdn/stvv5MTsNhs7Vu3ztf5rxGw+tIoUwXw7r7l5rBEovzsh0SXfebMGXvzww/dPtMI9ZFDhoTOF7Wx98d96s5fBW50ngXPewWqtW1tW7W060eMCAVftG/e+misqyiQ7FQ/2te6fi1YssS1164dO8QMkARpfYKlzFUhRQlLCoYpkUhJT9EqTkTySTKxyqEvXr7CfVeguEZOoEv7RwkSSh665dprwxKldD1SAlUpSzywmogZBVxFQgG0N8Z84NqBAlrXjRwRageqbKQ2ooCm5qi/bsSIAmkHSp5TIFMJAqpqovantq2A+4efjXfXdy1fQcRgMsvM+QvctUvX4Juvvjqsqoveu3L1mtD1ORG6Ty5ZsdItR8vz79Wyxk6Y4K6hH02YYN9+8MHQuXfbdde6+9vv//JXt/8jE7XyouQBBVrlqmFDrVfXrqH7vCo1jPn0U5ccoH3qg611a2ffVxVQjha01PpOmZWdqKNqN0H6f7UZnUvXXDncHWPRcpQAo2pH47+a5J4JwhKKcs4HBTh1f/r2gw+E7qOh+1eMkviia+C9t97qrqXlyl3q4lLg+OMvJroEotnzF9i1I66Mmlyga5qqmFx75fBQYp6qX+m6pLaqgLeut4nQtuh81D01soKSTy7QvujYpo27T/vrt+6RurbreUWfmeiUIroXqI34+5CqZvTtcUXoOE+bM9clz+leE5kkkcrzTfZ+y94OXfeUqPX4Aw+4qjjZ63PMXn77bXf/WrFmjfXr0cOSoc9XdRJV6QlWlNH9RfcHVYiYNnuOSxYtiH3r36fkJz2X6bPbtW6V85nn7P1x49x2Llq2PFeSRLLntBK/tF6+gkmvbl1dtYmC2A/JLDvV8zTVvwXi8fddLePtsWOtf89erlJGItc5f+z8NUHHzVd2CiZJKBFGz9A6BpHXD1XTiFfVaeOWLa4tS+sW2YlpOj9UMU7PmUP797P+PXuG3TsOHDzkkv0ikyMBAABQNCU32TQAAAAuq7GffxEKkCpRIDiiUp1/KqnsAyBBGtGugHaX9u3thlGjwgLIXTp0sA5tWoc6EYOjHxUc/WLqVBegue/220KdoqLP1ihDvS/788M79VeuXeu+ayqGyDniK1WqaH2uuCLhMtYKuCtBQuv2wJ13hBIkRMtQgNoHhZSAETUgmeQ85tpfSrpQZ62mRFAg1Xfcaj9pOg2N7FWATUEYz+8/vV8d2pEU4NfIfyUVZAYCsyo5rqCFgkMafRgMwCngcfWwoa4qxKHDR0LbFBy56j67Th2755abQwkS4td5d6hDOfd+0KhkJTFEBkIViOyRM4I8cmRxMElASQAKMAU71NVx7EdQamRiokId3xEd7a6jO6fcsdr/VUOHhgUP1I59gMuXlE9Uh7ZtXJAwspNcwQ8FVUXTAkTbfgUWdE5ppL3f1/quUstuXXISSaIF5zTS9e6bbwoL0Ov4t8kZJa5jXRDnvzr51aGv4xMrQSLV/ZDIshW40f7q2qGD3XbddWEBN7Wxm6+5OhTQDtIIaFUJUPBTAaPg6FRNN6BASOj6k0RgRsEjHSO1J40c1rQpT7/0sktA0ihUH5iNtGf/fne+670+iOsDapFzocdKStK+8kGXehE/U3BNASoFeiQ4wl/T54iCSJGVZHS8FOjyQZyCoOSVhQVcRUJBXLWDls2aumkVgu1A10Il7/n2q0BUftuB9rWClHLHjTe4ILBv26rQoumW/OdE3h/8Nf2a4cNzTXuj93bv3CnhKki6tiuYqvV2o78D13Yt6/oRI921Vwkiwetq2Gjp2rWTSpBQQoqSQNRebxw1ygWrg9MtaF8oMUBWBK4X2iati97n7ytBqjajgKaeGYKVmVSVQueStlEJdz7wKlpvTQ+gJCwl122KuB/4bVbw9YHbbw+dW/69ed3HtR5q+8EECR841n3Wn7uxkgsUDL7jhuvDKhcpEczvn8j1jSc03UBEMPZMIKlQUz/ccu01Yddv3X/1PBHtuh9P8FlACRID+/QOO879c+5DqroUnIoh1eeb4DZqiis9F/oECdGzntpWrHtfXnTsVZ0kcsodJZz069kz6rHMz77126LrwF03XUqQyP7MsqH2E7ktBXFOx0vsS2U/JLLsVM/T/PwtEI+eCfx1VBVf/v7uuy4pTEndSlpRZYxYIhMRwyo7Bdp6sKpTrsQlf5+Oss5KPNQ0QKJEMn+90/HUtVEJQnqWiEyG0BQrfZNMDgIAAMDlQyUJAACAYkIltzVqVp180comS4O69XJ16Cq4vGxVlpsK4erhw6K+T/NYawR+5GhajThTwLB/r55hgQtP/6f1UaA+GJxSh7PvMF2/aXPc+ZzzouXMnDffvR45dEiu6QdESRPqHNXvHjp6xCVh5BV0z4s+U0FUjQ7XaL5oGtSr60bXB/e3EhTUsa2ghPZ9cDoNVQlQpQgFlH1AW7SPFRz1QbnIUvO+A1ul0NXJrwCPqk0ER66KRjPGGvHtgymRgVnR+7PLa2+2/YcOumkUfKBYwVIpUyY8QOc7/jWyfcSgQVE/UxUJNHrz+InYHd2x1jMyyOQ/T/tWCRLR+HUOTvmQCAWC1EG/adtWNye5AgR+n/rgS+R+9e1KQVofFIpcZrR10UhbH5zTyOZo88SrTHrke1M9/4PrmleiUH72Q6xlK7iiUcxq89cMHxZ1tKaSRPRzBXZ13ugYK9lp9sJF7udKwIkWIG7cIDsJR/swMpAdjwI8Kr8/a958lxSh5AQFPfS1cNlyF7xX4kswEJS9rdkBFQV1/HFTgPWTUhNt3cZN7vrj/z/WfvHLkE+//MrK5GyXyuGrVL2uOdrHGqXvg7TZP89OuFKlEFVRidZuCtL0Aq4ioXavduCvcdGOpw9kqh3oS8l8+WkHGg2v5XRs28ZaNQs/lj7BTve87Kkzwq83PsFN9y+/XqmaOnuO+67KN5FTCInuV0ow0rmi63u0ke7JTCcjC5YsdckMCt4qAB7r/qUA+MEjl64X2r+a5mL3vn1uvwQTQbQ87VP9zpUR13yV4RdVRwom6YV9Xt26riKKpnPwFATVckWVRaLd+xK5jyv4q2uX7rlKBvD3Ap8YEHn/8vcZtW0lWUa7Lun+pYoSmsYiUaHjFVnWP+fz9DlKzon2eX6dy1dI/P7l74tq90psyLXMnOu3Pi/47JTq801w6oKRQwZHTXS9cN7fv8ID/Mlsk6oB6HN0/Py9VNdp8dfMgti3/r1KPFRyQKTzF85HfV/+zunYVb3ysx8SWXaq52mqfwvkRe/RFChzFi22JStWuOcMXbPXbtzovpRgqcooesYMXvt1rw1Vdcr5vGBlJ71XibPBahFRqzrlnK9rN2wI7Ttt5+GjR+zY8ezrku4dt157beg9ulfL4WPH3DUymLQNAACA4ockCQAAgGJi+aqs0MirWIGEMmWzg1nBAJoSJBTo1MjIWJUbfGdrMBCjzs6snBGEwdK10d57NuK96qRWgE+j7TSfs6Yd0Kg4jVpLJpgp6uxUp6mCNdECXf7ztB7quiwbGcSNM1IsFu2vFauzR9dGTpUQ5IOHZcuEP1arZLmmqNAowmCSxMRp092yB/XtE1bNQx3Sqn6g7VBp7FjUIRv5eT5Ioo7t4IjOIJWNVme9ll+3VvjvqANe01REjnaMFKx6Eexc7tqhY67ReZ4ScySZssOhkbi5gsvZ/9+uVctcI4ZFwVQlH+j4x6uWEElB54/GT8iz+kSN6tWirmfndrkTJET728/bHq09KkARHIkd/b018n3+hwcJ6hT4fshr2f76o7b35ocfxVyuEjKC6561fp0bIazqAdGCMlKmdPb21k1ylL1b3zp17NbrrnWBp607drrtV8KEAiUKDml6n+88/FBYW4oWqNU1VcdR5/CGLVtcSXbtQx+c9clMkcsQJRB5SojRNa550ybWs2vXXNfJDm3augQOBYD+/NLL1qldO2vTorkL7BVEEkOQEqMWLVvm5pwvqCoSK9ZkV4do1bx5zAoMwe3w17j8tANNwZPI/StaEoLul5qKQtfjVWvWWMd2bd20A7GmSIll5+7dLnCv670P2kXj71v+HM5vJSRNESF9r7gizmeWjX7/qlM7J0nigHW4lKfjAqUK0Pbu1i2sfSqIrvYv85cuc+00moOHsytTlA1so79+6DzSSPBY/BRFkcdJCYO6f+n8jSezWvWo128lOQUrkwT5+0z5cuVTSPKrG/XzdK1QdZZIOjf8db92jZqJf17O9cRPIxNpf04gWYlo/vzKz/ONr+ik63TbVpeqLoR9pt+OwP0rEXrO+/jzz13CbjLPIvnZt/69up7G3ZbA+/JzTutc8QlgsZIUUt0PeS071fM0P38LJEJJkUP69XVf2t+qBKHkND376/6sxKyMSpXDkoBURcNPBxRMUlAlEN0jVdnJH5tYiSN6XtT1w+2bI0dCzz46bhkZGe5vGCWY6Z4ePLd0but8UkLHc6+/4X6uShO6fgUrmAAAAKB4IEkCAACgmNBISVGp8lj8qP9gGXN1OOb1vgOHDufqRNTnqTNcI22D0wFEdjKqUzbaSO6br77KdV4uXLbMdczq6/PJU1ywZ9iA/rnmd85zuyNKzEcGWPWljsxgJ7k6x/fkTG2QzOg2TSmg7VLgUqNJ89zf1cI75lUaPTtJYn/YcVi3caPrvO6TU8LZU3DVr6/f3niCx8MHSdrE2Z+792VXm6hTS/OkX/oTQB3Er77zrgvoKnipeawV1FXAygern3zuebedkQFKH1xoEadd+c72YHuMx48q1ohXtbvIbXCf1yT6qNedOUG0ZALmCtC98f6Y0Ijabh07ukQTBRkUIFJw4DfPPOs644Pb70bT5hxbBbWTGfnsp9po3qRx1PdphKQCMO69gWBDqud/eKJQnQLdD4ksW3N6i9pYXm1bwUofmPOlvuNft7LbV7KB6yB9no6FvjS/+PwlS1yFB40W1SjyAb16xSzv7bVv3dpd37LWrnPXNf97SjKITEzzP1PygaYiSpSqWtx/x+1uHnp9luad15eOkZIqFGAqqOoSmq9ebWFgnz4FloCh62Gi7Vf7zF+nUm0HmhdeyS5K1IqVjKRr4sGcwFhkGxo+cICbOmDuosUu8K0vVfvRdUlJbhp5ntB257R5rUO05K7QtueUlc917iY46jxI9y4/2r9l0+iJhXIkxvXCn+PB+5eCl7qXK+FtSP9+Ue9fomlRkrl/+fuIEihjXbd1T9Co7sj7gpJndP9SsFLVsJQQoqoiGtnvz4WX3nrLtu3YmSuBMFQJqGnez0WR9/dY1J725uz33Nf9nPtXjPuFtkHPMGr7kdOTxeOX27xJ04TvQ/l5vvH3L70vWlWvWNfJeYsX55oGSnp07eLuN7q3/OODD9yx0nOKppRQApiuoTqWerbT1AdKvA1OCZeffavt0zOsrnFNGzVMeFvyc07nVRkmP/shr2Wnep7m92+BZOi9+tI9TUkdL775ljv/l6xcGZYk4Y+LryLnhSo7bdocquwUO/F2Tygp4qfffyLh50Yd80fuHm2TZ86yFWvWuGo8+tKx0bQ/IwcPproEAABAMUKSBAAAQDHhS99WqxK7w96PEgsGhfz7alQPDzoHqYx/ZOfqpfdF7xQVjfaKFZhWx7OmlFC5dgVsNCpMQRZ1TL790Vh79J6743bQe36kV9U4gQMfgNN6BKcoUPBMI1/VOR5rtGjcz6xSJerozEsJDdkdzZFBuFCQ6cClINPEqdNCZY4jg5lHjmR/3rVXXulK6SfDj1xtWD92SXjfSVwvYnTrtDlzXPBa1Qk0j3ckjZj0gZLgiPhgkkCs9qERgJu2Zo/ubREnEBX2eTmd1hrJHLnffXAm1ojqWAHseL6aNt11/g/u29cFRaPNSa1t1fEKjn5XwFAd8ApQx6pa4dcnspLA7jwqm+hc0WcqUBCswJHq+e/WZXf8fZPqfkhk2X69v/PQg0kFDhK5bq3fvCnns1NPkoikkbKq+KIArG/7/nwPBVsiglAavfr5lCluBKv2U7y2mEhVj1gU0NWXgkaaHkeVEnRsVPFA63vNlcMtv7TNuk4rKNejS8FUkQheU7Xcgr5/RWsHKpfuPq9q1ZiJHprCRtO7aPRv5P1B1x+1BX3pfNd0KpouROfn2Amfu/tMcDqUWELnbZz7l5KiFGTUlDPBc8RXAJLIgGg8vt3qfA1OPRVpq9/fjaLfvzT9i6cEEbXtgb175RotfTjn/qVS/JomJhk+sBvv/hXrvjB38WIXAFeVkQfuuD3XM4jOk+07d0U9F/25nJlZPc/nokTvXzqOSq6qmpGRax/lNS1RKvcvPdv4aZBiXU+iLTc/zzehpLgY6+lG5x87litYrqoI0ZLkfOB75Zq1LjFA95fH7r03V3Uqtb31mzaFpgEpiH3r36dzLuY0ZVGSTPJzTseb+iG/+yGvZad6nub3b4FUqVKGKvoowSZ4Lw47phH34mBlp/WbN7v1UfKGNIh45gpdV1JYZyViaWoXTQGl82T1+nW2ePkKN+WPnpufePTRmElEAAAAKFpIkgAAAChm/HzEkTTae+nKVblKByt4lv2+7O/RAgE+EBMM6F56X/TPk/lLl+S8L3bAT53wSr7QV78ePeylN99ygf3NW7cllCSRM6V2aI74aDSdhy85nd/Ag/tMu5jnZ6o0vyofKBAQufxLQabsigAqba25vbV/NVI/kt/HCrwntZ6BkauRwfhEqhpszBml3T1GSf0lK1a671UywoOIruR2zrpqHaJZsXqNS8BQJ3asMvmRLgWhw7dFHeQKeGnEX6xA+6VgfWLBZ633ppwS7Vd0ib/9arvBTvRY+zORgIV/b2SAI/f76hTI+e+DVgoCRZvmID/7Ia9lB68jfh7vROV13dI1y7ffZKbSyYuCUH7++mCb1+f5fR8ZmFHwSEFsTVGwZfuOmEE7HaNQ0Dsf1S8UgO3Sob37UtWHL6dPt41bL40Szo+Z8+YXeBWJIAX+o1E79OXfk7l/xWoHZ86czfv+tXhJQpVI/OhmTU2gKWNUEUifmUiSRM6tJO56LFhy6f4VDForIUP7JTJhKs+PzLkmK1HNl6SPdp3ZvmuXK3UfOc2FT8jYd/Cge//O3XvcSGkF/3UPj33/Om/JijXKO+x3/DUz4jwP3b86dYy6jUtXrQpN9RM8xmpTvvrIxQvR718Ksmr/694Xr9pEtG2pF7EtwaTCWPfpVJKn9HnaPl1/FIyPu9zAvsvP801weq+on5dzH1YbCh6TB++8I+62+IpDOgeiTd+l9fFTGAXXJz/71ieYxnqfrlV61vHPsAVxTidaeSnZ/ZDIslM9Twvqb4FU+OfMyCS2nXGewUKVndatC93LVcEtMmEsr8obyVaiUiWbZ1951VW9URWeRP6+AQAAwOVXMCm+AAAAKHS+zPTOPbuj/lyVCtSp3bxJE2vcsEHYiCfxowIjOz8nTJ4cCrz53w2+Tx3QKlUcSaNqfQWHRDsZNbrQB3syKoeXoY+lZo3MsM73SCqJr45jV3a+S9ewn4WClUl2gvoRkAoAa59GUnBYU4eISq9H8kkBrszz6dM2acZM9+8RQwZHHbnpy1lr5Fsy/MjVaCOhExl150fYKZgWSXO8z1vig4iRo3CzEzNiHRftn6+mT3evhw7on/D2xEo+8MGMeCP+QnPBJ5gQo853v93Rtl+j9FUZIFrHf16BPbUZfZWLGE3rR/5GBu2it9m6BXL+H8wpGa+KF9H2XX72Q17L9j8LjspOlL/+aFRtJAU+xk38MhQAjSylH0u0czmSqihon2i57Vu1znVcMqtXs4oVc4/OVzUJWb1uXZwqItlB7/wGZoL8iPXKlfI/F7r2z4KcKhJXdE5sOolEhdrv7ujtd9aCBW6/6Xzp0KZ1vtuBf58SrPxc85Gl55dlZbnX9RIM7OkzKucE2pK9f/kkrkgaXa/rrIJtKq0fLeiZbFvRCGydj9ovvi1GBn/HfTHRvVbSQ2RlI1XfUNBd1wNdryZOy66CNGzggKiB28yc+5emlPJByUQoSK9khVxB6Aih/RBxnHRvFSX1REuemZxz31WbCq63kpm8aNdTnf/jJ2U/Fw3q0yfhaWxi3b985aHs6R6iVwIKJc8lkdAZSliIkXDnqt9Emf4jP883/t4f6zNTTUw9fSb2sdT6fPbVV+61RucH76n52bd53cf9NGUuSSnQfvJzTvt7ZuR0YvndD4ksO9XztDD+FtDfHvGSdPy1W4kOEkxIy55GL/ax8/fiNes32I5d2ed3tOetVKYyiiej8qW/nyIryQAAAKDoIkkCAACgmPCja2fNXxAaGScnT55ygSJ1ymrUY+Q8962aZc9JPmfhwrByx5qz/Y0xH9jenGoHkZ2ICrYq0KJAhDpm/YguBU7mLV5in36Z3Vmb/d5LHZWaO1il5yODM+oUVclujfBSQkPbVq2S2m5N16GArZe9Hovtg8/Gu/W8+Zqrc48USzJw7vnRk+qM1Xb6kXSi0beaB12BN62bpqqIpACT5qHW+ydMnuICQRqtG2tEqkoK+0DyF1Omhn2e6BhoDmrNgZzsKFzXoRwKlIT/nq/KMHP+vNBn6vc1alhTopTJCXzXjyxvnRO00n6fPHOmHcwZlSv6rNffe98FYLp36uTmiE5UrOkM8goGKflAwRIXbKudWIe3gmYaXSjTZs8JJQgoeKDKJB9//kVoJH3uEZs+6BK9RLw/PxUwCCbF+JG/CmLEGhkeqwJFque/nzP92IkToRGoBbUf8lq2dMyp7jJ11mx3bYisPKLS77MXLHTtO6hl82ah0eBKggoGTt79+BMX6BHty2iB22g+mTjR/v7ue5a1dl2uigYKFirR5LOvJoWCoz4YFp5oFL0NtmudHdhX4F1tP3qyz57QOgenBYpHQal/jPnABYsi11klzn0AuyCSGmbMm+eu8wOTCAwnqlP77Pa7aMUKl6AQPHd1XdO+V+Dv1muvCatgkWo70L5X4FQ++WJiKCFM7U+VffTe0O8GjqkS1dTmdZ8KtlXtlzkLF9myVVlu/aJVBIpG1z9VwFGAUdOi+GX69Xjzgw/duXb1sKG5gp6JVKyJRvuxbcuW7rXuP6rCE0yse2PMGFfZSNNUaPqMaHyinyqVKGGubq1a7noedRtbt3b7RKOnP5owIezzRO1Wx25CTuJBaPv2Rg9CR4qV7OjvX/MWLQ77TE3zpHuQ39eRwXG/PN2/dO0JXk91X3/zww/d+a5pNnp37x5zvXKtZ173r7jVnpJPLshrmgmXRHn2rAtyBxNgU32+8RWdIpP/CmI6odo1s4/lkhUrQlOIuG3cu9dee/c9O3XqdMx7aqr7Nq/3xkouzc857du5jk1B7odElp3qeZrq3wLxaLqoP7/0srvn+OlfPO07Peu//Nbb7rlCVar8tCx++3xlqmjJD76yk96rBA73exHHWPcdX00mmSS0z6dMdc9gfgoST/9+75NPXHKL9lesadgAAABQ9DDdBgAAQDHRq1tXFyhTwOKFN/7hOgIVFFKHoTorFSS666Ybc80brM7F5atXuw5RdTqq07Z06VIuOUKdqgoEqMRv5ChNjYQa2Lu3TZszx5XxX71+g9WsXt11nqvzUUEYJS5IcATvlm3bXcekgg9KFqiRWd3On8sekaqOTf3fnTfe4BIlEqFOUI1mnD5nrkuIUDCtSpUMO3Aou0qDgo0KDPugUEGNFLtx1Eh7VQHVdevsD3/b7PbbqdOn7NDh7FHJXTt2sBtHjYo5p7eCTOo4XbxihfudEYMHx/wszaGsoKw63DWiWskfGg2s43Pi1MnQZ+p3wrcv7wCBkgfcPOlVqoSCht7APr1de1LZ8ieff8FqZWbakWNHXblgBYc0ylYjuGONwLxy0EBXJeOZV1612jVq2PkLF0LTCbRt1dKuG3GlJTuqWJ3xkdNz5BUM8skHdWrVDAXuEzG4Tx8XOFcAdt2mTa5jW+1UAVVt29yFi+zo8eMxtz/2aNq9KQVlXOnwKCN/83P+q93q/7RdT734kjtO2setW7SwwTmjhFPdD4ksWyPVFRBRMPuj8RPcCG29R2eNkgl8oOa+228LW3aPLl1s4dJlbvuUsKN10rm+NycA1KZFC3f9Sebc1shffabWRevpklXKlXPbpiQJtSEFkYb062uD+/aNXso8xrHTvlGyyeGcqgW6vvnkk9AyAskziVKb0THRl9a5Vo1MK1e2nLu2+H3Xt8cV1rVjR8t3FYmlhVNFwgcWu7Rv75JIFMB25c8rVsy+PrlAbmW77brrrWFEifJU24GO44jBg1zCg5LPnvzbc1arZk07dlyj509Yk0YN3XRIbkqgwHt37Nrl7l/6Utvwo7JdVaAzZ1xwVAl5sYLEkRScvnr4MJfIpCRB3RdVOejwkaNuZLjW85rhw6xXt2653ptXYk48CtBu27nTJUb+8fkX3LmpAJ6/Prdq3txuv/66mMkwCjIqUUT3L9H9K9a9TsdEn6cEIyWRaKol/Z8qrpw6dcrtO51bkYkliST5xbsvaJS+gtIKVj/1wosumHri1Cl3DqrkvtZXCX+RSX67c85lXaPmLlrsrqc6zkoKVLvSujZq0MDtn1jbnMx0UaH7QYz7hU8+CLa3ROSVrBfvvpnK843/vMhplwqikkTPrl3cc4/OTU1ZoGPpj70qI13RubNLBIyV8JLsvj2TQBWTWNuSn3O6dYvmLkFp7Oefu8RlJUtqXz5wx+3ue6r7IZFlp3qepvq3QDxKptE958tp092X7jvar7rG6t7mEzGUcHDbddeGJVH5Y67nCE0XFI2qSaiKio5H9jHMnbiUSlWnNevXu2PxxdSp2etcpUrYvlPi1i3XXJPw8gAAAHD5kSQBAABQTKjj9b7bbrWZ8xe4UdfqlFPHoZIIVIlAQdRoI6oVHH9k9Gg34ljJEOrYrFolw40KVdB9/KRJMQMxwwcOcGXFNVpMn6evOrVrWbcOHd13leGPLGWtoFZGRmWXLHHo8GHbs2+/66CtkZnpOnEVNA2OakzElQMHupGsCmio4/PUgTOuUkOPLp2tT/fuuYKRog7YvEY9xtOgXj177N573Aj4jVu3Zs9PXrmyKwV/RZcuueZxj6SAju847taxY56dxwpGN23UyM3tvH3nLtcRq+OrfdWxbRsXeI6synApyBSnVHmcKUe0DaNvvsltowJNahsN69Wz3qO6W5uWLey3zzwbc9oAUdBTCR6qJqGyxuok1r/V0d+lQ4ckA0zZUxEoKBAZuEt8xGdygZkeXbu4tjlz/ny3v9U5r7mlFXxTZ7c67yODc75dxQtoxUrOCR2vGAEdBYIVHNA5G1muOdXzX++759ZbbNKMGW7KHQVRtJ99SepU90Oiy9axvP/229w0FgqQ63qgc0kBcrXt9q1bubbdvHHjsGVr2zSPvT5b1xklN/jzb0CvXqERoskEOB648w4XFFRpcO0/HcsDZ864oLvaffOmTaxH587uWhUpkVH97Vu1sjmLFsX8vVQqA+jadc3w4S6xQ/vtyNFjLulJ+8IfdwWS8mvGvPk5VSR6F3gVCe+Wa69xwefFy5e7tq7gks4htQElZUUmceW3HSigqOu/tk2fp4o32p9q153atrU/vfBirvuDguuKnW3ettUl4vlR2brHKBFF884HK4wkomfXrpZZrbqr2LN9126XoKgAm67r2m5fESF2BaDkk/y0vt+8716bMmu2uw8p+K+pQpQc0b1Tx1BlmljqBu5Xal+6H8SjgLCOwdxFi9z5pXtJuRMn3DmuRBbd+1O5f8W7L+ie+kBO21AFBJ3TWocrBw5w95/nXnvd/V6swHrLZs3cftD7lXymJBJdT7t16uiOWbCiSV5C94Xy5V1iaDL3r1iVh+LRPtm7L9GpL+oUyPNNXtuhALdPaFGSTTJ0z3t49F2ukpaOhY6ljrmufbrG+covuRNeUtu3e3Lalc69WImV8a7XqZzTkp18V8rdw91UIefPu9/1SSep7odElp3qeZrq3wLxKHlb26W/SdT2dP7o2V7rqv3YqEF969K+g1ufVJKfdR2fOntO6N+5rwHJV3WSkYMHu3XeunOnS8DRcnSPUgJLxzZt3bNvYd0/AQAAUDhKXYysd1rErFm/ybbt3OVed2jbKuHMZAAAAAAFTx3DTz73vAto/vg732YXA8WcAlSqBKLEle8/+ghBHpRY6v761Z+fdglB//bE95IKkAIAAAAA8rZ81RrbdyC7imGPrp0ss1pVK6qi18gDAAAAgCh8ye3IqgIAiidVJ1HQeFAhVpEAigJfrSezeva0LQAAAACA9EUPCAAAAICE+fLWdWtHLycNoHi5augQ9wWkz/2LJD8AAAAASHdUkgAAAACQMD9Pd73aseeDBgCgqOH+BQAAAADwSJIAAAAAkDBG4gIAiiMqIQEAAAAAPKbbAAAAAJCw7z3yMHsLAFDs3Hf7bZd7FQAAAAAARQSVJAAAAAAAAAAAAAAAQFogSQIAAAAAAAAAAAAAAKQFkiQAAAAAAAAAAAAAAEBaIEkCAAAAAAAAAAAAAACkBZIkAAAAAAAAAAAAAABAWiBJAgAAAAAAAAAAAAAApAWSJAAAAAAAAAAAAAAAQFogSQIAAAAAAAAAAAAAAKQFkiQAAAAAAAAAAAAAAEBaIEkCAAAAAAAAAAAAAACkBZIkAAAAAAAAAAAAAABAWiBJAgAAAAAAAAAAAAAApAWSJAAAAAAAAAAAAAAAQFogSQIAAAAAAAAAAAAAAKQFkiQAAAAAAAAAAAAAAEBaIEkCAAAAAAAAAAAAAACkBZIkAAAAAAAAAAAAAABAWiBJAgAAAAAAAAAAAAAApAWSJAAAAAAAAAAAAAAAQFogSQIAAAAAAAAAAAAAAKQFkiQAAAAAAAAAAAAAAEBaIEkCAAAAAAAAAAAAAACkBZIkAAAAAAAAAAAAAABAWiBJAgAAAAAAAAAAAAAApAWSJAAAAAAAAAAAAAAAQFogSQIAAAAAAAAAAAAAAKQFkiQAAAAAAAAAAAAAAEBaIEkCAAAAAAAAAAAAAACkBZIkAAAAAAAAAAAAAABAWiBJAgAAAAAAAAAAAAAApAWSJAAAAAAAAAAAAAAAQFogSQIAAAAAAAAAAAAAAKQFkiQAAAAAAAAAAAAAAEBaIEkCAAAAAAAAAAAAAACkBZIkAAAAAAAAAAAAAABAWiBJAgAAAAAAAAAAAAAApAWSJAAAAAAAAAAAAAAAQFogSQIAAAAAAAAAAAAAAKQFkiQAAAAAAAAAAAAAAEBaIEkCAAAAAAAAAAAAAACkBZIkAAAAAAAAAAAAAABAWiBJAgAAAAAAAAAAAAAApAWSJAAAAAAAAAAAAAAAQFogSQIAAAAAAAAAAAAAAKQFkiQAAAAAAAAAAAAAAEBaIEkCAAAAAAAAAAAAAACkBZIkAAAAAAAAAAAAAABAWiBJAgAAAAAAAAAAAAAApAWSJAAAAAAAAAAAAAAAQFogSQIAAAAAAAAAAAAAAKQFkiQAAAAAAAAAAAAAAEBaIEkCAAAAAAAAAAAAAACkhbKXewUQ37btu+zM2bPsJgAAAAAAAAAAAAAoBsqXK2eNG9W/3KuBGKgkAQAAAAAAAAAAAAAA0gKVJIo4MowAAAAAAAAAAAAAACgYVJIAAAAAAAAAAAAAAABpgSQJAAAAAAAAAAAAAACQFkiSAAAAAAAAAAAAAAAAaYEkCQAAAAAAAAAAAAAAkBZIkgAAAAAAAAAAAAAAAGmBJAkAAAAAAAAAAAAAAJAWSJIAAAAAAAAAAAAAAABpgSQJAAAAAAAAAAAAAACQFkiSAAAAAAAAAAAAAAAAaYEkCQAAAAAAAAAAAAAAkBZIkgAAAAAAAAAAAAAAAGmBJAkAAAAAAAAAAAAAAJAWSJIAAAAAAAAAAAAAAABpgSQJAAAAAAAAAAAAAACQFkiSAAAAAAAAAAAAAAAAaYEkCQAAAAAAAAAAAAAAkBZIkgAAAAAAAAAAAAAAAGmBJAkAAAAAAAAAAAAAAJAWSJIAAAAAAAAAAAAAAABpgSQJAAAAAAAAAAAAAACQFkiSAAAAAAAAJc7ohx63Zh1727sffJz0e7/1/Z+49+rr5//1GyuqvvnEj0PrGfnVtvtAG3TVzfbPP/2/tnzl6kLZT5F++bs/uWX96Ge/SPg9iXx+UdtOAAAAAEDxRpIEAAAAAABAjnkLl9iELyeH9sfqNeuL7L5ZuXptzJ+dPnPGtm7bYWPGfmo33/2Qff7llEJfny+nTHffRwwbUqK3EwAAAABQvJW93CsAAAAAAABQVKgaQlDW2nVWFB05esy2bd/hXv/kh9+1R+4fHfrZ2bNnbfvOXfaPdz6w199+386dO2///p+/thHDBlmZMmUKZX02bd5q6zdssvLlytmQgX1L7HYCAAAAAIo/KkkAAAAAAACY2bgJX9qiJcvcvhg5fLD7fuTIUdu5a3eR2z+rVq8Jve7Qvo1lZFQOfWVmVrdOHdrZL//vT23Y4AHud/bs3Wdr1m0otPWZOHma+96vT0+rkpFRYrcTAAAAAFD8kSQBAAAAAADS3tmz5+y3Tz7j9sPAfr3tmw/fF9onWWsKpprExYsX7eixY3bhwoV8L2vlqkvJA21aNo/5e/169wi9PnDwkBV2ksSIYdnJJQWlqG0nAAAAAKD4Y7oNAAAAAACQ9l5/6z3btGWrlSpVyn724x9ao4b1Qvtk9dr1NnzIwJT20blz5+yt9z6ydz4YaytWrXZTQmgqiA7t2tgdN19v9999h5Url3z3zMrVa933ChUqWONGDWP+nrbHq5FZ3QrD4SNHbd6CRe61prooSEVpOwEAAAAAJQNJEgAAAAAAIK2pusNTf33Rvb7lhmusc8d27nWdWjVt7/4DKVeSOHX6tH3r+z+2KdNnh/3/+fPnbfnKLPc14asp9vKzT1qlShWTWvaqnOSBVs2bWunSsQuFzl+4xH2vWSPT2rZuaYVhyvRZLvmjfdvW1iROIkMqitJ2AgAAAABKBqbbAAAAAAAAae3Z515xUzRUKF/efvyD74T+v01OsH11ikkSf3zmOZcgUa5sWfvJD79rM74Ya+uWzLLZX31iP/vxD1x1hFlz5ttv/5g9zUcy1SnWrNvgXrdu1SLm702fOce+nDLdvdb0IWXLFs5YmYmTphbKVBtFbTsBAAAAACUDfzUCAAAAAIC0tXPXbnvp9bfc64fvH22NGzUI/axt61Y2c858W7dhkwvYJxN8P378hL36xrvu9c//7Z/t4fvuCv2sQf169vijD7hpIX7y8/+yN94eYz/8zmOWmeA0ERs2brbTp0+7100aN3Kf5Z07f96279hp4yZ8ac+//LpduHDBrh45zL71yP1WGFQVY/K0We71yOEFmyRRlLYTAAAAAFBykCQBAAAAAADS1u+f+oudOnXaMqtXt+9965Gwn/lpG86cPWsbNm1JahqH6bPm2omTJ61qlQy7767bov7OHbfcYL958hnbt/+ATZo202698dqElr0ia03o9TPPvey+ounYro09eN9ddvftN1upUqWsMMxbuNgOHzlitWvVtO5dOhXosovSdgIAAAAASg6m2wAAAAAAAGlp1eq1NmbsZ+71E48/YtWrVQ37eds2rUKvV69NbsqNlTkB/u5dO1u5ctHHqJQuXdp6XtE17PcTXe9EbNyy1ZavyLLTZ85YYZk4aZr7PnzIQLc9BakobScAAAAAoOSgkgQAAAAAAEhL//P7p9w0DU0aN7SH7r00HYYXrByRtWa9JVjowVF1CAlO3xFN44YNwn4/meQBVb9YMmti2M80Lcj2Hbvs7TFjXeWF199+344cPWp//v0vrTB8OTk7SWLU8CEFvuyitJ0AAAAAgJKDShIAAAAAACDtTJs5x6bOmO1e33TtVZa1Zq0tXb4y7Gvzlq1WvVo19zur1yRXSUJTbUjlSpXi/l7lytk/P37iRMLLXpVTdaJTh7a5fla2bFlr1rSx/es/fdduuGak+7+PP/vCdu7abQVt46YtbhqSCuXL2+ABfQt8+UVlOwEAAAAAJQuVJAAAAAAAQFpR9QhVkfCeef4V9xVPVpLTbegzJK8pKMqULuO+X7xwMaHl7tm7z/bmVJ3o3LF93N/t36eXfTJ+ol28eNElMzSoX88K0sScKhL9+/YKJXsUlKK0nQAAAACAkoVKEgAAAAAAIK2MGfuprcypUpCobdt32rHjx+1yC653XskDpUqVCr0uU6bgu4B8ksTI4YNL9HYCAAAAAEoWKkkAAAAAAIC0cer0afvDU391rwf172NvvPhM3N9/94OP7cf//p+uSsGadRusR7cudjmtWr029Lpzx3Zxf3f2/IWhahatWjQv0PU4fOSozV+42L0eMXSQldTtBAAAAACUPKTXAwAAAACAtPHS39+0Hbt2u+oD//bP38vz91u3bBF6vXpNclNuFIaVOckDVTIyrEWzpjF/b+qM2fbJZ1+411cOHWh1atcq0PWYPG2mnTt33jq2b2sNG9S3krqdAAAAAICSh0oSAAAAAAAgLRw8dMj+8sKr7vX1V4+wrp075vme1q0uVSbIWrPeLreVq7KnoWjbpqWdOHEy7GcnT52yjZu22MeffWFvvPO+XbhwwSUN/OJnPy5WU20Upe0EAAAAAJQ8JEkAAAAAQJp78unn7I/PPp/w7//Td79p//zEt+xyrmvjhg1sxsSxBbbcH/3sF/beh+OsX+8e9varfyuw5aJo+dOzL9iRo8esbNky9uMffCeh91StUsXq1a1ju/fstay16y77VCEbN29xrxcuXmYdew+N+/tXdOtiT/7qF9akUcMCXY9z587ZlGmz3OuRwwaX2O0EAAAAAJRMJEkAAAAAAIDLmqTyddi8ZZu9/tb77vXo2262Fs1jT+EQqXXL5i5J4nJPt7Fm7Xo7f/581J+VLl3aqmRUtgb167kKGTdcM9KGDOzn/r+gzVu4xA4fOeKqNyRSjaO4bicAAAAAoGQiSQIAAAAAEPLF2LetUYP6cfdIuXLl2GModpo1bWzrlmZXP0jWP156Nun3/Om3/+W+8qKqLIlWZlFSwOaV8+xymzhpqvs+YtggK1WqVIEvv6hsJwAAAACgZCJJAgAAAAAQUqlSRcvIqFxk90gyAWUAhePLydMLbaoNAAAAAAAKG0kSAAAAAAAASNjkz7KnLQEAAAAAoDgiSQIAAAAAkC8/+tkv7L0Px1m/3j3s7Vf/ZstXrra/vviqzZm/yA4dOmx1atey4UMG2g+++5jVq1M7z/fPmb/QXnrtLVu0ZLnt23/ARgwdZM8//Xv3u08+/Zz98dnnrXHDBjZj4tio63Py5Cl7450x9sWkqbZu/UY7cuSo1apVw5o0auSmB7jpuqusYR5TiuR3G+YtWGzPv/KGLVy81I4eP27NmjS2e+64xR68904rU6aMe8/OXbvtby+9Zl9NmWG79uy1zOrVbNTwIfajH3zbatbIjLt+J06ctNffft8+/3KKrduw0Y4fP2E1amRaryu62cP33WV9el1RoMdq1twFdvfD3w79e9uOndasY++wZd9xy/X2h//5Rdz1BgAAAAAAuNxIkgAAAAAAFJiPxk2wH//s/9mZs2dD/7d95y4X0P9yyjT74B8vWYP69WK+/+XX37b//PX/2oULF1L6/KXLV9o3v/8T27V7T9j/79y1x33NXbDI1q7fEDeYn99tePO9D+3f/9+v7fz586H/W712vf3iV3+wZSuz7H9/9QtbtmKVPfztf3JJIN7uPXvdZ8yat8A+eutlq1qlStTlr1i12r7xvX9x2xOk94+bMNF9ffexh+zf/uWJuPsqv9sJAAAAAABQHJEkAQAAAAAoEJu2bLOf/Pt/Wq8e3ez73/6GtW/b2lU8eH/sOFcBQkH9X/7uT/b0H/4n6vvXb9hk//WbJ12VA//+4ydO2NZtOxL6/I2btti9j37Xjh47blUyMuzxbzxgV48YavXr1rVjx0/YyqzVNmHiZCtfvnyhbcPGzVvtP/7rtzZ0UH/73rcetlYtmtnevfvtD0//zcZ/Mcne/2icq4zxP79/ympkVrdf/eJn1qN7Fzt9+oy9/f5H9qe/vOD2w9N/e9n+vx99P9fyt23fafc88l07fOSItWjW1J54/BHr27uHVatSxbZu32mvvfWuvfXeR/bsC69a40YN7b7RtxXIdvbp2d1Wzptizzz3sj3z/CvWqEF9+2Ls22HLLFsufheDkkZOnTpt+ZGRUTlf7wcAAAAAACBJAgAAAAAQNlWFpm6Ip3LlSlaqVKlc/6/qDVcOHWQvPP370JQSmjbih995zA4fPmIv/v1Nlyhw9NixqFUS9u4/YIP697G/P/dU2PubNGqY0BH69//8tUuQyKhc2d57/Xnr0K5N6GfVq1ezRg3r26grh9q5c+diLiO/26BqDlePHGZ/+9NvQ/uoRmamPf37/7Erb7jDtmzdbj/415+76T7G/OMlq1b10jL+5fuP2+at2+zDT8bb+x9+EjVJ4j9++VuXING0SSMb+86rYe/XNv7mP3/upsz4819fst/96Vm77abrrFKlivneTv2OEhTKlSvnflfblmzCwtwFi8Om7EjF5pXz8vV+AAAAAAAAkiQAAAAAACGjbhqd596Y/sVHMRMX/uPf/jkUdA+6/ebrXeD97LlztjJrjfXt1SPq+3/6o+9HfX9e1q7faDNmZwfQf/Cdx8ISJCKVLRv/T+H8bsPPf/LDXEkk5cqVtWtGDrfnXn7dzp07bz/49jfCEhy8m667yiVJKGFk+45dLrHD27J1m301ZYZ7/Yuf/Tjq++WJbz1iL776ph08dNimzpjtkjYKYzuLurdf/dvlXoVi4XLvp6/r8y/3dgIAAAAAig6SJAAAAAAABULVDVo0bxr1Zy2bNwu93rfvQNTfqV2rpnXp2D6lz54551KFgdtuutYu1zY0b9rEmjZpHHPZ3uABfWP8zqX37t23PyxJQkkgFy9etLJly1jXTh3iVvxo1bK5LVuxypauWBU1SSK/25mK/n16UgkCAAAAAABcdiRJAAAAAAASqhKRl3p16sT8WXDKh5OnTkX9ncYNG6R8JDZv3R5KtKhbp3bKy8nvNmiqi1gqVqiQ5+9VrHjpd06dDv+M9Rs3u++qRNFryDWWiAMHDhbKdgIAAAAAABRXJEkAAAAAAApEmTKlE/o9VUPIK0EgWcePH3ffMypXtsu5DYm+P5EpRSI/4uixY5asM2fPFMp2puL8+fN26tTpfC0jIyN/xxcAAAAAAIAkCQAAAABAsZeRkeG+Hz8RewqK4s4ngDSoX9dmfzXOipu5Cxbb3Q9/O1/L2Lzy0rQqAAAAAAAAqSBJAgAAAABQ7DVv2th937f/gO3Zuy9fU24UVU0aN3Lfd+3ea/sPHLRaNWtc7lUqcpp17H25VwHFFAk4AAAAAJA+SJIAAAAAABR7A/peCo5/8PFn9vijD1hJM6h/n9AUGO9/NM6+9cj9X/s6lC2b3Y1w/sL5pN/bv0/PQg9EE+gGAAAAAAB5SWwSUgAAAAAAirDWLZuHkgj+/NcXbfXadTF/99y5c1YctWnVwoYNHuBe/+nZF2zZyqy4v79t+047feZMga5Djczq7vuBg4eL7X4EAAAAAADpjSQJAAAAAEDIyZOn7PjxE3G/Tp06VST32C//46dWtUqGHT123O64/5v29N9etjXrNtjhI0dt567d9uWU6fav/+e/7f/+8vdWXGkba9bItGPHtY2P2W/+92lbunylHTx0yA4cPGRZa9bZO2PG2mPf+5ENvfZWd7wKUpdO7d3306dP25/+8oKb2kTJEvq6cOGCpYvRDz3upvZ494OPk37vt77/E/deff38v35TqJ8V6Ze/+5Nb1o9+9ouE35Ps5584cdLGjZ9oT/zoZzbqptHWfcBIa9NtgPUafLXd++h37dnnX7Hde/fl+3MAAAAAAEgV020AAAAAAEIU1MxLv9497O1X/1bk9lrzZk3s9Reescee+JHt3bfffvenZ91XpDtuud6Kq8aNGrh9//gPfmIbNm2xZ1941X1FU6ZMGStTumDHRnTr0smu6NbFFi1ZZk/95UX3Fdyvf/ifxIPv6WjewiU24cvJoX+vXrP+a/18JQrJiGFDCmX54yZ8ab/4n9+75JlIe/cfcF8zZs+z//3z3+zRB++xf/2n74amcAEAAAAA4OvCX6IAAAAAgBKje9dONvmz9+21N9+zz7+aYus3bLaTJ09a7Vo1rUnjhjZi2GC7+fqrrThr27qlTfjwLfvg489s/MRJtnxllh06dNhKlS5tdWrXsvZtW9nI4UPs6hHDrHr1agX++a/+9Y/21F9ftEnTZmZP6XH6dIF/RkmlSg5BWXGmhSlomzZvtfUbNln5cuVsyMC+Bb78//Pfv7W//+Nd91rn24P33GFDBw1w59358+dtx87dNmnqDHvjnTEuiUnn6E//5YkCXw8AAAAAAPJS6uLFixetCFuzfpNt27nLve7QtpXVq1P7cq8SAAAAAABIY5oaYva8hfb7X/6H3XnrjQlXWfjuP//UvR45fLBNnDTNvZ791SfWoH69Av2saF549R/2X7950oYM7GevPf/nhN+XyOf/v1/9wV567S33+rqrRthv//vnVrVKlai/q6li/vu3f3RT4Yx548UC304AAAAAwOWxfNUa23fgoHvdo2sny6xWtcgeCipJAAAAAAAAFKKzZ8/Zb598xr0e2K+3ffPh+0JJEllr1sVNkigoEydnf56qqRSkjz/7PJQgceO1o+yp3/23lY4zzUuVjAz79f/7d1uZtaZA1wMAAAAAgEQV7OSkAAAAAAAACPP6W+/Zpi1brVSpUvazH//Q2rVpFfrZ6rXrC31vHT5y1OYtWORejxg2qMCWe+DgIfuP//6de928aRP73X//R9wEiaCO7dsW2HoAAAAAAJAMkiQAAAAAAAAKydFjx+ypv2ZPK3HLDddY547trEZmptWpVTNUSaKwTZk+y86dO2/t27a2Jo0aFthyX3rtTZcoIT//1x9apUoVC2zZAAAAAAAUFpIkAAAAAAAACsmzz73iEgkqlC9vP/7Bd0L/36Z1S/d99deQJDFx0tQCn2rj1KlT9sbbY9zr1i1b2MjhQwps2QAAAAAAFCaSJAAAAAAAAArBzl277aXX33KvH75/tDVu1CD0s7ats6fcWLdhk507d67Q9v/58+dt8rRZ7vXI4QWXJDFzzoJQFYnbbrrWTSUCAAAAAEBxQJIEAAAAAABAIfj9U3+xU6dOW2b16va9bz0S9rO2OZUkzpw9axs2bSm0/T9v4WI7fOSI1a5V07p36VRgy50xe27odUFWqAAAAAAAoLCRJAEAAAAAAFDAVq1ea2PGfuZeP/H4I1a9WtWwn7dtk11JQlavLbwpNyZOmua+Dx8y0EqXLrhuoBVZa9z3ihUrWJtWLQpsuQAAAAAAFDaSJAAAAAAAAArY//z+Kbtw4YI1adzQHrr3rlw/95UkJGvN+kLb/19Ozk6SGDV8SIEud9++/e57vTp1rEyZMgW6bAAAAAAAClPZQl06AAAAAABAmpk2c45NnTHbvb7p2qssa83aqL9XvVo1NxXG6jWFU0li46YtbiqPCuXL2+ABfQt02ceOH3ffMypXKtDlAgAAAABQ2EiSAAAAAAAAKCCqHqEqEt4zz7/ivuLJKqTpNibmVJHo37eXVS7gZIZqVavazl177NiJEwW6XAAAAAAAChvTbQAAAAAAABSQMWM/tZVZa5J6z7btO0OVGQojSWLk8MEFvuyWLZqF1v3I0WMFvnwAAAAAAAoLSRIAAAAAAAAF4NTp0/aHp/7qXg/q38c2r5wX9+v3v/wP97sXL160Nes2FOgxOHzkqM1fuNi9HjF0kBW0oQP7hSpnvP/RuITft2//gQJfFwAAAAAAkkGSBAAAAAAAQAF46e9v2o5du61UqVL2b//8vTx/v3XLFqHXq9cU7JQbk6fNtHPnzlvH9m2tYYP6VtBuvfFaq1O7lnv9h6f+YstXro77+9t37LIf/uv/sbGfTijwdQEAAAAAIBllk/ptAAAAAAAA5HLw0CH7ywuvutfXXz3CunbumOdeat2qeeh11pr1xWaqDalYsaL98df/zx58/Id29Nhxu+OBx+yhe++yq0cOs2ZNGrvf2b//gC1ZvtK+mjLDPv9qskvauH/07YWyPgAAAAAAJIokCQAAAAAAgHz607Mv2JGjx6xs2TL24x98J6H3VK1SxerVrWO79+y1rLUFV0ni3LlzNmXaLPd65LDCSZKQQQP62it//aP907/9h+0/cND++uLf3VcsjRs1dJUtAAAAAAC4nEiSAAAAAAAAyIfNW7bZ62+9716Pvu1ma9G8acLvbd2yuUuSKMjpNuYtXGKHjxxx02EkUtEiP4YM7GfTJnxo74wZa19NnWGrVq+1w4ePWKnSpa1GZnW3L3p07WzDBg+w3j27u6lIAAAAAAC4nEiSAAAAAAAAyIdmTRvbuqXZlRuS9Y+Xni3wfT9x0lT3fcSwQV9LUkJGRmV75IG73RcAAAAAAEVd6cu9AgAAAAAAACg4X06eXuhTbQAAAAAAUFxRSQIAAAAAAKAEmfxZ9tQfAAAAAAAgNypJAAAAAAAAAAAAAACAtECSBAAAAAAAAAAAAAAASAskSQAAAAAAAAAAAAAAgLRAkgQAAAAAAAAAAAAAAEgLpS5evHjRirA16zfZtp273OsObVtZvTq1L/cqAQAAAAAAAAAAAACAHMtXrbF9Bw661z26drLMalWtqKKSBAAAAAAAAAAAAAAASAskSQAAAAAAAAAAAAAAgLRAkgQAAAAAAAAAAAAAAEgLJEkAAAAAAAAAAAAAAIC0QJIEAAAAAAAAAAAAAABICyRJAAAAAAAAAAAAAACAtECSBAAAAAAAAAAAAAAASAskSQAAAAAAAAAAAAAAgLRAkgQAAAAAAAAAAAAAAEgLJEkAAAAAAAAAAAAAAIC0QJIEAAAAAAAAAAAAAABICyRJAAAAAAAAAAAAAACAtECSBAAAAAAAAAAAAAAASAskSQAAAAAAAAAAAAAAgLRAkgQAAAAAAAAAAAAAAEgLJEkAAAAAAAAAAAAAAIC0QJIEAAAAAAAAAAAAAABICyRJAAAAAAAAAAAAAACAtECSBAAAAAAAAAAAAAAASAskSQAAAAAAAAAAAAAAgLRAkgQAAAAAAAAAAAAAAEgLJEkAAAAAAAAAAAAAAIC0QJIEAAAAAAAAAAAAAABICyRJAAAAAAAAAAAAAACAtECSBAAAAAAAAAAAAAAASAskSQAAAAAAAAAAAAAAgLRAkgQAAAAAAAAAAAAAAEgLJEkAAAAAAAAAAAAAAIC0QJIEAAAAAAAAAAAAAABICyRJAAAAAAAAAAAAAACAtECSBAAAAAAAAAAAAAAASAskSQAAAAAAAAAAAAAAgLRAkgQAAAAAAAAAAAAAAEgLJEkAAAAAAAAAAAAAAIC0QJIEAAAAAAAAAAAAAABICyRJAAAAAAAAAAAAAACAtECSBAAAAAAAAAAAAAAASAskSQAAAAAAAAAAAAAAgLRAkgQAAAAAAAAAAAAAAEgLJEkAAAAAAAAAAAAAAIC0QJIEAAAAAAAAAAAAAABICyRJAAAAAAAAAAAAAACAtECSBAAAAAAAAAAAAAAASAtlL/cKAAAAfN1OnjxlU2fPtrUbN9rho0ft/PnzVjMz05549JFieTBefutt27pjh917663WukVzK4kuXLhgcxcttmWrVtm+gwft7Nmz7v//6ZvftGpVqxTY52zYvNlef3+MNapf375x7z12uT353PN29Ngx++7DD1ntmjVz/XzxihW2cNky27tvv50+c8b938Oj77J9Bw7YJ19MtE7t2trt119/GdYcRb3toPhJh2t9qg4ePmx/fvElq1ihgv3r975rRdG5c+fsV39+2r3+6RPfs3Llyl3uVSpxvpoxw6bPmWv9e/a0UUOHXO7VKdZWr19vcxctsp2797jni4sXL9qt115jXTp0uNyrltZ0Hfn108+45+Kffv8JK1+EryPF5Tnkpbfesm07dtq9t91qrZtfurcuXLrMPpmoZ+l2dvv1113WdQQAAEDhIEkCAACkFSVE/P2992z33r1WoXx5q1e7tpUpU8aaNGpoRdHSlavswKFDLthdp1atqL+zZ/9+971u7dpWUn321SRbsHSplS5d2u0HdQorwFSQCRKye9++IrMvlcyjzmW1TyXxRJoxd559OX26e619ouCg1KtTx1auXlNktiMVSog5cfKk9ezaxapWSe0YK1FEbWbT1m126MgRF1ioXKmSVc3IsEYNGliLJk2sXetWVqpUqdB7ps+da19Nn+Fe161Vy7790INxg50KGj3+wP1un3vLs7JszKef5XpP2TJlrEZmdWvfuo0N7NO7UAMbebUdFN71OFXpeK0/c+aMPfXiS+58+sFj33DnZ6p279lb5PeN7i+6ZihYSIJEIe3jvdntoH7dS9fkknzdKCwrVq+298d96l7XqlEjtN4N69e/zGtWNEydPcclKQzs3dvKlSv7tS577/797ue6txflBIni8hyia/KevdnP/vUDz3Kyy19PIv4/Edt37bJFy5bZlu077IgS8i9csCqVK7tn2iYNG1rL5s2sVbNmYe9RcrMSn6VNyxZ2zy23RF32/oMH7ZmXX3GvlRTon/8jn2ODypUta7Vq1rQu7dtb3x5XuL+nAAAAQJIEAABIM0tXrXKd6EqOePju0S5RoiibNHOmHT5yxDq0aR3158eOH3edbjUzqxd4wkBRceDgIRfsVlDpsXvvKdQgw8lTp1xAoCgkzRw8ctitS62aNXJ1ZmpU59Q5c1yA//7bb7cWTZuE//zsGffe4hjQUCLTF1Onuu/9evZIaRmTZ860aXPmus5vddBXr1rVdSIfPX7cduze7b6UzNA+4rzatWdPWEBaba9mjdyd+7v37gstO3J05M6cZWS48zL7vfpdjXTfu/+A7d0/x9Zv2mSP3D3avf/rbjsovOtxfqTjtX7e4iUuGWpA7175SpCQXXv3pBzM+rr4RI5gUhUKeB/nBDuL0j4uzOtGYdD9auK07ATMm6++yrp16nS5V6lIOXT4sHvG0DPFsAH9v/ZlHzl2zN3fI5/7ipri8hyiBKYzZ89alYzKViUjI+p9pV4SSVd6dlVit092UHJC9WrVXEKLEna37dzpvjZt3ZorSSL4DLph8xaXSFg+yt+p/vdqVK8eliAR/Fm1qlXds69bpwsX7OChQ+5n+tqyfbuNvvmmhLcJAACgJKOSBAAASCtZa9e5772v6F7kEyQUPFLHerRArKcOve898rCVZFnrs49Z+1atCn0U5pUDB7qvoqBhvXoxj62C7JpyRKPRonWU33z11VZcabS1Opmjdf4mU269UsWKNnzgAOveqZOVLXvpzx51UmuU7P4DB3O9d1dOELNalSouEJG1bp0L4Ob6vZyO87q1a+VKdPDLuKJL57C2pMCTRvhNmjEzO0lj9Wrr1rGjfd1tB4V3PU5VOl7rFZSatWCBCxwN6JX7HEuWH/FblILjkfIzKhmJJTlqxLaq9hSV0v6Fed0oLDt373brrPsgCRLR9k9OQlbdupdl2e1atXJfRV1xeQ7x1Wfq1ambcIWJeMZO+NyWZWW5BIUrBw1yFWSCSSKqBLI8a7WrBhKkf+/JqWbnn0HXbdpkHdu2zfUZ/jkzWsUc/7Mh/fpajy5dQv+v5+rxkya7pHNNpbN52zZr1rhxwtsFAABQUhXddF4AAIBCoPKnUhw6hnxnabRAbDrZvjP7mDVt3Ohyr0qRUZL3iQI0qZZLV7UGTUMiGiXXq1u3sAQJyaxWzZWxvunqq3JV59CIQhnUt4/7riSJaPxIvfoRnerhPwtff1X9GNy3rxtRKNt27Eh6+1Ayr8fpeK2ftzh7Sp1e3bvlu4pEeJWGojvdRiqjkpF8sFNTrhSVkevF8dz2z8lNG5W854uCsGPP7kJLkijMZSO6WAkHlypMZOSqMBGLEg+UIKFz/YE777AuHdrnuhYp2VsJvCMGD8o1Rdy58+fds+KA3r3DEvtzr3P0ZJrgc2zkM6jW6ephQ0PXoa08gwIAADhUkgAAACWe5sX93V/+EvZ/fi5Xue26a61z+/b2+vtjbMPmzXbbdddZg3p1XbBV/1aJ/s7t29mt117rfn/tho22fvMm18Gk+XZPnDzlqlI0ql/f+vfqaS2aNo25LsdPnLD5S5bauo0b3ZyymotdJVH13m6dOlrLZs3cSKK//v21sE72//zfJ0P/VnnW+26/zb3263z79ddZp3btcn2eSv/PX7zElXXVumrKCnV8D+7X143yijZ39ofjx7u5cEffdJMtWr7clqxcaXv37Xcda40bNrBRQ4a4ErqpWL1uvVumAuEnTp1yUxK0bdnChvTrl6sTUvvAj6qScRO/dF+i0VE3jBqZUOniOQsX2YYtm+3Q4SNuZFjlypWtVo1Ma9m0mTtevgNTx/Plt952oz2/+/BDUdvRjPnzXKfl4aNHXel7tZuh/fvZzPnzbfLMWW6e36uHDSuQ/ak2qjby6D13W+MGDdz/vfXhR7Zmw4bQ76iN+qQAtZ37b7/NTSMx5tPPrFXz5nbfbbfmWq72wco1a9xINgVDNPq2UsUKVqtGTWvfupX17No1LLEglfaeynZru7R93qq168Lafc+uXez6kfGP+Zr16932ZVavlnSAx3c6a2T7FZ07u4oPKomsbdYc0kE7Q53q4R3Uahfan9F+5mmqBI3SVWd45LVhzqJFtm7DRtfJrfLIlStWtMzM6taiSVM39UiilTWitR0/17lKeffu3t1GDRlscxctdhUt9Ls6pi2aNLFRQ4ckHBAInhsr165x5aF1zh47ccJd26pWybDWzXV+9426zNfee882btlqd95wgzVq0MBmLZhva9ZvCO1znV+D+/bJleiiUtGvvP2O1a1Vy7790IOuMohKWyvYoeOvfT9y8KCY083odzT10tKVK23Pvv126vRpN3KzQ5s27vMqBPZzMtfjZPdDSb7Wx6MqOLPmL3DrWBBVJHTO6dxTcKlOrdpulOzcRYvc/tR5pHNAVV0aNagfdV1WrV3rjtnOPbvt2PETLtCk63vzpk3cvclPmxONjuGCJUtt49atoXVQIpaui7pm+eoBeY1Knr1goU2cNs2VZr/5mqutfevWYes4e+EiW5GV5a4NFStWdCPJrxw00NZu2GAfjp/g/h0sn+7vZzp+up/p/Fi8fIUbxazA3xOPPhLaLrXRhcuW2/LVWa7Cjn6uaV06t+9gA3r1zBXc19Qv//u359zx++kT33PbHOm9Tz6xlWvW2vUjR7h7SkG1u9OnT7sKJFq27u+6nysQObRfv1CSRDIJdqFtKVvWfvr9J1xb0DOa7ge6Ltx/x+3WsmnTQj23ffvQPVDHaPuune4+W7lSRdeOhvbvH7cNxpPMdWH8V5Ns7uLFoX/r3qAv0fFIpCqA7iV6Rtq4dYtrqzqf/Ps7t2vn7mOR7Ukj6H/156fdSPcff+fbdvCQntsW2qZt29zxrlmjhrtO6DhHyu89Tcd4/pIl7rjrc3VfrlOzpqsEpXYbbNta9vhJk0L/nr1ggfvydD4O6tMnpf2Q6LLVTn7956ft7Llz9i+PfyvqdiXznC0ff/6F+32dd1d07mSzFiy0rHVr7eDhI5ZRqZK1bdnSfX7wvliSnkNiJbbG+v94fGKtqrwle876z6tds4arPjFh8mRbu3GjOy9yVSwLVSWqG3UZ+rtGyWKR9CylY6oqFVpuqn8vAQAAlCQkSQAAgBLv2InjrsNKo1bVCadAY3DaBh/M9B3s6tAcO2GC6xytXaum65zyJbwVjHnzww/da3U8Vs3IsIzKGa5zSWVR9XXXjTda+yhzT6sTVmVYFYBRR5M6YkuXKW37Dx5yI4/0eQp0Hz9x0q2vRhXp89TRps/yWrdoHqVMbO5OvCmzZtvU2bNdR5c6BrXNGmmvAJLW884bb3Cdn9E63qpXreYCLAqiK8ijoM/eAwdcAFNzfn/noQddMDlRCrp88Oln7rNF21S7UiV3PBSQUKfuI/fc7T7H0+hiBTl35Ixq1D7xmjfJey5mdS6++/EnrpNUc/pq+oZSpUu7ALUCswqODuzTO6F9qZ+9MeYDF1BRKW8FZ9UBO2PePNcpWSlnJHTke1PdnwqKqR2qTdQLdHRqO7QfFLzXcVWQw3eetm7ePHxUXJTtUCly7RM/UlRVDbTOx48fdyPgFFgLBrRSbe+pbPfp02fctum92n518AanxEnkmCuhKXv/nXOBl2Q6dH3nsvaH9mmbFi1cIF1tVhUpopVEjgzG7coZNezbWzRHjh5z36tVyZ4rWjT9xhvvj3H7W53Y2k/6ruO1dfsO27ZjpwtWJiJW25HdOSPZdQ1UAE9Tj+g6lFmtqu3df8Bdh/YfOmTfuOfuqIHPWJasWmmfT57i9pvaSK3MzNCIRgWglBT2rfvvc8Hd8PXZF7pG/+XVV12b1vzlbv7sw4dt2pw5Lhh01dAhYe/zbbxWzZqhxCFdU9Se9+3f7wKCr733vgsORya46Bx+95NP3H7VNipopWCkrsNKdlq7cYM9Mnp0aF2TuR4nux9K4rU+EfMWL3H3YwU+C6KKhD93dSy/mDrF3VNU6lxf+w4etI1bttir27fbg3fe6YLwQes3b3ZJBrpWaN/pvD17TufQYRfQV5LYY/fdm+t81r5WIpWm0BEFEBXcOn/uvDueukaoeoBPkog1Klnt4+PPP3dBf/3uXTfdGDYtgxIvdG3QMt1zQ61advrMaVcyXddrP6d9ZLvw7UVtSueCzgklj+n8UrDcb4+Wq3uCkifUbtVmFSjed+CgTZoxwyXmKPkuGKTz1RFUtSPWdeLS7xTMPdGv6z/GfOCuW376ipMnT7pEQSWgVK7s78N1k0oiEC3rg8/GuyRDXTO033TM/H20MM9tJQK8N+5TN42WKHFWx/nAwYO2bFWWa4MPj74ratAznmSvC6XLlHHrrH2i5ybdP3Qvi3z+imfi1Glu+Tp22o96VlICoNqjvvTsEkzmEe0jBWyVqKZzd8qsWZZRuZK7Rx48f96974PPPrMKFcrnuo7l556m8+f9cZ+6+6ySZJSMoWOh7d/55VfuHnHrddnJ0XL+wnm3H9Rm9RwQfP6K3EfJ7odEl619pXtilYzKuQL+qTxnByvcqJ0888qrbh+oreh4qH3PW7LE3aPvvPFGS1Rxeg4JJRxEJLaGpnBKMunKJ98ky1+LlPiga4WOuRJC9fdK8Hqh67faUvY6R0/sqF2jRq7kUtF5ffzkydB1JtW/lwAAAEoSkiQAAECJp86+R+4e7QJg6rjUaOEbrxqVq2PLdzqpg7Zvjx42bED/UCe9nztWnZCqKNGyWdOwDm918M5euNC+nDbdJs2cmStorE7L9z4Z5zr8hg8caH2u6B4KAGvZrlPzYvbvtmjaxFo0HW3Pv/GG61y/YdQoa94k9/Qgfp01MjBy1KW2QR3k6ki9cdRVbtSmX88vpk51I7eUsPGDx74RFojwnZcaVaYpSb7/jUdDwRQFfV5++x3XaabASXCkazz6zHfGjnWju1R9QNMc+CQVBcreGfux6wj8dOKXdm+g8sGDd97hEiRe+Mebbh10DBOlTl51FmvfXjfiSuveqVNYh6E6En0Q59L/RU8u0Dq++eFHbn/37tYtbETdkhUr7KMJn7vEiWjvTXV/qnNWHdYK3Oj4BqueaH1+/5e/uo5gjdCLTAS41OEbvi5qSxqNrk52BdauGjY0LFlIQTKNyA/up1TbeyrbrVGi+vrds39xHex333Jzrs78vPjP0Hnx9tix1r9nL2vaqGFCyRKRx79d61YuSSJrXXiShCuJfO6cO5cjj7fv5I4VPFSwVtstvtNbx3nMuE/d8VFVkv49e4YCU3Lg4CEXuA/+X9ztiNF2gtuoa2HHNm3cOeWPq4Koah865xTESabctz5L56s69YOBHXXmvzdunAs2LVy+PKxqgAJTasui67JGuaoSi29/0+bMdUFataHIJAkfAFZyhIJPjz/wQGiaBSWhvPz2224/r1izxvr16BF6nwIXSnbS+5UEc93IES6QLkr8UUKQgjRfzZhh140YkdT1OJX9UNKu9QlXkViQU0Wid/6rSASveQqG6dqvoL4SDn170HHVMZ8wZYoLvAWp/d97661uH5Qrd+nad+rUKfv4i4kuuXH2/AV27Ygrw96nqg+qhqH3X3vlcLeP/HVG66DjHBw9HW1Usq65CkzpmtKhTWu7+eqrw85zXWfe+Whszs/buHuZP1/Xb9psb330kQukRw+WZe8TBcN1HdW57gOtui/q+qRzUOe8vuseqfLvfvnap0pIUPKcrvWaoujS/s7elgYxkhF0nikYHy9Ammy7cwl7H3zoAqo9unS2EYMHW6WcYOeyVatcgoO/diRTScJfS7S9SkjRfccH4oOJdoV1busz3v1knNteveea4cND92XtRyXQqKrSp19+5RIlEpXKdUHXWd07fvPMs+7f99x6S1ggNRFqx3p2jkyO0X1F7VXPunodrPLj26qCt6oAo8RL3X99wuP748a5a/2iZctzJ3uleE9z7fuDD925quCvqjT4Z3IFhd/68EMXrG/bqmWoco/uzapi9punn3FtQJ8VawqVZPdDosu+lCxXt0Ces9X+VMVF9CzXu3s3V5XFP+P6c0ttMLvqWHiCQXF/DtF9VfdXrWNk5Qc/hVMylSQyc65lut5/NH6C258N6tVLKNnD7xeflKFzQMdM1SmCSRL+OVPndmQSaOQyIqlNq634pPxU/14CAAAoSaiVBQAA0kaowytK55HvdBIFRNVZHAwo+Y5yBdQUyA0GjEUdcurk1O8poKHOQU9BGnVA6f9uueaa7HLugUCI3qMASIe2bUL/p04sjdyJtb7BdVYQItgBp9Fx6hxXsOWBO+4IdY779VRJXY1AU8epOuajjezWqK67b74pbPSsRjG2yalWoFKsiVJATB23Wk/N0RsMzGsUsTrl/IhelctNZIRXXlT6XAGGVs2bueMZOaJKy1M55UQ+a8KkyS6I1LVDBxcoC5Yc7tapk7Vu0cKNvPWjfAtif4bKhkfpnA1VPIgx7/ruGNuhgIjapoLD6iSPXFf9e9iAAWH/l0p7z892KzimjniNMEw2QUJ0jPwobI2+/fu777qEEgXXFHzxo/wSmeNZlTnUbtRpr4BprioGNWrk7vzPCQBGlkD2wUpNgyIKvPiApY6XgrsK9qusemQyRM0amS5pK1Gx2s6ZM2dc4FJUzvqWa68JO66q1OHLrydzfvt9pfdHBnXUgd+jc/Z55gMxkeebKEFCgargedq/Z49QB76fwiRyG1U2WiXrfYKEn85E0yNlb0f29npKxtB7lfSjUbw+QcIHNxR4FU1FE2zTiVyPU90PJelanwiNaFZgSglnBVFFInhv1/XITY+QE/zx7UFJDLJ9585QYo6n81ABqGCChGi0scrBy5794cdMI7KVIKHr1EN33Wkd27YNuxbrHqHrY/AaGzkqWW1MCYAa5a3EO43Sjjz3p86Z4469kgnuuOH6sPNV9zZNC+Sn7YlVsUHXKO2T4Ch3v65jP/8ilCChoGpw+bp+6FnFr2u8a2W8yh6R18hU251Kz+v61aV9e5dwEAzWdunQwQWlffJaZGJGPP56LsEEieB+KsxzW4FinyChZJ1gm1H7UuKMqhwoWBrZdmPJz3VBU07oXqFzM9kECenasWPUajdKBtAUYNHOJ3891zVXlVR8goTovPTnYeT1PNV7moLBeibXvUXPPCMGDQp7Jtex8ImRsdq+2m+sBIlU90Miyw4lc0a0p1Sfs13SZ841RNeBq4YODXvG1bnlK1YoQanEPYf4pIIoia2p/P3R94orQtujaYR0jdd0PkqGU5JPvAoTkZ/nk8SUUBN8HrmU0Jt7vS4l44X/TO9XVRr9PSNK3vDPP6n8vQQAAFCSUEkCAACkDR/AjDWdgqi87MicQFksCmhqdKk6qtTJfO78OVcFQh2v+lIHXbCzTXPvakS+Arg+eJcXlZ1WB7vKY6ujPJpY829rvl/p0717rkC4uOlDatd2AeNgJ6SSOXwnvJIBopVqVRl8CXYox6Ogxcx5893rkRGJJ57WUZ+l3z109IhVqlQxzw7hvGj0oShAoVFkkaOtIqkDcW+UKRTUgay5khXoUeWFaNShqzK+kR3b+dmf8UaDXerUrRN3xHlwVJxGA6ujVZ3fStRJpoRxsu29ILY72aQYT9utkeJzFi12VT7UGa/OX5US1tfnU6a6agUKigQDYGp7OueyP7tOaFmai14jWPVeBQvCSiJHWUe//ms3bAh1VmsfHT56JDQnuap4qDqHp9LZcvjYMTeyPNo5m4xYbcd3wOtY3TBqZNQ24CvmlK+Q2PkdPH8UoFHAbf+hg27qFL8snX9SpkzpqNcvBVKjVRS4kBMU0HoGrxvB6U5GDhkcNdB+4bxvXxXC2qVGr4tGa0dLMPLBGbUZfflAbCLX41T3Q0m51idaRUJBYZ1bmt+8oPhzTVVDou0HVXTQ8daxOHrseK42o6Cykqr81Aj+mPnknOAx0880El9GDR0SNjVG/HXMPoZKhhr/1SSbu3ixa1+jb7oxLKnDUyBz7sJF7rUCnNHO14b165ktyU7KCCYbBO9nVw4cGHXqH5X7Vzt197aISi1eg7r1oganL10D6yQ11Uaq7U7HRQE+JQtcPXxY1M9s1KCBG+2u+16iVXeC55YS0YKVP76Oc1vtS1OFqG1eP2Jk1MC4tkXTQGg99TmJJBalel3I6zk5EXrWXb1unW3ZvsMlPerfPsCr+5v7/NJlot6bFKRv2qhRrmVqKopo96VU72l6ntG6KEg8KMYUAhr9H63t55UglK/9kMCyQ4H0wPHJz3O2b/+6hihBIpoLKdwListzSKykAl9honyUChPxKKHk8Qfud0krumb5Z3K1OX2NnzzZhvXvn+v+dzCnClL2umTvM123lTim5x3do/y5EaudBJ9jFy5b5ipQiJu+7NCh0P2sa8cOYcc62b+XAAAAShqSJAAAQFpQ55Hm2I49Oj+7465ju7a5RpR66uweP2mSLVi6LO5nBUfAq/NJUxjIoJxRmYmI1XEXbZ2Dndl+ZKA6HjVSKJYyZbM7Z8vmfA92jlevVi3m/NMa9Sqa1zwRCi4r2KhAkp87PZLWtUzp0qZuOj9tReQ6xdsP0WgkoAJQ6px++uVXXElfjabUOgRHyXmX5nnOCBvVpk5Odbx2btcuZnDC78PcncGp789LHeFRAvFxRrfFGnG+eMUK971n1y5hSSjxpNLe87vdeQXfEqHjO6RfX/elz1ElCJWmV1vUNrkS+ZUqhwXmNaLTl1YPBpU0olVJEups9kkSlzqoc0/LolHZfsSlH3WpNp2RkeGqxahzWiOVg8dGx1id4UroeO71N9zPVWlCoyIjK3gkIlbb8cEQHZNoI4TVzv1xqV0jscCvqANf5bj98vMqQx15/erUrm3UQMn+nM5+7Ztg8NCPfFXAp22rSyOOw97rtyPQvlasya4OoVG8sQLbwc8pWya83HRe16HU90PJuNYnYv7S7CoSKneeStuOeW/PWdee3bpG/R2d10oGUBCqfPlLAUSdrzpmSiKLJ7PapWOmoKcSxzTnfbeOHRNeTz/NxFfTZ7hpHRrUrWt33nRjzIo5WevXucCq7mWxEqf8/TKycoK/n2m7VXkhmuWrstx3JW+qakb8thOYhuT0aRdY17IVwIsmdB3PVd0itXbn78Oq2BHrPqxniGSD+0ra0bGUeMeysM5tJS6ePnPGtc2PJkyIuVy/T6IllUTKz3UhP4mpoiDwuIlf5lnxQkkjQX6/+mktIsW6L6V6T1N7Eo2ajzUVlz+3ykTs80T2T6r7IZFlR6sUlp/nbP+Z7Vq1jPq3j5K1tB16TzLVvYrNc0iMihf+GlY3xtRp8ejvCFVrUcL97n37bPPWbS6ResOWLe6aoyS76tWquutZ5HFQ4k7wGteuVSu3bZr2LXeSRPg6++dY0T70+1HJZUp8UBW57p0755r2J9m/lwAAAEoakiQAAEBaUIlndb7FGmXoO9Yi5zsOUgfcyjVrXMfegF49XVlXdRqqg1udaAp+TJ87NyxwsHXHdhesUKAv0RGn8TrB8uos1WgjbafWId5oID+qKthJ6ctgR5s3+1KiSc5o+wQDEVof0Yj8WBQk0Jf2YXB9tB17ctYp2Q57dVJ+4957bNKMmS4QobK3+lIQVMHqkYMHRWx79MQDBdh9J2IsGpkcLTiT6v6MVdUikbYRazs0uk7USZqoVNp7frY7PCkm9SSJIFUp0FfPrl3diNAX33zLBWnVFoJJEqEy1zmjLYMd1J+UmmjrNm12662fxdrHfhkKJvz0+0/EDL5EUmBCc3JPnjnLVqxZExpxqH2saQDU0Z5odYl4bcevd6y2rCSN06Ey64mNJFSQ+dV33nUjFJV80KtbVxf81TL8fnzyuefd9SZ3O8len+ZNol8bYgWMfPvSvOvRRszGGmmp+chFU23kdV3U+gcDRnldj/OzH0rKtT4vShicVQhVJHxgSOd5cPqUyECfEiR0Tiq5wZd91zFTu1cVApVJb9ygvrt3+GP20ltv2bYdO8Omc9mwJXt6Ah3nRM/x7FHJJ9y14diJ7IoyQ/r3ixt03LTF33ua5nnviTxGwUBkrKS40L05gfMhuF99e1VFjNhTAuQk68U4d5Ntd/4+HG9dDxw6nPSzggKkOoeUtBE8xl/Xub0x576swKk/HrHofhCrfQfl57oQts5JJqYqmfC9T8a515pKRwnHum+pgobOEwVsn3n5Fffz4H7SeigIr7bUtFH0xJlYI+dTvacl0vaPRGn7wXWJlYyT6n5IZNm+KkFkdYP8PGfv3pezD5tE34c7c57JtK6JXu+K43NIrmtVnCktEqV9reuYvlTBbOfu3fby2++469ySFSvDkiT8fo5s4+3btLZpc+a4qiSq+KOp33wCbqxnULWNJx59pND+XgIAAChpSJIAAABpIRR8jdJZHRxN6EvsRlKgRAFjdbo9du89oTl6IztHs5dxqeNKI0alRmbuctfxxCvpH1xndcLVrVU7rJy1nzYkFgWLFKzQe4Plpf1I11idghrN5INRiZaz9iPrq8bp7PTBS3VeBsv5avs0klb7PJXyr+oovP3669y+2rxtuws8L121ypZnZblt+faDD0SZ4ze8fWhklcTrIPQd1LkDVantTz8KWMG8yNHWCjT60WHR5l2Pth36DHWu+32SiFTbe362261/Tpn2VKfbyGsEoTql5y1eHAoQhT43xshnbb8CjZoPXnN5q436OaUbRGxfaORhEsEET/tXpaevvXK4bd2x01avX2eLl69wUwCoY/2JRx+NmRAQFK/t5DWVSaJlxIPUea/AhEaja27xSFp3v68VtPB0Xvt5yWMFEGOtz6VrefT1dBU9jh1zwRG1schrUbxro46zRI5yz+t6nOp+KEnX+rwsWLrUJQooQaKgqkgEg1nRppSITGzQSFwfNNN0F2qDzRo3tgfuuD3XOatg5Padu3IdmyNH/D098RLs/hg3qF/PenTpYh+Nn2Afjp9gj949OmYClEriS/UE7j25qhjl7BM/fUw0/tmkWpWqSZ0PfuR1tKmg/HkWK8Em1XYXeo6qHnufq3qCW68kknp8gDTefirMc9tv1z233OJGbheE/FwXwitVJZccNXHqNBccv/GqUXZF584xnxd0bIPPeb6t1ombdBN9nVK5p+kZyk87EP+5LqftBxI3wqblitHOUt0PiSw7VnWD/Dxn+8SlgnwuKC7PIf55Wvsy8nk6VoWJ/NDfl0oO0XNdrmfQGFMUaX2VxKV7la5XJ05mt93IKZYSSbIpqL+XAAAASprkeu8AAACKqVijv4OjCdVJFGvO3Y1bc0bit2wRNWC8Y/du15EU+RlnzpwNm/M1EVqXUMdljM4uv84qJx9eIvdiKAgZy6IVK1wgQqPOgmVdfedlZNA7P1Mh5EzB7Dre4gXPfNn9/HaWRqORyxqRr07rh+660/2fjpWCYHkFBvx+9CVsox33WB2Tqe7PSyPbcr9P663jHit4GW1U3NnAXNiJtsNU23t+ttsHtxUoSabqSjIUiJDIpJudccpct2/d2n3XlBv+WKvTOnKEdrQpEZKlbdco66uHDXPzWruR58dPhOYwz0ustqP2qxH30QJ5+Tm/N+aMdlcJ52g0WlKqZFQO2+daT7VJdfJrhG3c9YlRrjvmdvhgQ4xklVjngNZn4bLlucq+J3I9TnU/lKRrfTza5zPnz3PXYk21UZD8tfv8hfMxf2fe4iXue3DqidAx69QxajtRgEj73Y8G9vz+Pnsu9n6PFByVrGkdBvbp7QLVb334UcyS/L6daj75aBRkU5n96BV54gcig2K1H903lq5clet82JdzHQkm7AQtz1odauuR945U252qfmSva/b3aAkSPnkw1nUhmrwCtoV9bvt97+9LBSP160J2xZPjuRLMEknMUGBc51G0aUv0Wcty2lLupLf413OdB1q2C2RHJH+mck+7mLN/4u0j7YPV69a7z+zYpm2uqjW6b0WbhiA/+yGvZYf9HRNxT0z1OVvXEB1zPWfESta6lLiafPJRkX8O2efvrTVzTWVz6W/GgkuSkHPnzkd9BvXJn9H2i6ZCCT2DBqqtRU4DEu85tiD/XgIAAChpSJIAAABpYWecAGYo6Baj41786PHzOR1cQergHjv+0nzSwU4uzTsr6hhUUCQRCpooKKCAaayRbrE6S2tmZndsK6gareP9wMFDNmXWbPd6YJ8+uUZ2RwaEolcpSDwIUbNGZljgMpKqFai8q+bD7dklfD75WNUd8sOPatO+DR9NF33bqlbJCCUFRNL+/XTil6FRm8FgQ372Z7x5qX0HabR2HBxxHhwVpw53H4j2oyPzkmp7z892H8wpl64S9MlWYtD5Ei9A4AMC6mQWlRAOn9YldrCsXetW7vua9Rtsx67sdhBt2xKZNiEZGZUvBRgTHXkfq+0ogKjjll3COvo1JRTATCLIGGon53O3k83bttm8JUviJzrEuOa6AGPMct374r43VnKVr6KiUaXRzFqwwL1XwcEObbITYxK9Hqe6H0rStT6eBcuyq0ioDHpBVpEIHm+1i2j7YenKla4igoKPXTt2CP2/vx+fi3LMdL5MnjHTvVZ7UODI85Ud/MjshNYxYhqhKwcOdNcgHYe3Pxobdb19gsHOnGtO5Pnx6Zdfufama2VkgDORZ5rQ+bBnd8wR8QoWa5qlxg0b5GrrFh6fC+23qbOz21y9iLaTn3bn94Wq7ETS+TNh8mT3Wm0rWlJfLHkF6Av73PZtSVWKCkqq14X8VEM6feZ0qF2eP587qWfGvHkxE1HjJTAHA9kuMTRwHqZ6T9MyQudWlHuBPuuTiRPddUGJDsEpHw7mVJyLVZErP/shr2UHtyfyvak+Z/t1iXe8k0m4Km7PIbEqiOlZ0leYiJyeIxZN6RQrmdrTPUrrIx3aXnoG1XVW98dYz4/tWvlE3fUxnzODz7H5SdRN5O8lAACAkoYkCQAAUOK5zqOcgFu0jrdEAkI+CKGOxmDAXB2bb4z5IDRdh0YHBYNAmudXAWoFZFReO3I0zvZdu2zG3Hlh/1cuZ0STOvz8cmOtc2TJ62aNG7nRUio9+8WUqWGdhus3bba/v/uuW5cBvXuFzQvuR3argzZWefW8SkdH40egasTrslXZI+j8tmnagw8+G+86Z2++5urcI/NT6JwVlTLXqLnIfa2gwfvjPg2NKPYjx0LzPJcvn2talJbNmrvvM+fND+tQ1zQcb374Uagt5J57PfX9Ga/Mb/wEitij4nw1hC+nTw910nra/tkLF7rgSX7be36224+SP3biRKgcdqI2bN5if37pZReE8KWnw0ZvrlplL7/1tluu9o/af2Rp6Fj7XMFVJZ3ovYuWL4+67goA+n2STAf151Om2qz5C0Jl1z39+71PPnGBGgUplTiSiFhtJ6/qC+69KZzfvp2oSoAf7a3jr1LNCv6WyQm81I8I1uY1gtsdk7NnXTArGPT0I1/LxRnpHGskaqf27UKjqDdsya6U4o/d5JmzXFBYAbRbr70mrOx7ItfjVPdDSbrWx6KgmK6f2taCriIRTKZRoEznkw9U6fvCZcvs4y8munvMrddeG3Zc/TGbt2hx2L1i09Zt9vp774eq7zSIMke86DqqYHNwv+u1yukH73XRjqGCb7dcc42b5kFTZnz8+Re5tq1Vs2buu645wSC6gu+6b+raLLVr1Ai73vv7mRI74lUD8PdmXX/88fbBvnETv3SBRbUvjSgOyqyWfY9csnxF2H7TOr7+/pjQfot2DUq13fl9MWfhwtAUI6J7lu5He/fHn6YgGq3L3hiJWF/Xue2PgdqpjkNkkFf7d+HSZTZ30aKEtyvV60Kw4kmDJBP91CZUjUD7RUkyvg3o3J86e447T3wQPvJ8yuveFCuQnZ97mt/vuu775ARfCeLdjz9xCZF6Thg1ZEjY+3yylJJ9oiWg5Gc/5LXs4DZH7otUn7PzSqbSvfHSdBSJt4ni8hwSrfJa9v/vi/ksHcuSlSvt2VdedVWLIqsD6Xjq/1977z13X9IUT8FKI/66pyQWVSmLdk7rZ1pfncPR9k/4c2zdQvt7CQAAoCTiSQcAAJR46uRTwC3WKMNQh16cUZed2ra16XPmus7uF//xptWpWdMV7VVHkgIR6nBWJ2hk559G8V8/ckQoqLFu0yYX8FRH6oHDh916RU4zoQCC5k7X6Ne//v01q5szV7M6xa69cnjcTkh1ZN046ip7e+xYF+RYmpVlNatXd4GTo8ePu9/Ruo4cPDipzktXJjeBgEIkrd+gvn3cvtM+UCCySpUMO3DosOuo1+gkBWHatswuJ5vfkflazxWrV7sOSwV6MqtXs8qVKtuJkyfs0OHsOd61b68eNjRXB6UC4ZHlawf27uXm5FUH4gv/eNMFeNThqrmjdRy7dGhvy1ZlJd3xH29/xho5HrZPovwsVge6XDlooAvqqQP+1XfedXNF6zgo6KwRbNruHl265Lu952e7tVwlJGgdn3rxJRf8U8d+6xYtbHDf8BGvkXQM1ca/nDbdfamyh87102fOuIQDH3RQwsFt110bNiLVBzDjlblWNQkFY33nd7Rj5gMiySRJrFm/3u3jL6ZOzV7nKlXs1KlTbh9oeer8VzA1UbHaTigpIcY1zicfaL/EG8kaSdMGqF2p3PWTz79gtTIz7cixo65N9e7e3Y1S37ZjZ672cGkEd72oy42VROHfp30cc+RrjCBLu1atXGf/sqwsFwR3U6ZUrBi6PyiweNt111vD+vWTvh6nuh9K0rU+lgVLl7l16t+zZ4FXkVD1GQXFNCK/e+dOLgipUdP+OqLzVftTiS/BagjSr2dPd69QlaenXnjRBcROnDrlEuCUVKZrooJskeeMAlw6pgo+Tpk1y2bNn281MjNdGzp05Ijbf9eNuDJXhR+1Vx3fYFLY6JtvctdXtclaNWvYkH79Qj/v0aWzC14p+PXG+2Pcvaxi+Qru3qP7Zq9u3Wz+kiW51i90P6uT+34WpKoeqqyjdvvCG/9w+0yBWn2egqu6Dtx104255r1XNQ4l1Wm//fH5F9x9UAF57eve3brZ/kMHXdJarPtaKu1O7Xj56tXuGqVkN90rSpcu5ZIjdM1q0bSpbdyyJVewMx4fVHT3wjjVJwrz3Naz31ol1WRluXuA2pP2d+kype348RNu+im5YeTIhLcr1etCXpWq4lFb1vk0fe5cl6io6i1KoNx38KBrS9ePGOGqM0Re831yYeRUGmHrFCuZNB/3tKH9+9mGzZtc+3nmlVetlqvEUMq1Cd13lbykth+ZuNu4QQN3z9A6q+3rvC9dqpS79lzRuXPK+yGRZYddRyKqG6T6nJ1XsqJ/rqlTq2bEVE8l5DkkRrW6yMo/idBn6Ph89tVX7sv/7XEyZxo5n6zUsW0bu+mqq8KuzfGSn0XHXFPfafqj0DNojH2bXaUjfCqPgvx7CQAAoCQiSQIAAJR48aZtCI4mjDe6ScGDB++80z6fMsXWb9rkAjDqxBs+cID169HTvpoxPfszoixDo7wyq1e32QsW2JZt29171WmpjmsFXK7oknse3duvv84mzZjpRjwr6KH19OsfPgIy9+epM+2R0aNt2pw5roNewQwFArp26OACK5HBorBOzRidl+o8VqA5snJAIlRaXAGCuYsWu0DzqQPZQS0Fgfp07x515FRoNGySc2Nr39wwapTrMN2+c6cLCmh0oPa3AildO7S3rh07hnVQxksuUPDkkbtHu07njVu3uioFOu7DBui4X2Gvvfe++72mjRsXyP6MV9UiWBElWmdqrFFxfju+ed+9NnPBAluzLjswf/L0aRcYVkBQX8HEgVTbe37akQJI99x6i02aMcOVVVcFC22zn+4iHgV9FORUoEz7SPtRbU2dy0o+aNSgvnVp38HNtZxKMo72j5JCvNyd7dnL0D5KpiyxAlVa561qq8eOueXoOOgc1VzoPbt2SXgEX7y2k1eA8lJgNfc81/G0bt7cBXqnzprtrjMKEivA1HtUd3cd+u0zz+b63LBrbp5TZiQXaFVSjA8kBaec8W659hpr1KCBLV6+3LVFJaTomLVv3coFU4JT5iR6PU51P5TEa330KhLzsqtIBKq3FBQfzNK5P7R/fxfgUfKCEksUcFRAf1CfPu7nkZRE8MCdd7ikKlV00jVO7f/KgQOsS4cO9txrr8c8ZgpyN23U0FUAUIl7JY9VrlTRGtav56oeBKfz8aOSlWgWeS5rH999y8328tvvuFHtmiqhc07Fk+xr8B2u+s/aDRvt6LHjVqZaGevb4wob2Ku3C8RJs0bR7z0N8hhNrOvtfbfdajPnL3CJgNp+XXvU9jq2beuSKILTjHgK0N57261uOhJda5UgoetrnyuucNv+v397Lmf/1imwdqf/UzufOG2au17q/NJUWN07dXLHd/ykSUmPoPbT9uQ1Yr2wzm3RtfbW6651y1m0fIW77ikJRtXHqlSubM2bNrE2LVpYmyhJpPGkcl3I7zQ7ejaoUKG8LViy1AWFtW26rg7s3dvOX7jgtt0lpgUSD/b4c6NWrZhB+FgB5Pzc07R/s/fPXFu1bq3tP3jI/Z8CwkqkU2JCtCQ83R/uvfVWmzxzplu+njFF52R+9kMiy86rukEqz9mJV/FIvD0Up+eQS8/TiVWYiOfqYcOseeMmtmnrVncOaz9o/+lY6RmjScMG7m8PtbFY+zlyiqLIZ1AlSfhrtxJXoi+jTqH+vQQAAFASlbrohzwVUWvWb7JtO3e51x3atnKdGQAAAMDldiin4oE6t//5W98MK+UOAOlO0wSMnzTZVZEYNTS8dD1Sp+oZSkbQdDy69xR0hQ4AAAAAAFK1fNUa23fgoHvdo2sny6xW1Yqq6DVKAQAAAMQtU/vh+Anudc+uXUmQAIAAVQWYMW9+oVWRSGcqja6pCjRdAwkSAAAAAACkhuk2AAAAgBi+mjHDenXtatWqXsp6PnjokE2YPMWVsVYZXc2HDAAIdDSULeuqHCA1mgpA89cHp5tSCXdN37AsK8tVMBoxaDC7FwAAAACAFJEkAQAAAERx+MgRmz5nrvvSaF3N73zq9Gk7cOiQ+7nmhr7nlpvdPO4AABRUpaKps2fbpBkzrFLFipZZvZqdPXvO9h886OaRr1qlio2+6UarVrUKOxwAAAAAgBSRJAEAAABEe1AuW9b69expGzZvtqPHjtmuvXutQvny1qxxYzfCt3unzlauHI/TAICCc/78BRvQq5et27TRDh85arv37rNy5cpZw/r1rV2rlq66UcWKFdnlAAAAAADkA726AAAAQBSqHnHV0CHsGwDA10bJd8MHDnBfAAAAAACgcJQupOUCAAAAAAAAAAAAAAAUKSRJAAAAAAAAAAAAAACAtECSBAAAAAAAAAAAAAAASAskSQAAAAAAAAAAAAAAgLRAkgQAAAAAAAAAAAAAAEgLJEkAAAAAAAAAAAAAAIC0QJIEAAAAAAAAAAAAAABICyRJAAAAAAAAAAAAAACAtECSBAAAAAAAAAAAAAAASAskSQAAAAAAAAAAAAAAgLRAkgQAAAAAAAAAAAAAAEgLJEkAAAAAAAAAAAAAAIC0QJIEAAAAAAAAAAAAAABICyRJAAAAAAAAAAAAAACAtECSBAAAAAAAAAAAAAAASAskSQAAAAAAAAAAAAAAgLRAkgQAAAAAAAAAAAAAAEgLJEkAAAAAAAAAAAAAAIC0QJIEAAAAAAAAAAAAAABICyRJAAAAAAAAAAAAAACAtECSBAAAAAAAAAAAAAAASAskSQAAAAAAAAAAAAAAgLRAkgQAAAAAAAAAAAAAAEgLJEkAAAAAAAAAAAAAAIC0QJIEAAAAAAAAAAAAAABICyRJAAAAAAAAAAAAAACAtECSBAAAAAAAAAAAAAAASAtFPkmiVKlLry9evHg5VwUAAAAAAAAAAAAAAEQIxvIDIf4iqcgnSZQufWkVL1wgSQIAAAAAAAAAAAAAgKLk/IULUWP8RVHRXjszK1OmTOj12bNnL+u6AAAAAAAAAAAAAACAcOfOnQu9Llv2Uoy/KCrySRKVK1UMvT556vRlXRcAAAAAAAAAAAAAABA+1caJk6fc61KlSlmFChWsKCvySRKVKgaTJLJ3LAAAAAAAAAAAAAAAuPzOnD1rF3Km26hYoYKVLlXqcq9SyUmSOHHypMtCAQAAAAAAAAAAAAAAl9/JnCoSkTNFFFVFPklC85VUqFDevT579pwdP3Hycq8SAAAAAAAAAAAAAAAwswOHDof2Q0blSkV+nxT5JAmpU7Nm6PXeffsv67oAAAAAAAAAAAAAAABzM0Hs3XcgtCtq17oU2y+qikWSRN06tUKv9+w7wJQbAAAAAAAAAAAAAABcZsdPnLCTp7Kn26hQvrxVr1rFirpikSShHakdKtrBwUwUAAAAAAAAAAAAAADw9du0dXvodZ3aNa1UqVJF/jAUiyQJ7cjGDeuH/r1u02Y7d+78ZV0nAAAAAAAAAAAAAADS1f6Dh2zf/oOhmH6j+vWsOCgWSRLSpGF9q1ypont95sxZW7dxM9NuAAAAAAAAAAAAAADwNTtz5qyt3bAp9O+mjRpYRuVKxeI4lLp48eLFy70SAAAAAAAAAAAAAAAAha3YVJIAAAAAAAAAAAAAAADID5IkAAAAAAAAAAAAAABAWiBJAgAAAAAAAAAAAAAApAWSJAAAAAAAAAAAAAAAQFogSQIAAAAAAAAAAAAAAKQFkiQAAAAAAAAAAAAAAEBaIEkCAAAAAAAAAAAAAACkBZIkAAAAAAAAAAAAAABAWiBJAgAAAAAAAAAAAAAApAWSJAAAAAAAAAAAAAAAQFogSQIAAAAAAAAAAAAAAKQFkiQAAAAAAAAAAAAAAEBaIEkCAAAAAAAAAAAAAACkBZIkAAAAAAAAAAAAAACApYP/H3pLKT/rlYOtAAAAAElFTkSuQmCC",
"text/plain": [
""
]
- }
+ },
+ "metadata": {},
+ "output_type": "display_data"
}
],
"source": [
@@ -181,49 +179,7 @@
"kernel": "SoS"
},
"source": [
- "## Input"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS",
- "tags": []
- },
- "source": [
- " 1. `significant_variants_path`\n",
- "- **Format**: RDS file containing significant variants. This must contain some variants that are not in the `baseline_anno_path` input. \n",
- "- **Columns**:\n",
- " - `chr`: Chromosome number (integer, required).\n",
- " - `pos`: Genomic position (integer, required).\n",
- "- **Example**:\n",
- " ```r\n",
- " chr pos\n",
- " 1 12345\n",
- " 1 67890\n",
- "\n",
- " ``` \n",
- "2. `baseline_anno_path`\n",
- "\n",
- "- **Format**: RDS file containing a tabular data frame with baseline annotations. This must contain some variants that are not in the `significant_variants_path` input.\n",
- "- **Columns**:\n",
- " - **CHR**: Chromosome number (integer, required).\n",
- " - **BP**: Genomic base pair position (integer, required).\n",
- " - **SNP**: SNP ID (character, optional).\n",
- " - **CM**: Centimorgan position (numeric, optional).\n",
- " - **base**: Base-level information (integer, optional).\n",
- " - **Annotation columns**: Binary columns (0/1, required) for various genomic annotations (e.g., `Coding_UCSC`, `Conserved_LindbladToh`, `CTCF_Hoffman`, etc.). Multiple such annnotation columns may exist in the input file. The columns start index of this file is given in the `--annotations-start` argument.\n",
- "- **Example**:\n",
- " \n",
- " ```r\n",
- " CHR BP SNP CM base Coding_UCSC Coding_UCSC.flanking.500 ⋯ Human_Enhancer_Villar Human_Enhancer_Villar.flanking.500\n",
- " 1 11008 rs575272151 0 1 0 0 ⋯ 0 0\n",
- " 1 11012 rs544419019 0 1 0 0 ⋯ 0 0\n",
- " 1 13110 rs540538026 0 1 0 0 ⋯ 0 0\n",
- " 1 13116 rs62635286 0 1 0 0 ⋯ 0 0\n",
- " ```\n",
- " \n",
- "\n"
+ "**When to run it.** After a significant variant set exists and an annotation is available to test it against."
]
},
{
@@ -232,7 +188,28 @@
"kernel": "SoS"
},
"source": [
- "## Output"
+ "## Input\n",
+ "\n",
+ "- `--significant-variants-path`: the variant set being tested, two columns `chr` and `pos` with a header. Required. Example `input/enrichment/protocol_example.eoo_significant_variants.tsv.gz`:\n",
+ "\n",
+ " ```\n",
+ " chr pos\n",
+ " 1 100015157\n",
+ " 1 1000291\n",
+ " ```\n",
+ "\n",
+ "- `--baseline-anno-path`: the annotation table covering the genome, one row per variant, with the annotation columns starting at `--annotations-start`. Required. Example `input/enrichment/protocol_example.eoo_baseline_annotation.tsv`:\n",
+ "\n",
+ " ```\n",
+ " chr BP ref alt rsid maf ANNO_baseline\n",
+ " 1 10000654 A C rs1_0 0.3734 0\n",
+ " 1 10100228 C T rs1_1 0.0602 0\n",
+ " ```\n",
+ "\n",
+ "- `--annotations-start`: the 1-based column index at which the annotation columns begin, `7` by default. Everything left of it is treated as variant identity and frequency.\n",
+ "- `--trait` and `--annotation-name`: labels, both required. They carry no analytic meaning; the output file is named `{trait}.{annotation-name}.enrichment_results.rds`.\n",
+ "- `--cwd`: the directory outputs are written to.\n",
+ "- `--numThreads`: threads for the jackknife, `8` by default."
]
},
{
@@ -241,35 +218,25 @@
"kernel": "SoS"
},
"source": [
+ "## Output\n",
"\n",
- "1. `enrichment_results.rds`\n",
- "\n",
- "- **Format**: RDS file containing the following components:\n",
- " - **`summary`**: A data frame summarizing the OR, OR_SE, Enrichment, and Enrichment_SE for each annotation column.\n",
- " \n",
- " ```r\n",
- " Annotation OR OR_SE Enrichment Enrichment_SE\n",
- " Coding_UCSC 1.23 0.12 0.85 0.10\n",
- " Conserved_LindbladToh 0.98 0.08 1.12 0.05\n",
- " Human_Enhancer_Villar 1.45 0.15 1.30 0.12\n",
- " \n",
- " ```\n",
- " \n",
- " - **`OR_blockJacknife`**: A matrix (22 rows for chromosomes × annotation columns) of log2-transformed OR values.\n",
- " \n",
- " ```r\n",
- " Coding_UCSC Conserved_LindbladToh Human_Enhancer_Villar\n",
- " 0.12 -0.02 0.25\n",
- " 0.15 0.01 0.18\n",
- " ⋮ ⋮ ⋮\n",
- " ```\n",
- " \n",
- " - **`Enrichment_blockJacknife`**: A matrix (22 rows for chromosomes × annotation columns) of enrichment values.\n",
- " - **`OR`**: A numeric vector of mean log2-transformed OR values across chromosomes for each annotation column.\n",
- " - **`Enrichment`**: A numeric vector of mean enrichment values across chromosomes for each annotation column.\n",
- " - **`OR_sd`**: A numeric vector of standard errors for OR values across chromosomes for each annotation column.\n",
- " - **`Enrichment_sd`**: A numeric vector of standard errors for enrichment values across chromosomes for each annotation column.\n",
- " - **`annotations`**: A list of annotation column names."
+ "- `{cwd}/enrichment/{trait}.{annotation-name}.enrichment_results.rds` - a list of 10 holding the point estimates, their block-jackknife standard errors and the per-chromosome estimates they derive from. Example `output/eoo_enrichment/enrichment/protocol_example.baseline.enrichment_results.rds`:\n",
+ "\n",
+ " ```\n",
+ " List of 10\n",
+ " $ summary :'data.frame': 2 obs. of 13 variables:\n",
+ " ..$ Annotation : chr [1:2] \"maf\" \"ANNO_baseline\"\n",
+ " ..$ OR : num [1:2] 1 1\n",
+ " ..$ OR_SE : num [1:2] 0 0\n",
+ " ..$ Enrichment : num [1:2] 1 1\n",
+ " ..$ Enrichment_SE : num [1:2] 0 0\n",
+ " $ OR_blockJacknife : num [1:22, 1:2] 1 1 1 1 1 1 1 1 1 1 ...\n",
+ " $ Enrichment_blockJacknife: num [1:22, 1:2] 1 1 1 1 1 1 1 1 1 1 ...\n",
+ " $ OR : Named num [1:2] 0 0\n",
+ " $ Enrichment : Named num [1:2] 1 1\n",
+ " ```\n",
+ "\n",
+ " `summary` carries one row per annotation column, here `maf` and `ANNO_baseline`. The two `blockJacknife` matrices are 22 by 2: one row per chromosome left out, one column per annotation, and the spread down each column is what the standard errors are computed from. On this toy annotation every variant scores 0, so the odds ratios come back at 1 with zero standard error; that is the expected result for an annotation with no signal, not a failed run."
]
},
{
@@ -278,7 +245,9 @@
"kernel": "SoS"
},
"source": [
- "## Steps\n"
+ "## Minimal Working Example\n",
+ "\n",
+ "One workflow, `enrichment`, run once per annotation. `--trait` and `--annotation-name` are labels only; together they name the output file."
]
},
{
@@ -287,9 +256,7 @@
"kernel": "SoS"
},
"source": [
- "**Step 1.** Run the chromosome-specific enrichment analysis (`enrichment`).\n",
- "\n",
- "Compares the significant-variant set against the baseline annotation, computing per-annotation odds ratios and enrichment with a 22-chromosome block-jackknife for standard errors. Writes `.enrichment_results.rds` plus a summary `.tsv.gz`."
+ "**Timing**: TBD (on toy dataset)"
]
},
{
@@ -314,31 +281,58 @@
"kernel": "SoS"
},
"source": [
- "## Anticipated Results\n",
- "\n",
- "Writes `.enrichment_results.rds` (per-annotation OR, Enrichment, and their block-jackknife standard errors) plus a `.tsv.gz` summary. Annotations with a higher proportion of significant variants than expected show OR and Enrichment above 1.\n"
+ "## Command Interface"
]
},
{
- "cell_type": "markdown",
+ "cell_type": "code",
+ "execution_count": null,
"metadata": {
- "kernel": "SoS"
+ "kernel": "Bash"
},
+ "outputs": [],
"source": [
- "## Command interface\n",
- "\n",
- "List the workflow and its options:\n"
+ "sos run pipeline/eoo_enrichment.ipynb -h"
]
},
{
- "cell_type": "code",
- "execution_count": null,
+ "cell_type": "markdown",
"metadata": {
- "kernel": "Bash"
+ "kernel": "SoS"
},
- "outputs": [],
"source": [
- "sos run pipeline/eoo_enrichment.ipynb -h"
+ "```\n",
+ "\u001b[91mERROR\u001b[0m: \u001b[91mNotebook JSON is invalid: %s\u001b[0m\n",
+ "usage: sos run code/SoS/enrichment/eoo_enrichment.ipynb\n",
+ " [workflow_name | -t targets] [options] [workflow_options]\n",
+ " workflow_name: Single or combined workflows defined in this script\n",
+ " targets: One or more targets to generate\n",
+ " options: Single-hyphen sos parameters (see \"sos run -h\" for details)\n",
+ " workflow_options: Double-hyphen workflow-specific parameters\n",
+ "\n",
+ "Workflows:\n",
+ " enrichment\n",
+ "\n",
+ "Global Workflow Options:\n",
+ " --cwd output (as path)\n",
+ " Path to the work directory of the analysis.\n",
+ " --modular-script-dir code/script (as path)\n",
+ " --significant-variants-path VAL (as path, required)\n",
+ " --baseline-anno-path VAL (as path, required)\n",
+ " --numThreads 8 (as int)\n",
+ " Number of threads\n",
+ " --trait VAL (as str, required)\n",
+ " For cluster jobs, number commands to run per job\n",
+ " --annotation-name VAL (as str, required)\n",
+ " --job-size 1 (as int)\n",
+ " --walltime 12h\n",
+ " --mem 16G\n",
+ "\n",
+ "Sections\n",
+ " enrichment:\n",
+ " Workflow Options:\n",
+ " --annotations-start 7 (as int)\n",
+ "```"
]
},
{
@@ -357,7 +351,24 @@
"kernel": "SoS"
},
"outputs": [],
- "source": "[global]\n# Path to the work directory of the analysis.\nparameter: cwd = path('output')\nparameter: modular_script_dir = path('code/script') # override with --modular-script-dir\n\nparameter: significant_variants_path = path\nparameter: baseline_anno_path = path\n# Number of threads\nparameter: numThreads = 8\n# For cluster jobs, number commands to run per job\nparameter: trait = str\nparameter: annotation_name = str\nname = f\"{trait}.{annotation_name}\"\nparameter: job_size = 1\nparameter: walltime = '12h'\nparameter: mem = '16G'"
+ "source": [
+ "[global]\n",
+ "# Path to the work directory of the analysis.\n",
+ "parameter: cwd = path('output')\n",
+ "parameter: modular_script_dir = path('code/script') # override with --modular-script-dir\n",
+ "\n",
+ "parameter: significant_variants_path = path\n",
+ "parameter: baseline_anno_path = path\n",
+ "# Number of threads\n",
+ "parameter: numThreads = 8\n",
+ "# For cluster jobs, number commands to run per job\n",
+ "parameter: trait = str\n",
+ "parameter: annotation_name = str\n",
+ "name = f\"{trait}.{annotation_name}\"\n",
+ "parameter: job_size = 1\n",
+ "parameter: walltime = '12h'\n",
+ "parameter: mem = '16G'"
+ ]
},
{
"cell_type": "code",
@@ -366,7 +377,19 @@
"kernel": "SoS"
},
"outputs": [],
- "source": "[enrichment]\nparameter: annotations_start = 7\noutput: enrichment = f'{cwd:a}/{step_name}/{name}.enrichment_results.rds'\n\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output[0]:bnn}'\nbash: expand = '${ }', stderr = f'{_output[0]}.stderr', stdout = f'{_output[0]}.stdout'\n Rscript ${modular_script_dir}/enrichment/eoo_enrichment.R \\\n --significant-variants-path ${significant_variants_path} \\\n --baseline-anno-path ${baseline_anno_path} \\\n --annotations-start ${annotations_start} \\\n --output ${_output['enrichment']}"
+ "source": [
+ "[enrichment]\n",
+ "parameter: annotations_start = 7\n",
+ "output: enrichment = f'{cwd:a}/{step_name}/{name}.enrichment_results.rds'\n",
+ "\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output[0]:bnn}'\n",
+ "bash: expand = '${ }', stderr = f'{_output[0]}.stderr', stdout = f'{_output[0]}.stdout'\n",
+ " Rscript ${modular_script_dir}/enrichment/eoo_enrichment.R \\\n",
+ " --significant-variants-path ${significant_variants_path} \\\n",
+ " --baseline-anno-path ${baseline_anno_path} \\\n",
+ " --annotations-start ${annotations_start} \\\n",
+ " --output ${_output['enrichment']}"
+ ]
}
],
"metadata": {
@@ -412,4 +435,4 @@
},
"nbformat": 4,
"nbformat_minor": 4
-}
\ No newline at end of file
+}
diff --git a/code/SoS/enrichment/gregor.ipynb b/code/SoS/enrichment/gregor.ipynb
index 8a9dae7bf..10a2080fc 100644
--- a/code/SoS/enrichment/gregor.ipynb
+++ b/code/SoS/enrichment/gregor.ipynb
@@ -8,20 +8,22 @@
"source": [
"# GREGOR enrichment analysis\n",
"\n",
- "## Description\n",
- "\n",
- "**Aim.** Evaluate the global enrichment of a set of trait-associated (positive) variants within experimentally annotated epigenomic regulatory features, relative to a matched negative set, using [GREGOR (Genomic Regulatory Elements and Gwas Overlap algoRithm)](https://genome.sph.umich.edu/wiki/GREGOR).\n",
- "\n",
- "## Methods overview\n",
+ "Tests whether a set of trait-associated variants falls inside annotated regulatory features more often than a matched control set does."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "## Overview\n",
"\n",
- "GREGOR performs enrichment analysis for given positive set of variants against matched negative set, matched by: \n",
+ "A set of trait-associated variants is more interpretable if you can say what kind of sequence they fall in. Enrichment testing asks whether they overlap a class of genomic feature - an annotation, a chromatin state, a set of regulatory elements - more often than chance allows, where chance has to account for the fact that variants are not exchangeable: they differ in minor allele frequency, in the number of LD proxies they carry, and in distance to the nearest gene. [GREGOR](https://genome.sph.umich.edu/wiki/GREGOR) builds matched control variants on exactly those properties - minor allele frequency, LD proxy count, distance to the nearest TSS, and local gene density - so the comparison is fair.\n",
"\n",
- "- MAF\n",
- "- LD proxy\n",
- "- distance to TSS\n",
- "- Gene density\n",
+ "GREGOR's own `PValue` column, defined in the [GREGOR paper](https://pubmed.ncbi.nlm.nih.gov/25886982/), sometimes comes back greater than 1, and a p-value says less about a result than the size of the enrichment does. This module therefore parses GREGOR's intermediate output into a 2x2 table - inside versus outside the annotation peak, positive set versus matched negative set - and runs Fisher's exact test on it to recover both a p-value and an odds ratio.\n",
"\n",
- "Methods to obtain the column \"PValue\" from GREGOR software can be found in the GREGOR paper: https://pubmed.ncbi.nlm.nih.gov/25886982/. We have noticed some PValue are greater than 1. Also, p-value itself is not as informative compared to odds ratios of enrichment. We therefore parse intermediate outputs from GREGOR to get a 2 by 2 table where the rows are within annotation peak vs outside; the columns are positive set vs matched negative set. We perform Fisher's exact test to get both p-value and odds ratios."
+ "**When to run it.** After you have a variant set worth characterising - fine-mapped credible sets, or GWAS hits - and an annotation you want to test them against."
]
},
{
@@ -29,7 +31,38 @@
"metadata": {
"kernel": "SoS"
},
- "source": "## Input\n\n- `--gregor_db`: a GREGOR reference database.\n- `--index_snp_file`: an index SNP file of the positive variant set.\n- `--bed_file_index`: a bed-file index listing the annotation `bed` files.\n\n> **Note.** `gregor_conf` (Step 1) only *writes* a configuration file from the supplied paths. Step 2 (`gregor`) invokes the GREGOR `perl` tool (`GREGOR`, on `PATH`) and parses its output; it needs a GREGOR reference database. The full reference (per-chromosome LD databases, ~20 GB) is distributed under the University of Michigan license and is not bundled here, but a downsized chr22 EUR slice is committed under `tests/fixtures/gregor/` so the pipeline is smoke-tested end-to-end. Index SNPs must be `chr:pos` (hg19), and `LDWINDOWSIZE` must match the window the reference was built with (1 Mb for the standard r²≥0.7 databases). The `gregor_fisher_plot` workflow (Step 3) is self-contained R and only needs two enrichment-result tables.\n"
+ "source": [
+ "## Input\n",
+ "\n",
+ "- `--index_snp_file`: the positive variant set, one `chr:pos` per line with no header, on the same build as the reference database (hg19 for the standard ones). Example `input/ld/protocol_example.index.snps.txt`:\n",
+ "\n",
+ " ```\n",
+ " chr1:752566\n",
+ " chr1:776546\n",
+ " chr2:233567\n",
+ " ```\n",
+ "\n",
+ "- `--bed_file_index`: a two-column tab-separated index naming each annotation and pointing at its `bed` file. Example `input/enrichment/protocol_example.bed.file.index`:\n",
+ "\n",
+ " ```\n",
+ " annotationA /annotationA.bed\n",
+ " annotationB /annotationB.bed\n",
+ " annotationC /annotationC.bed\n",
+ " ```\n",
+ "\n",
+ "- `--gregor_db`: the GREGOR reference database directory, one SQLite `CUBE` table per chromosome. The full per-population reference is around 20 GB and is distributed under the University of Michigan license, so it is not bundled here and `input/enrichment/protocol_example.gregor_ref` is a placeholder. A downsized chr22 EUR slice is committed under `tests/fixtures/gregor/EUR` so the notebook can be smoke-tested end to end.\n",
+ "- `--pop`: the reference population, `EUR` by default. It has to match the population the database was built for.\n",
+ "- `--r2_threshold` and `--ld_window_size`: what counts as an LD buddy, `0.7` and `10000` by default. Both have to match the values the reference database was built with, 1 Mb for the standard r2 >= 0.7 databases.\n",
+ "- `--fisher1` and `--fisher2`, used by `gregor_fisher_plot` only: two enrichment-result tables to plot against each other, in the form the `gregor` workflow writes. Example `input/enrichment/protocol_example.trait1_enrichment_results.txt`:\n",
+ "\n",
+ " ```\n",
+ " Bed_File odds low high p_fisher\n",
+ " Promoter_UCSC.bed 1.377 0.751 2.011 0.0653\n",
+ " Enhancer_Hoffman.bed 1.886 1.15 2.307 0.4567\n",
+ " ```\n",
+ "\n",
+ "- `--cwd`: the directory outputs are written to."
+ ]
},
{
"cell_type": "markdown",
@@ -39,12 +72,22 @@
"source": [
"## Output\n",
"\n",
- "All files are written under `--cwd`, prefixed by the run name:\n",
+ "- `{cwd}/{index_snp_file}.gregor.conf` - the GREGOR configuration file built from the supplied paths and parameters. The key lines of `output/gregor/protocol_example.index.gregor.conf`:\n",
+ "\n",
+ " ```\n",
+ " INDEX_SNP_FILE = input/ld/protocol_example.index.snps.txt\n",
+ " BED_FILE_INDEX = input/enrichment/protocol_example.bed.file.index\n",
+ " REF_DIR = input/enrichment/protocol_example.gregor_ref\n",
+ " R2THRESHOLD = 0.7 ## must be greater than 0.7\n",
+ " LDWINDOWSIZE = 10000 ## must be less than this window; these two values define LD buddies\n",
+ " POPULATION = EUR ## define the population, you can specify EUR, AFR, AMR or ASN\n",
+ " ```\n",
+ "\n",
+ "- `{name}_gregor_output/StatisticSummaryFile.txt` - GREGOR's raw overlap statistics, one row per annotation `bed` file.\n",
+ "- `{name}_variant_counts.txt` and `{name}_enrichment_results.txt` - the parsed 2x2 counts (inside versus outside the annotation peak, positive versus matched-negative set) and the per-annotation Fisher exact-test p-values and odds ratios.\n",
+ "- `{cwd}/{fisher1}_vs_{fisher2}_enrichment.pdf` - the odds-ratio comparison plot across annotations.\n",
"\n",
- "- `{name}.gregor.conf` — the GREGOR configuration file generated from the supplied paths (Step 1).\n",
- "- `{name}_gregor_output/StatisticSummaryFile.txt` — GREGOR's raw overlap statistics for the annotation set (Step 2).\n",
- "- `{name}_variant_counts.txt` and `{name}_enrichment_results.txt` — the parsed 2×2 counts (annotation peak vs outside, positive vs matched-negative variant set) and the per-annotation Fisher exact-test p-values and odds ratios (Step 3).\n",
- "- `{name}_vs_{ref}_enrichment.pdf` — an odds-ratio comparison plot across annotations.\n"
+ "Only the configuration file ships with the repository. Carrying the example past step 1 needs the full GREGOR reference database, so `output/gregor` holds the `.conf` and GREGOR's generated job scripts but no summary, counts, or results tables."
]
},
{
@@ -53,7 +96,9 @@
"kernel": "SoS"
},
"source": [
- "## Steps"
+ "## Minimal Working Example\n",
+ "\n",
+ "Steps 1 and 2 need the GREGOR reference database and the GREGOR `perl` software; step 3 runs on the two example result tables alone."
]
},
{
@@ -62,7 +107,7 @@
"kernel": "SoS"
},
"source": [
- "**Step 1.** Build the GREGOR configuration file from the index SNP file and bed-file index (`gregor_conf`). This step only writes a `.gregor.conf` file from the supplied paths/parameters and does not invoke the external GREGOR software, so it runs end-to-end with the toy inputs below."
+ "### GREGOR enrichment scan"
]
},
{
@@ -71,7 +116,18 @@
"kernel": "SoS"
},
"source": [
- "**Timing:** Requires external GREGOR tool on typical compute infrastructure."
+ "#### Configuration file only\n",
+ "\n",
+ "`gregor_conf` only *writes* a `.gregor.conf` from the supplied paths and parameters and never invokes GREGOR."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
]
},
{
@@ -96,7 +152,18 @@
"kernel": "SoS"
},
"source": [
- "**Step 2.** Run the full GREGOR enrichment pipeline (config -> run GREGOR -> Fisher test) (`gregor`). Requires the GREGOR `perl` software and reference database."
+ "#### The full chain\n",
+ "\n",
+ "`gregor` repeats the configuration step, then runs GREGOR and Fisher-tests its output. It expects the GREGOR `perl` tool as `GREGOR` on `PATH` and a reference database."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
]
},
{
@@ -121,7 +188,18 @@
"kernel": "SoS"
},
"source": [
- "**Step 3.** Plot an odds-ratio comparison from two enrichment-result tables (`gregor_fisher_plot`). Self-contained R; runnable from `*_enrichment_results.txt` tables alone."
+ "### Plot an odds-ratio comparison\n",
+ "\n",
+ "`gregor_fisher_plot` is self-contained R and runs from two `*_enrichment_results.txt` tables alone, with no reference database or GREGOR install needed."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
]
},
{
@@ -144,29 +222,63 @@
"kernel": "SoS"
},
"source": [
- "## Anticipated Results\n",
- "\n",
- "Produces a `*_enrichment_results.txt` table of per-annotation Fisher p-values and odds ratios, and (via `gregor_fisher_plot`) an odds-ratio comparison PDF. Annotations enriched in the positive variant set show odds ratios above 1.\n"
+ "## Command Interface"
]
},
{
- "cell_type": "markdown",
+ "cell_type": "code",
+ "execution_count": null,
"metadata": {
"kernel": "SoS"
},
+ "outputs": [],
"source": [
- "## Command interface"
+ "sos run pipeline/gregor.ipynb -h"
]
},
{
- "cell_type": "code",
- "execution_count": null,
+ "cell_type": "markdown",
"metadata": {
- "kernel": "Bash"
+ "kernel": "SoS"
},
- "outputs": [],
"source": [
- "sos run pipeline/gregor.ipynb -h"
+ "```\n",
+ "usage: sos run pipeline/gregor.ipynb\n",
+ " [workflow_name | -t targets] [options] [workflow_options]\n",
+ " workflow_name: Single or combined workflows defined in this script\n",
+ " targets: One or more targets to generate\n",
+ " options: Single-hyphen sos parameters (see \"sos run -h\" for details)\n",
+ " workflow_options: Double-hyphen workflow-specific parameters\n",
+ "\n",
+ "Workflows:\n",
+ " gregor_conf\n",
+ " gregor\n",
+ " gregor_fisher_plot\n",
+ "\n",
+ "Global Workflow Options:\n",
+ " --cwd output (as path)\n",
+ " working directory\n",
+ " --container ''\n",
+ " Software container option\n",
+ "\n",
+ "Sections\n",
+ " gregor_conf, gregor_1: make configuration file for GREGOR\n",
+ " Workflow Options:\n",
+ " --gregor-db VAL (as path, required)\n",
+ " --pop EUR\n",
+ " --index-snp-file VAL (as path, required)\n",
+ " --bed-file-index VAL (as path, required)\n",
+ " --r2-threshold 0.7 (as float)\n",
+ " --ld-window-size 10000 (as int)\n",
+ " --min-neighbor 10 (as int)\n",
+ " --job-number 10 (as int)\n",
+ " gregor_2: run GREGOR\n",
+ " gregor_3: Fisher test of enrichment\n",
+ " gregor_fisher_plot: enrichment plot with fisher test results\n",
+ " Workflow Options:\n",
+ " --fisher1 VAL (as path, required)\n",
+ " --fisher2 VAL (as path, required)\n",
+ "```"
]
},
{
@@ -257,7 +369,7 @@
"sudo apt-get install libdbi-perl libswitch-perl libdbd-sqlite3-perl\n",
"```\n",
"\n",
- "With our docker image:\n",
+ "With the docker image:\n",
"\n",
"```\n",
"cd GREGOR_folder\n",
@@ -274,7 +386,13 @@
"kernel": "SoS"
},
"outputs": [],
- "source": "# run GREGOR\n[gregor_2]\noutput: f'{_input:nn}_gregor_output/StatisticSummaryFile.txt'\nbash: expand = True, container = container, stderr = f'{_output}.stderr', stdout = f'{_output}.stdout'\n GREGOR --conf {_input} && touch {_output}"
+ "source": [
+ "# run GREGOR\n",
+ "[gregor_2]\n",
+ "output: f'{_input:nn}_gregor_output/StatisticSummaryFile.txt'\n",
+ "bash: expand = True, container = container, stderr = f'{_output}.stderr', stdout = f'{_output}.stdout'\n",
+ " GREGOR --conf {_input} && touch {_output}"
+ ]
},
{
"cell_type": "code",
diff --git a/code/SoS/enrichment/gsea.ipynb b/code/SoS/enrichment/gsea.ipynb
index 0a1dc68ea..6a8fd6f83 100644
--- a/code/SoS/enrichment/gsea.ipynb
+++ b/code/SoS/enrichment/gsea.ipynb
@@ -6,7 +6,9 @@
"kernel": "SoS"
},
"source": [
- "# Pathway Analysis"
+ "# Pathway Analysis\n",
+ "\n",
+ "Tests groups of genes for over-representation in KEGG pathways and Gene Ontology terms."
]
},
{
@@ -15,17 +17,15 @@
"kernel": "SoS"
},
"source": [
- "## Description\n",
- "\n",
- "Pathway enrichment analysis over groups of genes using both the KEGG database and all three Gene Ontology (GO) ontologies, via the `clusterProfiler` R package. It identifies the biologically significant pathways and functional categories associated with each gene group, and supports multi-group comparison (e.g. cell types, conditions, treatments) with a single unified output.\n",
+ "## Overview\n",
"\n",
- "## Methods\n",
+ "A list of genes is hard to interpret on its own. Over-representation testing asks whether a group contains more members of some pathway or ontology term than chance would give, turning a list of identifiers into statements about biology.\n",
"\n",
- "**Gene ID conversion.** Input ENSEMBL gene IDs are converted to ENTREZ IDs using `org.Hs.eg.db`; genes without a valid ENTREZ mapping are dropped, and group associations are preserved through the conversion.\n",
+ "This module tests each group against the KEGG database and all three Gene Ontology branches using `clusterProfiler`. Input ENSEMBL gene IDs are first converted to ENTREZ IDs via `org.Hs.eg.db`; genes without a valid ENTREZ mapping are dropped, and group associations are preserved through the conversion. KEGG over-representation then runs through `enrichKEGG()` and GO through `enrichGO()` for Biological Process, Cellular Component and Molecular Function, both applying a hypergeometric test with Benjamini-Hochberg FDR correction.\n",
"\n",
- "**Enrichment testing.** KEGG over-representation uses `enrichKEGG()` (organism `hsa`); GO uses `enrichGO()` against `org.Hs.eg.db` for Biological Process (BP), Cellular Component (CC), and Molecular Function (MF). Both apply a hypergeometric test with Benjamini-Hochberg FDR correction.\n",
+ "Every group is analysed in the same run, and results from all analysis types are harmonised into one column schema - metadata columns for analysis type and ontology category, plus a category to subcategory to term classification - then combined into a single data frame saved as a compressed RDS. That shared schema is what lets groups such as cell types, conditions or treatments be compared side by side.\n",
"\n",
- "**Output standardization.** Results from all analysis types are harmonized into one column schema with metadata columns (analysis type, ontology category) and a hierarchical category -> subcategory -> term classification, then combined into a single data frame and saved as a compressed RDS file with per-group summary statistics.\n"
+ "**When to run it.** After a set of genes of interest has been grouped, to ask which pathways and ontology terms those groups are enriched for."
]
},
{
@@ -36,29 +36,22 @@
"source": [
"## Input\n",
"\n",
- "### Gene List (TSV format)\n",
- "\n",
- "A table containing grouped genes with the following structure:\n",
+ "- `--genes-file`: a TSV of grouped genes with two required columns, `group` naming the category a gene belongs to and `gene_id` giving its ENSEMBL identifier. Every group present is tested. Example `input/enrichment/protocol_example.pathway_genes.tsv`:\n",
"\n",
- "**Required columns:**\n",
+ " ```\n",
+ " group gene_id\n",
+ " AD ENSG00000139618\n",
+ " AD ENSG00000091831\n",
+ " TL1 ENSG00000196839\n",
+ " TL1 ENSG0000008123\n",
+ " ```\n",
"\n",
- "- `group`: The group/category to which a gene belongs (e.g., `AD_A_cauchy`, `TL1_C_cauchy`)\n",
- "- `gene_id`: ENSEMBL gene IDs for the corresponding genes\n",
- "\n",
- "**Example:**\n",
- "```\n",
- "group gene_id\n",
- "AD ENSG00000139618\n",
- "AD ENSG00000091831\n",
- "TL1 ENSG00000196839\n",
- "TL1 ENSG0000008123\n",
- "```\n",
- "### Parameters:\n",
- "\n",
- "- `pvalue_cutoff`: Cutoff for pathway enrichment significance (default: 1)\n",
- "- `organism`: KEGG organism code (default: `hsa` for humans)\n",
- "\n",
- "---"
+ "- `--name`: the stem of the output file. Required.\n",
+ "- `--organism`: the KEGG organism code, `hsa` (human) by default.\n",
+ "- `--pvalue-cutoff`: the enrichment p-value threshold, `1` by default, which keeps every term so filtering can be done downstream.\n",
+ "- `--cwd`: the directory outputs are written to.\n",
+ "- `--modular-script-dir`: the `code/script` directory holding the analysis scripts.\n",
+ "- `--numThreads`, `--job-size`, `--walltime` and `--mem`: job resources."
]
},
{
@@ -69,26 +62,27 @@
"source": [
"## Output\n",
"\n",
- "### Combined Pathway Results File\n",
- "\n",
- "A compressed RDS file (`.rds`) containing standardized enrichment results from all analyses.\n",
+ "- `{cwd}/pathway_analysis/{name}.combined_pathway_results.rds` - one compressed RDS holding the KEGG and GO results for every gene group in a single data frame, 2842 rows and 17 columns on the example. SoS always inserts the step name, so results land in a `pathway_analysis/` subdirectory of whatever `--cwd` is given. Example `output/gsea/pathway_analysis/protocol_example.combined_pathway_results.rds`:\n",
"\n",
- "**Key columns in the results:**\n",
+ " ```\n",
+ " 'data.frame': 2842 obs. of 17 variables:\n",
+ " $ ID : chr \"hsa01524\" \"hsa04115\" \"hsa05210\" \"hsa05162\" ...\n",
+ " $ Description : chr \"Platinum drug resistance\" \"p53 signaling pathway\" \"Colorectal cancer\" \"Measles\" ...\n",
+ " $ GeneRatio : chr \"7/9\" \"6/9\" \"6/9\" \"6/9\" ...\n",
+ " $ BgRatio : chr \"75/9416\" \"75/9416\" \"87/9416\" \"136/9416\" ...\n",
+ " $ RichFactor : num 0.0933 0.08 0.069 0.0441 0.0438 ...\n",
+ " $ FoldEnrichment: num 97.6 83.7 72.2 46.2 45.8 ...\n",
+ " ```\n",
"\n",
- "- `ID`: Pathway/GO term identifier\n",
- "- `Description`: Pathway/GO term name/description\n",
- "- `GeneRatio`: Ratio of input genes found in the pathway/term\n",
- "- `BgRatio`: Background ratio of all genes in the database for this pathway/term\n",
- "- `pvalue`: Raw enrichment p-value\n",
- "- `p.adjust`: Adjusted p-value (Benjamini-Hochberg)\n",
- "- `qvalue`: Q-value for FDR control\n",
- "- `group`: Name of the gene group analyzed\n",
- "- `analysis_type`: Type of analysis (`KEGG` or `GO`)\n",
- "- `ont_category`: Ontology category (`PATHWAY`, `BP`, `CC`, or `MF`)\n",
- "- `category`: High-level category classification\n",
- "- `subcategory`: Detailed subcategory classification\n",
+ "The 17 columns are the same for every analysis type, which is what makes groups directly comparable:\n",
"\n",
- "---"
+ "- `ID` and `Description`: the pathway or GO term identifier and its name.\n",
+ "- `GeneRatio` and `BgRatio`: input genes found in the term, against all database genes in that term.\n",
+ "- `RichFactor` and `FoldEnrichment`: the ratio of the two, raw and fold-scaled.\n",
+ "- `pvalue`, `p.adjust` and `qvalue`: the raw hypergeometric p-value, the Benjamini-Hochberg adjusted value, and the q-value for FDR control.\n",
+ "- `group`: which gene group the row came from.\n",
+ "- `analysis_type` and `ont_category`: `KEGG` or `GO`, and `PATHWAY`, `BP`, `CC` or `MF`.\n",
+ "- `category` and `subcategory`: the high-level and detailed classification of the term. Both are `NA` for KEGG rows in this example."
]
},
{
@@ -97,7 +91,7 @@
"kernel": "SoS"
},
"source": [
- "## Steps"
+ "## Minimal Working Example"
]
},
{
@@ -106,7 +100,7 @@
"kernel": "SoS"
},
"source": [
- "**Step 1.** Run KEGG + GO over-representation (pathway) analysis on the grouped gene sets (`pathway_analysis`). Genes are mapped ENSEMBL→ENTREZ, then `enrichKEGG`/`enrichGO` are run per group and the results are combined into one RDS. _Note: KEGG enrichment queries the online KEGG API, so this step needs network access; GO enrichment uses the local `org.Hs.eg.db`._"
+ "KEGG enrichment queries the online KEGG API, so this step needs network access. GO enrichment uses the local `org.Hs.eg.db` and works offline."
]
},
{
@@ -115,7 +109,7 @@
"kernel": "SoS"
},
"source": [
- "**Timing:** ~1-2 min on typical compute infrastructure."
+ "**Timing**: ~1-2 min (on toy dataset)"
]
},
{
@@ -130,7 +124,7 @@
" --genes_file input/enrichment/protocol_example.pathway_genes.tsv \\\n",
" --name protocol_example \\\n",
" --pvalue_cutoff 1 --organism hsa \\\n",
- " --cwd output/pathway_analysis"
+ " --cwd output/gsea"
]
},
{
@@ -139,18 +133,7 @@
"kernel": "SoS"
},
"source": [
- "## Anticipated Results\n",
- "\n",
- "Writes a single compressed `.rds` with standardized KEGG and GO (BP/CC/MF) enrichment results across all gene groups, sharing one column schema for downstream comparison. Each group yields the pathways and ontology terms over-represented among its genes.\n"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "## Command interface"
+ "## Command Interface"
]
},
{
@@ -164,6 +147,42 @@
"sos run pipeline/gsea.ipynb -h"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "```\n",
+ "usage: sos run code/SoS/enrichment/gsea.ipynb\n",
+ " [workflow_name | -t targets] [options] [workflow_options]\n",
+ " workflow_name: Single or combined workflows defined in this script\n",
+ " targets: One or more targets to generate\n",
+ " options: Single-hyphen sos parameters (see \"sos run -h\" for details)\n",
+ " workflow_options: Double-hyphen workflow-specific parameters\n",
+ "\n",
+ "Workflows:\n",
+ " pathway_analysis\n",
+ "\n",
+ "Global Workflow Options:\n",
+ " --cwd output (as path)\n",
+ " Path to the work directory of the analysis.\n",
+ " --modular-script-dir code/script (as path)\n",
+ " --genes-file . (as path)\n",
+ " --name VAL (as str, required)\n",
+ " --numThreads 8 (as int)\n",
+ " Number of threads\n",
+ " --job-size 1 (as int)\n",
+ " For cluster jobs, number commands to run per job\n",
+ " --walltime 12h\n",
+ " --mem 16G\n",
+ "\n",
+ "Sections\n",
+ " pathway_analysis:\n",
+ " Workflow Options:\n",
+ " --pvalue-cutoff 1 (as int)\n",
+ " --organism hsa\n",
+ "```"
+ ]
+ },
{
"cell_type": "markdown",
"metadata": {
@@ -180,7 +199,21 @@
"kernel": "SoS"
},
"outputs": [],
- "source": "[global]\n# Path to the work directory of the analysis.\nparameter: cwd = path('output')\nparameter: modular_script_dir = path('code/script') # override with --modular-script-dir\n\nparameter: genes_file = path() # TSV file with columns: group, gene_id\nparameter: name = str\n# Number of threads\nparameter: numThreads = 8\n# For cluster jobs, number commands to run per job\nparameter: job_size = 1\nparameter: walltime = '12h'\nparameter: mem = '16G'"
+ "source": [
+ "[global]\n",
+ "# Path to the work directory of the analysis.\n",
+ "parameter: cwd = path('output')\n",
+ "parameter: modular_script_dir = path('code/script') # override with --modular-script-dir\n",
+ "\n",
+ "parameter: genes_file = path() # TSV file with columns: group, gene_id\n",
+ "parameter: name = str\n",
+ "# Number of threads\n",
+ "parameter: numThreads = 8\n",
+ "# For cluster jobs, number commands to run per job\n",
+ "parameter: job_size = 1\n",
+ "parameter: walltime = '12h'\n",
+ "parameter: mem = '16G'"
+ ]
},
{
"cell_type": "code",
@@ -189,7 +222,20 @@
"kernel": "SoS"
},
"outputs": [],
- "source": "[pathway_analysis]\nparameter: pvalue_cutoff = 1\nparameter: organism = 'hsa'\noutput: pathway_results = f'{cwd:a}/{step_name}/{name}.combined_pathway_results.rds'\n\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output[0]:bnn}'\nbash: expand = '${ }', stderr = f'{_output[0]}.stderr', stdout = f'{_output[0]}.stdout'\n Rscript ${modular_script_dir}/enrichment/gsea.R \\\n --genes-file ${genes_file} \\\n --organism ${organism} \\\n --pvalue-cutoff ${pvalue_cutoff} \\\n --output ${_output['pathway_results']}"
+ "source": [
+ "[pathway_analysis]\n",
+ "parameter: pvalue_cutoff = 1\n",
+ "parameter: organism = 'hsa'\n",
+ "output: pathway_results = f'{cwd:a}/{step_name}/{name}.combined_pathway_results.rds'\n",
+ "\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output[0]:bnn}'\n",
+ "bash: expand = '${ }', stderr = f'{_output[0]}.stderr', stdout = f'{_output[0]}.stdout'\n",
+ " Rscript ${modular_script_dir}/enrichment/gsea.R \\\n",
+ " --genes-file ${genes_file} \\\n",
+ " --organism ${organism} \\\n",
+ " --pvalue-cutoff ${pvalue_cutoff} \\\n",
+ " --output ${_output['pathway_results']}"
+ ]
}
],
"metadata": {
@@ -228,4 +274,4 @@
},
"nbformat": 4,
"nbformat_minor": 4
-}
\ No newline at end of file
+}
diff --git a/code/SoS/enrichment/sldsc_enrichment.ipynb b/code/SoS/enrichment/sldsc_enrichment.ipynb
index c19911dc1..d25d32bd9 100644
--- a/code/SoS/enrichment/sldsc_enrichment.ipynb
+++ b/code/SoS/enrichment/sldsc_enrichment.ipynb
@@ -8,9 +8,7 @@
"source": [
"# Stratified LD Score Regression (S-LDSC) Enrichment\n",
"\n",
- "Minimal working-example driver for the S-LDSC functional-enrichment pipeline. The **Steps** section below gives one ready-to-run `sos run` command per workflow, using the toy inputs symlinked under `input/`.\n",
- "\n",
- "> **Environment note.** Steps 1–2 (`make_annotation_files_ldscore`, `get_heritability`) wrap the external **polyfun** toolkit (`compute_ldscores`, `ldsc`, `munge_polyfun_sumstats`) and require pre-computed reference-panel files (baseline-LD scores, LD weights, `.frq`, and PLINK `.bed/.bim/.fam`). polyfun is **not installed in this environment** and the reference panel is not shipped with the toy example, so those two steps cannot be executed here; their commands are provided for use on a system where polyfun and a matching panel are available. Steps 3–4 (`postprocess`, `meta_subset`) use `pecotmr::sldscPostprocessingPipeline` (available here) and read the `.results`/`.log` files produced by Step 2.\n"
+ "Per-SNP heritability enrichment of genome annotations from GWAS summary statistics alone."
]
},
{
@@ -19,21 +17,33 @@
"kernel": "SoS"
},
"source": [
- "## Description\n",
- "This notebook implements the pipeline of [S-LDSC](https://www.nature.com/articles/ng.3404) for LD score and functional enrichment analysis.\n",
- "\n",
- "**Important: the S-LDSC implementation comes from the [polyfun](https://github.com/omerwe/polyfun/tree/master) package, not the original LDSC from `bulik/ldsc` GitHub repo.**"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "Markdown"
- },
- "source": [
- "Uses GWAS summary statistics together with annotation and LD reference-panel data to compute per-SNP heritability enrichment for each annotation. It supports single-annotation (individual contribution) and joint multi-annotation (independent contribution) analysis.\n",
- "\n",
- "**Background.** LD Score Regression (Bulik-Sullivan et al. 2015) distinguishes confounding (e.g. population stratification) from true polygenic signal by regressing GWAS chi-square statistics on LD scores: SNPs tagging more variation (high LD score) show higher chi-square under true polygenicity, whereas confounding inflates statistics independently of LD. S-LDSC (Finucane et al. 2015) partitions heritability across overlapping annotation categories; standardized tau accounts for negative selection (Gazal et al. 2017). The model details and the tau*/EnrichStat definitions follow below.\n"
+ "## Overview\n",
+ "\n",
+ "Heritability is not spread evenly over the genome. This module asks whether a functional category -- a\n",
+ "chromatin state, a set of regulatory elements, an xQTL annotation -- carries more heritability than its\n",
+ "share of SNPs would predict, using GWAS summary statistics alone.\n",
+ "\n",
+ "The reasoning is short. A variant's association statistic reflects every causal variant it tags, so\n",
+ "under real polygenic signal a SNP that tags more variation has a higher expected chi-square, whereas\n",
+ "confounding such as population stratification inflates chi-square independently of LD; regressing\n",
+ "chi-square on LD score separates the two ([Bulik-Sullivan et al. 2015](https://doi.org/10.1038/ng.3211)). Computing a\n",
+ "separate LD score per annotation category turns that one slope into one slope per category, each\n",
+ "estimating the per-SNP heritability that category contributes ([Finucane et al. 2015](https://doi.org/10.1038/ng.3404)).\n",
+ "Categories may overlap, so every regression conditions on a baseline-LD model -- without it an estimate\n",
+ "reflects LD-related architecture the annotation merely correlates with.\n",
+ "\n",
+ "Two numbers come out, for different reasons. *Enrichment*, the proportion of heritability over the\n",
+ "proportion of SNPs, reads easily but is defined only for binary annotations and is not comparable across\n",
+ "annotations of different size. *Standardized tau* ($\\tau^*$) is the per-SNP effect of a\n",
+ "one-standard-deviation increase in the annotation, conditional on every other annotation in the model;\n",
+ "it is comparable across annotations and across traits and extends to continuous annotations\n",
+ "([Gazal et al. 2017](https://doi.org/10.1038/ng.3954)). Both are reported.\n",
+ "\n",
+ "The regression itself comes from [polyfun](https://github.com/omerwe/polyfun/tree/master), **not** the\n",
+ "original LDSC in `bulik/ldsc`; standardization and the across-trait meta-analysis are done in\n",
+ "[`pecotmr`](https://github.com/StatFunGen/pecotmr).\n",
+ "\n",
+ "**When to run it.** Run this module after GWAS summary statistics and the required LD-score annotations are available to estimate annotation-specific heritability enrichment and its uncertainty.\n"
]
},
{
@@ -42,24 +52,23 @@
"kernel": "SoS"
},
"source": [
- "## Methods - Workflow Overview\n",
- "\n",
- "The pipeline runs in three stages: (1) annotation preparation and the S-LDSC regression (polyfun), (2) post-processing into standardized $\\tau^*$ and meta-analysis (the `pecotmr` package), and (3) optional re-meta on user-defined trait subsets. The concrete commands for stages 1-2 are in the **Steps** section below.\n",
+ "### Pipeline stages\n",
"\n",
- "**Stage 1 - polyfun.** Three SoS workflows wrap polyfun: `make_annotation_files_ldscore` converts target annotations into polyfun `.annot.gz` and runs `compute_ldscores` (toggles `compute_single` and `compute_joint`, both default `True`; the joint dir is only emitted when $N \\geq 2$); `munge_sumstats_polyfun` preprocesses each GWAS into LDSC format; `get_heritability` runs polyfun's `ldsc` once per `--target-anno-dir`, enforcing the MAF cutoff via `--frqfile-chr` (`maf_cutoff` accepts only `0` or `0.05`).\n",
+ "The pipeline runs in three stages.\n",
"\n",
- "**Stage 2 - pecotmr post-processing.** A single `pecotmr::sldscPostprocessingPipeline` call consumes all polyfun outputs: it extracts $\\tau$, $E$, $h^2_g$, EnrichStat p-value and per-block jackknife $\\tau$ values; computes $sd_C$ and $M_{\\mathrm{ref}}$ over the regression's MAF-cutoff SNP set; standardizes $\\tau \\to \\tau^*$ for single and joint modes; auto-detects binary vs continuous annotations; and runs a DerSimonian-Laird random-effects meta-analysis across traits, producing three meta tables ($\\tau^*$ cross-type comparable, $E$ within-binary, EnrichStat within-binary). Output is an R list with `per_trait` and `meta` entries.\n",
+ "**Stage 1 -- polyfun.** Three SoS workflows wrap polyfun: `make_annotation_files_ldscore` converts\n",
+ "target annotations into polyfun `.annot.gz` and computes LD scores, `munge_sumstats_polyfun`\n",
+ "preprocesses each GWAS into LDSC format, and `get_heritability` runs polyfun's `ldsc` once per\n",
+ "(trait, target directory) pair.\n",
"\n",
- "**Stage 3 - subset meta-analysis.** `pecotmr::metaSldscRandom` re-runs the meta on a trait subset without re-running the regression (lightweight, interactive):\n",
+ "**Stage 2 -- pecotmr post-processing.** A single `pecotmr::sldscPostprocessingPipeline` call consumes\n",
+ "all polyfun outputs: it extracts $\\tau$, $E$, $h^2_g$, EnrichStat p-value and per-block jackknife\n",
+ "$\\tau$ values; standardizes $\\tau \\to \\tau^*$ for both single- and joint-tau modes; auto-detects\n",
+ "binary versus continuous annotations; and runs a DerSimonian-Laird random-effects meta-analysis across\n",
+ "traits. The result is an R list with `per_trait` and `meta` entries.\n",
"\n",
- "```r\n",
- "res <- readRDS(\"sldsc_results.rds\")\n",
- "neuro <- c(\"AD_GWAX\", \"PD_meta\", \"ALS_meta\")\n",
- "view_neuro <- pecotmr:::.sldscViewForMeta(res$per_trait[neuro], \"single\")\n",
- "meta_neuro_taustar <- pecotmr::metaSldscRandom(\n",
- " view_neuro, category = \"my_target_anno\", quantity = \"tauStar\"\n",
- ")\n",
- "```\n"
+ "**Stage 3 -- subset meta-analysis.** `pecotmr::metaSldscRandom` re-runs the meta over a trait subset\n",
+ "from the cached `.sldsc_postprocess.rds`, without re-running the regression."
]
},
{
@@ -68,18 +77,7 @@
"kernel": "SoS"
},
"source": [
- "## Theory\n",
- "\n",
- "The statistical model behind the pipeline is summarized below. Because the same framework underlies several of the workflow steps, the model, its stratified extension, and the tau-estimation / enrichment definitions are described together here rather than repeated per step."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "### LDSC model\n",
+ "### Model\n",
"\n",
"Under a polygenic assumption, in which effect sizes for variants are drawn independently from distributions with variance proportional to $1/(p(1-p))$ where $p$ is the minor allele frequency (MAF), the expected $\\chi^2$ statistic of variant $j$ is:\n",
"\n",
@@ -87,22 +85,8 @@
"\n",
"where $N$ is the sample size; $M$ is the number of SNPs, so that $h^2/M$ is the average heritability per SNP; $a$ measures the contribution of confounding biases such as cryptic relatedness and population stratification; and $\\ell_j = \\sum_k r^2_{jk}$ is the LD Score of variant $j$, which measures the amount of genetic variation tagged by $j$. A full derivation is given in the Supplementary Note of Bulik-Sullivan et al. (2015); an alternative derivation appears in the Supplementary Note of Zhu and Stephens (2017) AoAS.\n",
"\n",
- "Equation (1) shows that LD Score regression can compute SNP-based heritability for a phenotype from GWAS summary statistics alone, without requiring individual-level genotype data as REML and related methods do."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "### Stratified LDSC\n",
- "\n",
- "Heritability is the proportion of phenotypic variation that is due to variation in genetic values, and it can also be partitioned over disjoint or overlapping categories of SNPs.\n",
- "\n",
- "Stratified LD Score Regression (S-LDSC) partitions heritability by leveraging both LD-score information and SNPs that have not reached genome-wide significance. S-LDSC exploits the fact that the $\\chi^2$ statistic for a given SNP reflects the cumulative effects of all SNPs tagged by it: in regions of high LD, the focal SNP captures the contribution of a group of nearby SNPs.\n",
- "\n",
- "S-LDSC declares an annotation enriched for heritability if SNPs with high LD to that annotation have higher $\\chi^2$ statistics than SNPs with low LD to it.\n",
+ "Equation (1) computes SNP heritability from summary statistics alone, with no individual-level\n",
+ "genotypes.\n",
"\n",
"Let $a_{jC}$ denote the value of annotation $C$ at SNP $j$:\n",
"\n",
@@ -121,9 +105,7 @@
"\n",
"Interpretation of $\\tau_C$:\n",
"- **Binary $C$**: $\\tau_C$ is the *additive increase in per-SNP heritability* for SNPs in category $C$, on top of the contributions from any other annotations they belong to.\n",
- "- **Continuous $C$**: $\\tau_C$ is the *additive change in per-SNP heritability per unit increase* in the value of annotation $C$.\n",
- "\n",
- "For application to real data and comparisons to other methods, see the three papers cited at the top of this notebook."
+ "- **Continuous $C$**: $\\tau_C$ is the *additive change in per-SNP heritability per unit increase* in the value of annotation $C$."
]
},
{
@@ -132,47 +114,32 @@
"kernel": "SoS"
},
"source": [
- "### Tau Estimation and Enrichment Analysis\n",
- "\n",
- "Goal: quantify the contribution of functional annotations to trait heritability and assess statistical significance, accounting for LD structure and (for continuous annotations) annotation scale.\n",
- "\n",
- "The pipeline has two computational layers:\n",
- "\n",
- "- **Regression layer** — the S-LDSC regression itself, performed by the [polyfun](https://github.com/omerwe/polyfun) engine. We do not re-implement this.\n",
- "- **Post-processing layer** — standardization, differential per-SNP heritability, binary/continuous detection, and random-effects meta-analysis across traits. Implemented in the [`pecotmr`](https://github.com/StatFunGen/pecotmr) R package (`R/sldsc_wrapper.R`).\n",
- "\n",
- "The notation below tags each modeling quantity as **(polyfun)** or **(pecotmr)**.\n",
+ "### Tau estimation and enrichment analysis\n",
"\n",
"#### Notation\n",
"\n",
- "For each annotation $C$ we use:\n",
+ "For each annotation $C$:\n",
"\n",
"- $\\pi^{h^2}_C$ = proportion of trait heritability $h^2_g$ assigned to annotation $C$.\n",
"- $\\pi^{M}_C$ = proportion of (effective) SNPs in annotation $C$. For binary annotations this is $M_C / M_{\\mathrm{ref}}$; for continuous annotations it is the share of total annotation weight in $C$.\n",
"\n",
- "#### Reference panel and MAF cutoff\n",
- "\n",
- "All LD-derived quantities — partitioned LD scores for the 97 baseline annotations and for our $K$ target annotations, the LD-score-regression weights, allele frequencies, and the SNP set — are computed against our own LD reference panel. We do not mix in pre-computed quantities from external panels (e.g. 1000G); $M_{\\mathrm{ref}}$ throughout this notebook denotes the number of common SNPs in our panel.\n",
- "\n",
- "By default we restrict to MAF $> 5\\%$ per the sLDSC recommendation: rare-variant LD is unstable and HapMap3-style regression weights are common-variant by construction. The cutoff is exposed as the SoS parameter `maf_cutoff` (default $0.05$); the regression, the standardized $sd_C$, and $M_{\\mathrm{ref}}$ are all evaluated on the same MAF $>$ cutoff SNP set. If allele-frequency files are not available the pipeline fails; the user must explicitly set `maf_cutoff = 0` to opt out (not recommended).\n",
- "\n",
"#### Quantities from the regression layer (polyfun)\n",
"\n",
- "Solving Equation (2) jointly across annotations, with 200-block genomic jackknife for inference, is performed by polyfun's `ldsc`. From each polyfun run we obtain, per annotation:\n",
+ "Solving Equation (2) jointly across annotations, with 200-block genomic jackknife for inference, is performed by polyfun's `ldsc`. Each polyfun run yields, per annotation:\n",
"\n",
"- $\\tau_C$ and its standard error — **(polyfun)**.\n",
"- $\\pi^{h^2}_C$ and $\\pi^{M}_C$ — **(polyfun)**.\n",
"- $E_C = \\pi^{h^2}_C / \\pi^{M}_C$ and its standard error — **(polyfun)**.\n",
"- The p-value of the differential per-SNP heritability test (defined below) — **(polyfun)**, computed internally with the full coefficient covariance matrix.\n",
"\n",
- "We also obtain, per run:\n",
+ "Also obtained, per run:\n",
"\n",
"- The total trait heritability $h^2_g$ — **(polyfun)**.\n",
"- The 200-block jackknife delete-values of $\\tau_C$ — **(polyfun)**.\n",
"\n",
"#### Quantities from the post-processing layer (pecotmr)\n",
"\n",
- "From the polyfun outputs above plus our reference panel, the post-processing layer computes:\n",
+ "From the polyfun outputs above plus the reference panel, the post-processing layer computes:\n",
"\n",
"- $sd_C$ — per-annotation standard deviation over MAF $>$ cutoff SNPs — **(pecotmr: `compute_sldsc_annot_sd`)**.\n",
"- $M_{\\mathrm{ref}}$ — reference SNP count at the MAF cutoff — **(pecotmr: `compute_sldsc_M_ref`)**.\n",
@@ -185,7 +152,7 @@
"\n",
"#### Standardized tau ($\\tau^*$) — (pecotmr)\n",
"\n",
- "$\\tau_C$ has units that depend on the scale of the annotation and on the total heritability of the trait, so raw $\\tau$ is not directly comparable across annotations or across traits. We compute the standardized version (Gazal et al. 2017)\n",
+ "$\\tau_C$ has units that depend on the scale of the annotation and on the total heritability of the trait, so raw $\\tau$ is not directly comparable across annotations or across traits; the standardized version is computed instead (Gazal et al. 2017)\n",
"\n",
"$$\\tau^*_C \\;=\\; \\tau_C \\cdot \\frac{sd_C \\cdot M_{\\mathrm{ref}}}{h^2_g}$$\n",
"\n",
@@ -231,7 +198,7 @@
"\n",
"#### Joint analysis — (polyfun runs the regression; pecotmr standardizes both modes)\n",
"\n",
- "For **joint analysis** (multiple annotations fit together), both $\\tau$ and $E$ are conditional on the other annotations in the model. We report joint $\\tau^*_C$ as the independent contribution of annotation $C$ after controlling for the others. The annotation-prep step exposes two independent toggles, `compute_single` and `compute_joint` (both default `True`), so the user can produce the $N$ single-target outputs, the joint output, or both in one invocation. With both defaults the post-processing layer reads all $N+1$ regression outputs per trait and presents single + joint side-by-side. When the joint subset is decided after looking at single-target results (exploratory $\\rightarrow$ conditional workflow), the user runs the annotation-prep step a second time with `compute_single=False` on the curated subset.\n",
+ "For **joint analysis** (multiple annotations fit together), both $\\tau$ and $E$ are conditional on the other annotations in the model; joint $\\tau^*_C$ is reported as the independent contribution of annotation $C$ after controlling for the others. The annotation-prep step exposes two independent toggles, `compute_single` and `compute_joint` (both default `True`), so the user can produce the $N$ single-target outputs, the joint output, or both in one invocation. With both defaults the post-processing layer reads all $N+1$ regression outputs per trait and presents single + joint side-by-side. When the joint subset is decided after looking at single-target results (exploratory $\\rightarrow$ conditional workflow), the user runs the annotation-prep step a second time with `compute_single=False` on the curated subset.\n",
"\n",
"### Meta-Analysis across Traits (Random Effects) — (pecotmr)\n",
"\n",
@@ -245,7 +212,7 @@
"- **For $E_C$ meta**: $SE_i$ is the polyfun-reported `Enrichment_std_error`.\n",
"- **For EnrichStat meta**: $SE_i$ is the back-solved SE from polyfun's `Enrichment_p`.\n",
"\n",
- "For binary-annotation enrichment reporting we use a two-channel meta: the **effect size and SE** come from the meta on $E_C$ (interpretable on the original enrichment-fold scale), while the **p-value** comes from the meta on EnrichStat (the appropriate hypothesis test). The pipeline produces a default meta over all supplied traits; users can re-run meta on any subset of traits without re-running the regression layer.\n",
+ "Binary-annotation enrichment reporting uses a two-channel meta: the **effect size and SE** come from the meta on $E_C$ (interpretable on the original enrichment-fold scale), while the **p-value** comes from the meta on EnrichStat (the appropriate hypothesis test). The pipeline produces a default meta over all supplied traits; users can re-run meta on any subset of traits without re-running the regression layer.\n",
"\n",
"$$Z_{\\mathrm{meta}} \\;=\\; \\frac{\\hat\\theta_{\\mathrm{meta}}}{SE_{\\mathrm{meta}}}, \\qquad p \\;=\\; 2\\,\\Phi(-|Z_{\\mathrm{meta}}|)$$"
]
@@ -256,9 +223,162 @@
"kernel": "SoS"
},
"source": [
- "## Minimal Working Example (MWE)\n",
+ "**When to run it.** Once GWAS summary statistics and an annotation worth testing are in hand. S-LDSC characterises a trait against a whole category; when the question is about a specific variant set, use `gregor` instead."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "## Input\n",
+ "\n",
+ "Every path-list input is a two-column index file (`#id`, `path`), one row per chromosome, so the\n",
+ "pipeline can fan out per chromosome. Example index files live in `input/enrichment/sldsc/`.\n",
+ "\n",
+ "Required throughout:\n",
+ "\n",
+ "- `--annotation-name` (required): prefix for every output directory and file of this run.\n",
+ "- `--cwd` (default `output`): work directory for the analysis.\n",
+ "- `--modular-script-dir` (default `code/script`): directory holding the modular R wrapper scripts.\n",
+ "- `--maf-cutoff` (default `0.05`): MAF cutoff, per the S-LDSC recommendation, since rare-variant LD is\n",
+ " unstable and HapMap3-style regression weights are common-variant by construction. `0` opts out of MAF\n",
+ " filtering (not recommended); any other value requires recomputing LD scores at that cutoff.\n",
+ "\n",
+ "`make_annotation_files_ldscore` -- build annotation and LD-score files:\n",
+ "\n",
+ "- `--annotation-file`: index of the target annotation files to test. Binary and continuous annotations\n",
+ " are both supported and the type is auto-detected per column. Multiple target annotations may be given\n",
+ " in one file (columns `path`, `path1`, `path2`, ...); single-target and joint-target analyses are then\n",
+ " produced in the same pass.\n",
+ "- `--reference-anno-file`: index of the baseline annotations, typically a baseline-LD\n",
+ " baseline-LD model in `.annot.gz` form: v1.1 of Gazal et al. 2017 carries 75 annotations, v2.2 carries\n",
+ " 97. The baseline conditions every regression.\n",
+ "- `--genome-ref-file`: index of the PLINK-format genome reference.\n",
+ "- `--chromosome`: restrict to a subset of chromosomes.\n",
+ "- `--snp-list`: HapMap3-style SNP list. Its presence switches the polyfun call: given a list, `ldsc\n",
+ " --l2 --print-snps` writes `.l2.ldscore.gz`; without one, `compute_ldscores` writes\n",
+ " `.l2.ldscore.parquet`. It also changes the LD-score column name that `postprocess` matches on, so\n",
+ " `--target-categories` may need to be passed explicitly.\n",
+ "- `--ld-wind-kb` (default `0`) / `--ld-wind-cm` (default `1.0`): LD window. Using the cM window\n",
+ " requires genetic-map positions in the `.bim` fourth column; switch to `--ld-wind-kb` if it holds 0.\n",
+ "- `--compute-single` / `--no-compute-single` and `--compute-joint` / `--no-compute-joint` (both default\n",
+ " on): whether to emit the per-annotation single-target and the joint directories. The joint directory\n",
+ " is only emitted for two or more annotations.\n",
+ "- `--score-column` (default `3`) and `--is-range` / `--no-is-range` (default off): which column carries\n",
+ " the annotation score, and whether the annotation file is interval- or position-based.\n",
+ "\n",
+ "With `--no-is-range`, one row per position and an optional score column, defaulting to 1:\n",
+ "\n",
+ " ```\n",
+ " chr\tpos\tscore\n",
+ " 1\t10001\t1\n",
+ " 1\t10002\t1\n",
+ " ```\n",
+ "\n",
+ "With `--is-range`, one row per interval:\n",
+ "\n",
+ " ```\n",
+ " chr\tstart\tend\tscore\n",
+ " 1\t10001\t20001\t1\n",
+ " 1\t30001\t40001\t1\n",
+ " ```\n",
+ "\n",
+ "`get_heritability` -- run the S-LDSC regression. All LD-derived inputs (baseline LD scores, target LD\n",
+ "scores, regression weights, allele frequencies) must be computed against the same reference panel as\n",
+ "`--genome-ref-file`; files from different panels (1000G versus ADSP, say) must not be mixed:\n",
+ "\n",
+ "- `--all-traits-file`: list of munged summary statistics to run, one per line.\n",
+ "- `--sumstat-dir`: directory holding those summary statistics.\n",
+ "- `--target-anno-dir` / `--target-anno-dirs`: the LD-score directory or directories written by\n",
+ " `make_annotation_files_ldscore`. Each (trait, target directory) pair is one polyfun call.\n",
+ "- `--baseline-ld-dir` and `--baseline-name` (default `baseline_chr`): baseline LD scores and their\n",
+ " per-chromosome file prefix.\n",
+ "- `--frqfile-dir` and `--plink-name` (default `ADSP_chr`): PLINK `.frq` allele frequencies, used to\n",
+ " enforce the MAF cutoff.\n",
+ "- `--weights-dir` and `--weight-name` (default `weights_chr`): LD-score regression weights.\n",
+ "- `--n-blocks` (default `200`): jackknife blocks for the standard errors.\n",
+ "- `--all-traits`: run every trait found rather than the list.\n",
+ "\n",
+ "`munge_sumstats_polyfun` -- optional preprocessing, run before `get_heritability`:\n",
+ "\n",
+ "- `--sumstats` (required): raw GWAS summary statistics to convert to LDSC format.\n",
+ "- `--n`: sample size, when the file does not carry one.\n",
+ "- `--min-info` (default `0.6`), `--min-maf` (default `0.001`), `--chi2-cut` (default `30`): quality and\n",
+ " outlier filters.\n",
+ "- `--keep-hla` / `--no-keep-hla` (default off): retain the HLA region.\n",
+ "\n",
+ "`postprocess` -- standardize tau and meta-analyse across traits:\n",
+ "\n",
+ "- `--traits-file`: traits to include.\n",
+ "- `--heritability-cwd`: the `get_heritability` output directory to read.\n",
+ "- `--target-categories` and `--target-categories-label`: which `.results` `Category` rows are the\n",
+ " target annotation, and how to label them. Auto-detected unless `--snp-list` changed the column name.\n",
+ "\n",
+ "`meta_subset` -- optional re-meta over a trait subset, no regression re-run:\n",
+ "\n",
+ "- `--postprocess-rds`: the cached `.sldsc_postprocess.rds`.\n",
+ "- `--subset-traits-file` and `--subset-name` (required): the trait subset and its output label.\n",
+ "- `--target-categories`: as above.\n",
+ "\n",
+ "Cluster resources: `--numThreads` (default `16`), `--job-size`, `--walltime`, `--mem`."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "## Output\n",
+ "\n",
+ "- `/` under the LD-score directory -- from `make_annotation_files_ldscore`: polyfun\n",
+ " `.annot.gz` files plus per-annotation LD scores (`.l2.ldscore.parquet` or `.l2.ldscore.gz`, `.l2.M`,\n",
+ " `.l2.M_5_50`). One single-target directory per annotation, plus a joint directory when there is more\n",
+ " than one. Recomputable from the annotation and reference panel, and read by every regression.\n",
+ "- `.` -- from `get_heritability`: the S-LDSC regression output,\n",
+ " one set per trait per target directory. The `.results` `Category` column carries the annotation name\n",
+ " with a `_` suffix.\n",
+ "- `.sldsc_postprocess.rds` -- from `postprocess`: a single object holding per-trait\n",
+ " tables (Gazal-style tau*, EnrichStat with back-solved jackknife SE) and three DerSimonian-Laird\n",
+ " random-effects meta tables (tau*, E, EnrichStat).\n",
+ "- `` tables -- from `meta_subset`: a re-meta of the cached `.sldsc_postprocess.rds` over a\n",
+ " user-defined trait subset. Lightweight: no regression is re-run.\n",
+ "\n",
+ "Each stage writes into its own `--cwd`, and leaves a `.stdout` / `.stderr` log and the generated shell\n",
+ "or R script beside its targets.\n",
+ "\n",
+ "LD scores for the example annotation, under `output/sldsc_ldscore/protocol_example_single_1/`:\n",
+ "\n",
+ "```\n",
+ "protocol_example_single_1.19.annot.gz\n",
+ "protocol_example_single_1.19.l2.ldscore.parquet\n",
+ "protocol_example_single_1.19.l2.M\n",
+ "protocol_example_single_1.19.l2.ldscore.parquet.log\n",
+ "```\n",
+ "\n",
+ "The regression result, `output/sldsc_heritability/protocol_example_single_1/sumstats.parquet.results`\n",
+ "(first four columns of each row shown; the file also carries `Enrichment`, `Enrichment_std_error`,\n",
+ "`Enrichment_p`, `Coefficient`, `Coefficient_std_error` and `Coefficient_z-score`):\n",
+ "\n",
+ "```\n",
+ "Category\tProp._SNPs\tProp._h2\tProp._h2_std_error\n",
+ "ANNOT_0\t1.6443e-02\t4.1714e-01\t2.9297e-01\n",
+ "Coding_UCSC_common_1\t5.2671e-03\t1.5724e-01\t1.0492e-01\n",
+ "```\n",
"\n",
- "The steps below run the four pipeline workflows end to end on the example data. Each step lists what it does, then the `sos run` command to execute it.\n"
+ "`postprocess` and `meta_subset` have no example output committed: `output/sldsc_postprocess/` holds only\n",
+ "the generated R script and its log, so no `.sldsc_postprocess.rds` was produced for the toy data."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "## Minimal Working Example"
]
},
{
@@ -267,7 +387,7 @@
"kernel": "SoS"
},
"source": [
- "## Step 1. `make_annotation_files_ldscore`\n",
+ "#### Step 1. `make_annotation_files_ldscore`\n",
"\n",
"*Annotation preparation and S-LDSC regression (polyfun).* This step accepts a single annotation file for a single-tau analysis (one annotation as input) or several annotation files for a joint-tau analysis (multiple annotations as input)."
]
@@ -278,90 +398,16 @@
"kernel": "SoS"
},
"source": [
- "#### **Inputs**\n",
- "\n",
- "##### 1. Target Annotation File\n",
- "\n",
- "- **Purpose**: Specifies the user-provided (\"target\") genome annotation files. The pipeline supports both binary and continuous annotations; the type is auto-detected per annotation column.\n",
- "- **Formats**:\n",
- " - Text file (`.txt`) listing per-chromosome paths to annotation files. Annotation files can be `.rds`/`.tsv`/`.txt`.\n",
- " - Alternatively, files for specific chromosomes can be provided directly.\n",
- " - **Multiple target annotations** are supported in one input file (one column per annotation, prefixed `path`, `path1`, `path2`, ...). Single-target and joint-target analyses are produced automatically in one pipeline pass.\n",
- " - **Format** (the score column is optional; if absent, score is set to 1):\n",
- " - `is_range = False`:\n",
- " ```\n",
- " chr pos score\n",
- " 1 10001 1\n",
- " 1 10002 1\n",
- " ```\n",
- " - `is_range = True`:\n",
- " ```\n",
- " chr start end score\n",
- " 1 10001 20001 1\n",
- " 1 30001 40001 1\n",
- " ```\n",
- "\n",
- "##### 2. Reference Annotation File (baseline-LD)\n",
- "\n",
- "- **Purpose**: Provides the baseline annotations (typically the 97-annotation baseline-LD model from Gazal et al. 2017) in `.annot.gz` format for each chromosome. The baseline conditions every regression.\n",
- "- **Formats**:\n",
- " - Text file listing baseline annotation files for all chromosomes.\n",
- " - Alternatively, files for specific chromosomes can be provided directly.\n",
- "\n",
- "##### 3. Genome Reference File\n",
- "\n",
- "- **Purpose**: PLINK-format `.bed/.bim/.fam` files for our LD reference panel, per chromosome. This is the panel against which all LD-derived quantities (target LD scores, baseline LD scores, regression weights, allele frequencies) must be computed. **Do not mix files derived from different panels** (e.g. 1000G vs ADSP).\n",
- "- **Formats**:\n",
- " - Text file listing per-chromosome reference files, or files for specific chromosomes.\n",
- "\n",
- "##### 4. SNP List\n",
- "\n",
- "- **Purpose**: Specifies the SNPs to include in LDSC analysis (typically a HapMap3-style list).\n",
- "- **Format**: A list of `rsid`s, one per line.\n",
- "\n"
+ "**Timing**: TBD (on toy dataset)"
]
},
{
"cell_type": "code",
- "execution_count": 9,
+ "execution_count": null,
"metadata": {
- "kernel": "Bash"
+ "kernel": "SoS"
},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "/restricted/projectnb/xqtl/jaempawi/.pixi/envs/python/lib/python3.12/site-packages/sos/targets.py:22: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.\n",
- " import pkg_resources\n",
- "INFO: Running \u001b[32mmake_annotation_files_ldscore\u001b[0m: \n",
- "INFO: \u001b[32mmake_annotation_files_ldscore\u001b[0m (index=1) is \u001b[32mcompleted\u001b[0m.\n",
- "INFO: \u001b[32mmake_annotation_files_ldscore\u001b[0m (index=3) is \u001b[32mcompleted\u001b[0m.\n",
- "INFO: \u001b[32mmake_annotation_files_ldscore\u001b[0m (index=2) is \u001b[32mcompleted\u001b[0m.\n",
- "INFO: \u001b[32mmake_annotation_files_ldscore\u001b[0m (index=0) is \u001b[32mcompleted\u001b[0m.\n",
- "INFO: \u001b[32mmake_annotation_files_ldscore\u001b[0m (index=5) is \u001b[32mcompleted\u001b[0m.\n",
- "INFO: \u001b[32mmake_annotation_files_ldscore\u001b[0m (index=6) is \u001b[32mcompleted\u001b[0m.\n",
- "INFO: \u001b[32mmake_annotation_files_ldscore\u001b[0m (index=4) is \u001b[32mcompleted\u001b[0m.\n",
- "INFO: \u001b[32mmake_annotation_files_ldscore\u001b[0m (index=7) is \u001b[32mcompleted\u001b[0m.\n",
- "INFO: \u001b[32mmake_annotation_files_ldscore\u001b[0m (index=9) is \u001b[32mcompleted\u001b[0m.\n",
- "INFO: \u001b[32mmake_annotation_files_ldscore\u001b[0m (index=10) is \u001b[32mcompleted\u001b[0m.\n",
- "INFO: \u001b[32mmake_annotation_files_ldscore\u001b[0m (index=8) is \u001b[32mcompleted\u001b[0m.\n",
- "INFO: \u001b[32mmake_annotation_files_ldscore\u001b[0m (index=11) is \u001b[32mcompleted\u001b[0m.\n",
- "INFO: \u001b[32mmake_annotation_files_ldscore\u001b[0m (index=14) is \u001b[32mcompleted\u001b[0m.\n",
- "INFO: \u001b[32mmake_annotation_files_ldscore\u001b[0m (index=13) is \u001b[32mcompleted\u001b[0m.\n",
- "INFO: \u001b[32mmake_annotation_files_ldscore\u001b[0m (index=12) is \u001b[32mcompleted\u001b[0m.\n",
- "INFO: \u001b[32mmake_annotation_files_ldscore\u001b[0m (index=15) is \u001b[32mcompleted\u001b[0m.\n",
- "INFO: \u001b[32mmake_annotation_files_ldscore\u001b[0m (index=18) is \u001b[32mcompleted\u001b[0m.\n",
- "INFO: \u001b[32mmake_annotation_files_ldscore\u001b[0m (index=16) is \u001b[32mcompleted\u001b[0m.\n",
- "INFO: \u001b[32mmake_annotation_files_ldscore\u001b[0m (index=17) is \u001b[32mcompleted\u001b[0m.\n",
- "INFO: \u001b[32mmake_annotation_files_ldscore\u001b[0m (index=19) is \u001b[32mcompleted\u001b[0m.\n",
- "INFO: \u001b[32mmake_annotation_files_ldscore\u001b[0m (index=21) is \u001b[32mcompleted\u001b[0m.\n",
- "INFO: \u001b[32mmake_annotation_files_ldscore\u001b[0m (index=20) is \u001b[32mcompleted\u001b[0m.\n",
- "INFO: \u001b[32mmake_annotation_files_ldscore\u001b[0m output: \u001b[32m/restricted/projectnb/xqtl/jaempawi/xqtl-protocol/output/sldsc_ldscore/protocol_example_single_1/protocol_example_single_1.1.annot.gz /restricted/projectnb/xqtl/jaempawi/xqtl-protocol/output/sldsc_ldscore/protocol_example_single_1/protocol_example_single_1.1.l2.ldscore.parquet... (66 items in 22 groups)\u001b[0m\n",
- "INFO: Workflow make_annotation_files_ldscore (ID=weae0ca3fdf468fd8) is executed successfully with 1 completed step and 22 completed substeps.\n"
- ]
- }
- ],
+ "outputs": [],
"source": [
"sos run pipeline/sldsc_enrichment.ipynb make_annotation_files_ldscore \\\n",
" --annotation_file input/enrichment/sldsc/colocboost_test_annotation_path.txt \\\n",
@@ -369,7 +415,6 @@
" --genome_ref_file input/enrichment/sldsc/genome_reference_bfile.txt \\\n",
" --annotation_name protocol_example \\\n",
" --plink_name reference. --baseline_name annotations. --weight_name weights. \\\n",
- " \\\n",
" --cwd output/sldsc_ldscore -j 4\n"
]
},
@@ -379,7 +424,7 @@
"kernel": "SoS"
},
"source": [
- "### Munge summary statistics (preprocessing, run before Step 2)\n",
+ "#### Munge summary statistics (preprocessing, run before Step 2)\n",
"\n",
"Before estimating heritability, each raw GWAS summary-statistics file must be converted into the LDSC-compatible format consumed by `get_heritability`. Run `munge_sumstats_polyfun` once per trait; the munged files are then collected in the directory passed to `get_heritability` via `--sumstat_dir`.\n"
]
@@ -407,7 +452,7 @@
"kernel": "SoS"
},
"source": [
- "## Step 2. `get_heritability`\n"
+ "#### Step 2. `get_heritability`\n"
]
},
{
@@ -416,51 +461,16 @@
"kernel": "SoS"
},
"source": [
- "**Inputs**\n",
- "\n",
- "##### 1. Allele Frequency Files (`.frq`, our panel)\n",
- "\n",
- "- **Purpose**: PLINK `.frq` files for the reference panel, used to enforce the MAF cutoff. **Required** when `maf_cutoff > 0` (default `0.05`); the pipeline fails if missing unless `maf_cutoff = 0` is explicitly set.\n",
- "\n",
- "##### 2. GWAS Summary Statistics\n",
- "\n",
- "- **Purpose**: One munged sumstats file per trait, listed in a text file (`all_traits_file`). The pipeline runs the regression once per trait per single/joint mode.\n",
- "- **Format**:\n",
- " ```\n",
- " CAD_META.filtered.sumstats.gz\n",
- " UKB.Lym.BOLT.sumstats.gz\n",
- " ```\n",
- "\n"
+ "**Timing**: TBD (on toy dataset)"
]
},
{
"cell_type": "code",
- "execution_count": 10,
+ "execution_count": null,
"metadata": {
- "kernel": "Bash"
+ "kernel": "SoS"
},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "/restricted/projectnb/xqtl/jaempawi/.pixi/envs/python/lib/python3.12/site-packages/sos/targets.py:22: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.\n",
- " import pkg_resources\n",
- "INFO: Running \u001b[32mget_heritability\u001b[0m: \n",
- "maf_cutoff = 0: skipping MAF filter (--not-M-5-50)\n",
- "maf_cutoff = 0: skipping MAF filter (--not-M-5-50)\n",
- "maf_cutoff = 0: skipping MAF filter (--not-M-5-50)\n",
- "python: can't open file '/restricted/projectnb/xqtl/jaempawi/xqtl-protocol/../polyfun/ldsc.py': [Errno 2] No such file or directory\n",
- "python: can't open file '/restricted/projectnb/xqtl/jaempawi/xqtl-protocol/../polyfun/ldsc.py': [Errno 2] No such file or directory\n",
- "python: can't open file '/restricted/projectnb/xqtl/jaempawi/xqtl-protocol/../polyfun/ldsc.py': [Errno 2] No such file or directory\n",
- "INFO: \u001b[32mget_heritability\u001b[0m (index=1) is \u001b[32mcompleted\u001b[0m.\n",
- "INFO: \u001b[32mget_heritability\u001b[0m (index=0) is \u001b[32mcompleted\u001b[0m.\n",
- "INFO: \u001b[32mget_heritability\u001b[0m (index=2) is \u001b[32mcompleted\u001b[0m.\n",
- "INFO: \u001b[32mget_heritability\u001b[0m output: \u001b[32m/restricted/projectnb/xqtl/jaempawi/xqtl-protocol/output/sldsc_heritability/protocol_example_single_1/sumstats.parquet.log /restricted/projectnb/xqtl/jaempawi/xqtl-protocol/output/sldsc_heritability/protocol_example_single_1/sumstats.parquet.results... (6 items in 3 groups)\u001b[0m\n",
- "INFO: Workflow get_heritability (ID=wa79eac1662f5dd2d) is executed successfully with 1 completed step and 3 completed substeps.\n"
- ]
- }
- ],
+ "outputs": [],
"source": [
"sos run pipeline/sldsc_enrichment.ipynb get_heritability \\\n",
" --target_anno_dirs output/sldsc_ldscore/protocol_example_single_1 \\\n",
@@ -480,50 +490,35 @@
"kernel": "SoS"
},
"source": [
- "## Step 3. `Post-processing (pecotmr) and meta-analysis`\n",
- "\n",
- "*Post-Processing (`pecotmr::sldscPostprocessingPipeline`)*\n",
+ "#### Step 3. `Post-processing (pecotmr) and meta-analysis`\n",
"\n",
- "A single R function call consumes all polyfun outputs for the run and produces the final tables:\n",
+ "Reads every polyfun output under `--heritability-cwd` and calls\n",
+ "`pecotmr::sldscPostprocessingPipeline` to produce the per-trait standardized tables and the default\n",
+ "random-effects meta across all traits.\n",
"\n",
- "- Reads each polyfun output and extracts $\\tau$, $E$, $h^2_g$, EnrichStat p-value, and per-block jackknife $\\tau$ values.\n",
- "- Computes annotation $sd_C$ and $M_{\\mathrm{ref}}$ over the same MAF $>$ cutoff SNP set as the regression.\n",
- "- Standardizes $\\tau \\to \\tau^*$ for both single-tau and joint-tau modes, including the per-block versions for jackknife SE.\n",
- "- Auto-detects whether each annotation is binary or continuous and tags every output row accordingly.\n",
- "- Reports the number and names of baseline annotations encountered (via `message()`) for transparency.\n",
- "- Runs the default DerSimonian-Laird random-effects meta-analysis across all supplied traits, producing three meta tables: $\\tau^*$ (cross-type comparable), $E$ (within-binary), and EnrichStat (within-type).\n",
- "\n",
- "Outputs are returned as an R list with two top-level entries: `per_trait` (one tidy data frame per trait, single + joint estimates side-by-side per target) and `meta` (three tables, one per quantity, with rows = target annotations and columns = single/joint mean/SE/p plus an `is_binary` flag).\n",
- "\n",
- "The `[postprocess]` step reads all polyfun outputs under `heritability_cwd`\n",
- "(which contains the $N$ single-target subdirectories and optionally the\n",
- "joint subdirectory) and calls `pecotmr::sldscPostprocessingPipeline()`\n",
- "to produce per-trait standardized tables and the default random-effects\n",
- "meta across all traits.\n",
- "\n",
- "Use `--target-categories-label` (same order as `--target-categories`) to give the target annotations friendly names in the output — e.g. `--target-categories ANNOT_1_0 ANNOT_2_0 --target-categories-label quantile_eQTL eQTL` makes the `target` column read `quantile_eQTL` / `eQTL` instead of `ANNOT_1_0` / `ANNOT_2_0` (the original names are kept in `params$target_categories_orig`). Omit it to keep the polyfun `.results` names.\n"
+ "`--target-categories-label` (same order as `--target-categories`) gives the target annotations\n",
+ "friendly names in the output -- `--target-categories ANNOT_1_0 ANNOT_2_0 --target-categories-label\n",
+ "quantile_eQTL eQTL` makes the `target` column read `quantile_eQTL` / `eQTL` rather than `ANNOT_1_0` /\n",
+ "`ANNOT_2_0`. The original names stay in `params$target_categories_orig`; omit the flag to keep the\n",
+ "polyfun `.results` names."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
]
},
{
"cell_type": "code",
- "execution_count": 15,
+ "execution_count": null,
"metadata": {
- "kernel": "Bash"
+ "kernel": "SoS"
},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "/restricted/projectnb/xqtl/jaempawi/.pixi/envs/python/lib/python3.12/site-packages/sos/targets.py:22: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.\n",
- " import pkg_resources\n",
- "INFO: Running \u001b[32mpostprocess\u001b[0m: \n",
- "INFO: \u001b[32mpostprocess\u001b[0m is \u001b[32mcompleted\u001b[0m.\n",
- "INFO: \u001b[32mpostprocess\u001b[0m output: \u001b[32m/restricted/projectnb/xqtl/jaempawi/xqtl-protocol/output/sldsc_postprocess/protocol_example.sldsc_postprocess.rds\u001b[0m\n",
- "INFO: Workflow postprocess (ID=wb64dc2b84958960c) is executed successfully with 1 completed step.\n"
- ]
- }
- ],
+ "outputs": [],
"source": [
"sos run pipeline/sldsc_enrichment.ipynb postprocess \\\n",
" --traits_file input/enrichment/sldsc/sumstats_test_all.txt \\\n",
@@ -531,8 +526,7 @@
" --target_categories ANNOT_0 --target_categories_label protocol_example_annotation \\\n",
" --target_anno_dir output/sldsc_ldscore/protocol_example_single_1 \\\n",
" --annotation_name protocol_example \\\n",
- " --maf_cutoff 0 --cwd output/sldsc_postprocess -j 4\n",
- "\n"
+ " --maf_cutoff 0 --cwd output/sldsc_postprocess -j 4\n"
]
},
{
@@ -541,10 +535,11 @@
"kernel": "SoS"
},
"source": [
- "## Step 4. `Subset Meta-Analysis (`pecotmr::metaSldscRandom`)` (optional)\n",
- "\n",
- "The default meta in Step 2 pools all traits the user supplied. To re-run the meta on a subset (e.g., neurodegenerative traits only, or autoimmune traits only) without re-running the regression layer:\n",
+ "#### Step 4. `Subset Meta-Analysis (`pecotmr::metaSldscRandom`)` (optional)\n",
"\n",
+ "Step 3 pools every trait passed to `postprocess`. To re-meta a subset -- neurodegenerative traits\n",
+ "only, say -- without re-running the regression or the per-trait standardization, `meta_subset` works\n",
+ "from the cached `.sldsc_postprocess.rds`. It is light enough to run interactively:\n",
"\n",
"```r\n",
"res <- readRDS(\"sldsc_results.rds\")\n",
@@ -553,42 +548,7 @@
"meta_neuro_taustar <- pecotmr::metaSldscRandom(\n",
" view_neuro, category = \"my_target_anno\", quantity = \"tauStar\"\n",
")\n",
- "```\n",
- "\n",
- "This step is light-weight and can be run interactively.\n",
- "\n",
- "\n",
- "The default meta in step 3 pools all traits supplied to `[postprocess]`. Use `[meta_subset]` to re-run the meta on a user-defined trait subset (e.g., neurodegenerative traits only, autoimmune traits only) without re-running the regression or the per-trait standardization. The subset operates on the cached `.sldsc_postprocess.rds` output; it is light-weight and can be run interactively or in batch.\n",
- "\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 17,
- "metadata": {
- "kernel": "Bash"
- },
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "/restricted/projectnb/xqtl/jaempawi/.pixi/envs/python/lib/python3.12/site-packages/sos/targets.py:22: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.\n",
- " import pkg_resources\n",
- "INFO: Running \u001b[32mmeta_subset\u001b[0m: \n",
- "INFO: \u001b[32mmeta_subset\u001b[0m is \u001b[32mcompleted\u001b[0m.\n",
- "INFO: \u001b[32mmeta_subset\u001b[0m output: \u001b[32m/restricted/projectnb/xqtl/jaempawi/xqtl-protocol/output/sldsc_postprocess/protocol_example.category1.meta.rds\u001b[0m\n",
- "INFO: Workflow meta_subset (ID=w09a2a0530119f1d2) is executed successfully with 1 completed step.\n"
- ]
- }
- ],
- "source": [
- "sos run pipeline/sldsc_enrichment.ipynb meta_subset \\\n",
- " --postprocess_rds output/sldsc_postprocess/protocol_example.sldsc_postprocess.rds \\\n",
- " --subset_traits_file input/enrichment/sldsc/sumstats_test_category1.txt \\\n",
- " --subset_name category1 --target_categories ANNOT_0 \\\n",
- " --annotation_name protocol_example \\\n",
- " --maf_cutoff 0 --cwd output/sldsc_postprocess -j 4\n"
+ "```"
]
},
{
@@ -597,33 +557,23 @@
"kernel": "SoS"
},
"source": [
- "## Output\n",
- "\n",
- "### Output summary\n",
- "\n",
- "| Stage | Cached on disk | Recomputable from | Purpose |\n",
- "|---|---|---|---|\n",
- "| Target LD scores | per-annotation, once | annotation + reference panel | input to every regression |\n",
- "| polyfun `.results` per (trait, mode) | yes | regression run | $\\tau$, $E$, EnrichStat |\n",
- "| Per-trait standardized table | yes (RDS) | polyfun outputs + $sd_C$ + $M_{\\mathrm{ref}}$ | reporting + meta |\n",
- "| Default meta tables | yes (RDS) | per-trait standardized | headline figures |\n",
- "| Subset meta | re-run on demand | per-trait standardized | custom analyses |\n"
+ "**Timing**: TBD (on toy dataset)"
]
},
{
- "cell_type": "markdown",
+ "cell_type": "code",
+ "execution_count": null,
"metadata": {
"kernel": "SoS"
},
+ "outputs": [],
"source": [
- "### Per-stage outputs\n",
- "\n",
- "Each workflow writes into its `--cwd`:\n",
- "\n",
- "- **make_annotation_files_ldscore** — polyfun `.annot.gz` files plus per-annotation LD-score directories (`.l2.ldscore.{gz,parquet}`, `.l2.M`, `.l2.M_5_50`). One single-target directory per annotation, plus (when more than one annotation) a joint directory.\n",
- "- **get_heritability** — per trait and per target directory, the S-LDSC regression outputs `.{results,log,part_delete}`. The `.results` `Category` column carries the annotation name with a `_` suffix.\n",
- "- **postprocess** — a single `.sldsc_postprocess.rds` containing per-trait tables (Gazal-style tau*, EnrichStat with back-solved jackknife SE) and three DerSimonian–Laird random-effects meta tables (tau*, E, EnrichStat).\n",
- "- **meta_subset** — a re-meta of the cached `.sldsc_postprocess.rds` over a user-defined trait subset (lightweight; no regression re-run).\n"
+ "sos run pipeline/sldsc_enrichment.ipynb meta_subset \\\n",
+ " --postprocess_rds output/sldsc_postprocess/protocol_example.sldsc_postprocess.rds \\\n",
+ " --subset_traits_file input/enrichment/sldsc/sumstats_test_category1.txt \\\n",
+ " --subset_name category1 --target_categories ANNOT_0 \\\n",
+ " --annotation_name protocol_example \\\n",
+ " --maf_cutoff 0 --cwd output/sldsc_postprocess -j 4"
]
},
{
@@ -632,31 +582,176 @@
"kernel": "SoS"
},
"source": [
- "## Anticipated Results\n",
- "\n",
- "Produces per-annotation enrichment statistics (tau, enrichment, p-value) from stratified LD score regression."
+ "## Command Interface"
]
},
{
- "cell_type": "markdown",
+ "cell_type": "code",
+ "execution_count": null,
"metadata": {
"kernel": "SoS"
},
+ "outputs": [],
"source": [
- "## Command interface\n",
- "\n",
- "List all workflows and their options:\n"
+ "sos run pipeline/sldsc_enrichment.ipynb -h"
]
},
{
- "cell_type": "code",
- "execution_count": null,
+ "cell_type": "markdown",
"metadata": {
- "kernel": "Bash"
+ "kernel": "SoS"
},
- "outputs": [],
"source": [
- "sos run pipeline/sldsc_enrichment.ipynb -h"
+ "```\n",
+ "usage: sos run pipeline/sldsc_enrichment.ipynb\n",
+ " [workflow_name | -t targets] [options] [workflow_options]\n",
+ " workflow_name: Single or combined workflows defined in this script\n",
+ " targets: One or more targets to generate\n",
+ " options: Single-hyphen sos parameters (see \"sos run -h\" for details)\n",
+ " workflow_options: Double-hyphen workflow-specific parameters\n",
+ "\n",
+ "Workflows:\n",
+ " make_annotation_files_ldscore\n",
+ " get_heritability\n",
+ " munge_sumstats_polyfun\n",
+ " postprocess\n",
+ " meta_subset\n",
+ "\n",
+ "Global Workflow Options:\n",
+ " --cwd output (as path)\n",
+ " Path to the work directory of the analysis.\n",
+ " --annotation-name VAL (as str, required)\n",
+ " Prefix for the analysis output\n",
+ " --modular-script-dir code/script (as path)\n",
+ " Directory holding the modular R wrapper scripts\n",
+ " (code/script).\n",
+ " --maf-cutoff 0.05 (as float)\n",
+ " MAF cutoff for sLDSC. Default 0.05 per sLDSC\n",
+ " recommendation (rare-variant LD is unstable and\n",
+ " HapMap3-style regression weights are common-variant by\n",
+ " construction). Set to 0 to opt out of MAF filtering (NOT\n",
+ " recommended; only use if you understand the\n",
+ " implications). Other values would require recomputing LD\n",
+ " scores at that cutoff.\n",
+ " --annotation-file . (as path)\n",
+ " for make_annotation_files_ldscore workflow:\n",
+ " --reference-anno-file . (as path)\n",
+ " --genome-ref-file . (as path)\n",
+ " --chromosome (as list)\n",
+ " --snp-list . (as path)\n",
+ " --ld-wind-kb 0 (as int)\n",
+ " --ld-wind-cm 1.0 (as float)\n",
+ " --all-traits-file . (as path)\n",
+ " for get_heritability workflow. Note: all LD-derived\n",
+ " inputs (baseline LD scores, target LD scores, regression\n",
+ " weights, allele frequencies) must be computed against\n",
+ " the same reference panel as `genome_ref_file`. Do not\n",
+ " mix files derived from different reference panels (e.g.,\n",
+ " 1000G vs ADSP).\n",
+ " --sumstat-dir . (as path)\n",
+ " --target-anno-dir . (as path)\n",
+ " --baseline-ld-dir . (as path)\n",
+ " --frqfile-dir . (as path)\n",
+ " --plink-name 'ADSP_chr'\n",
+ " --weights-dir . (as path)\n",
+ " --baseline-name 'baseline_chr'\n",
+ " --weight-name 'weights_chr'\n",
+ " --n-blocks 200 (as int)\n",
+ " --numThreads 16 (as int)\n",
+ " Number of threads\n",
+ " --job-size 1 (as int)\n",
+ " For cluster jobs, number commands to run per job\n",
+ " --walltime 12h\n",
+ " --mem 16G\n",
+ "\n",
+ "Sections\n",
+ " make_annotation_files_ldscore:\n",
+ " Workflow Options:\n",
+ " --[no-]compute-single (default to True)\n",
+ " --- snplist (--snp_list) vs no-snplist: which polyfun\n",
+ " script, output format, column name, and the CM\n",
+ " requirement --- --snp_list given -> ldsc --l2 --print-\n",
+ " snps -> output .l2.ldscore.gz --snp_list absent ->\n",
+ " compute_ldscores -> output .l2.ldscore.parquet\n",
+ " LD-score column name (this is what becomes the .results\n",
+ " \"Category\" in [get_heritability], with a \"_\" suffix appended there): * compute_ldscores\n",
+ " ALWAYS keeps the annot column name(s): single annot\n",
+ " column \"ANNOT\" -> ldscore column \"ANNOT\" joint\n",
+ " annot columns \"ANNOT_1\",\"ANNOT_2\",... ->\n",
+ " \"ANNOT_1\",\"ANNOT_2\",... * ldsc --l2 has a quirk: with\n",
+ " EXACTLY ONE annotation (n_annot == 1) it HARD-CODES the\n",
+ " ldscore column name to \"L2\" and DROPS the annot's\n",
+ " original column name. With >=2 annotations it uses\n",
+ " \"L2\" (\"ANNOT_1L2\",\"ANNOT_2L2\",...). => a\n",
+ " single-target snplist run reports \"L2_0\" in .results,\n",
+ " while a single-target no-snplist run reports \"ANNOT_0\".\n",
+ " [postprocess] auto- detects either; only matters if you\n",
+ " pass --target-categories explicitly. CM column\n",
+ " requirement for snplist: ldsc --l2 --print-snps\n",
+ " requires the target annot to (a) carry a \"CM\"\n",
+ " (centimorgan) column and (b) line up with the plink .bim\n",
+ " (same SNP set, same row order). This step handles both\n",
+ " internally (normalize_for_ldsc: takes CM from the .bim\n",
+ " 4th column, re-expands the annot onto the .bim rows,\n",
+ " filling 0). Therefore the plink .bim files MUST carry\n",
+ " genetic-map (cM) positions when using --ld-wind-cm (the\n",
+ " default); if your .bim has 0 in the cM column, switch to\n",
+ " --ld-wind-kb instead.\n",
+ " --[no-]compute-joint (default to True)\n",
+ " --score-column 3 (as int)\n",
+ " --[no-]is-range (default to False)\n",
+ " get_heritability:\n",
+ " Workflow Options:\n",
+ " --target-anno-dirs paths()\n",
+ "\n",
+ " --- about the \".results\" Category column and the \"_0 /\n",
+ " _1\" suffix --- Each (trait, target_dir) pair is ONE\n",
+ " polyfun call; its `ldsc --ref-ld-chr` always gets\n",
+ " exactly two LD-score sources, in this order:\n",
+ " \"/.\" (index 0) ,\n",
+ " \"/\" (index 1) With --overlap-\n",
+ " annot, every annotation column in the .results\n",
+ " \"Category\" is named _: index 0 = the target file -> \"ANNOT_0\" (no-\n",
+ " snplist; compute_ldscores keeps the annot col name) ->\n",
+ " \"L2_0\" (snplist + single annot; ldsc hard-codes \"L2\",\n",
+ " see below) -> \"ANNOT_1_0\",\"ANNOT_2_0\" (no-snplist\n",
+ " joint dir, N>=2 annot cols) ->\n",
+ " \"ANNOT_1L2_0\",\"ANNOT_2L2_0\" (snplist joint dir, N>=2 ->\n",
+ " \"L2\") index 1 = the baseline file ->\n",
+ " \"base_1\",\"Coding_UCSC_1\", ... (the 97 baseline annots)\n",
+ " So in this pipeline the suffix is only ever 0 (target)\n",
+ " or 1 (baseline); it would continue 0,1,2,... only if you\n",
+ " handed `ldsc --ref-ld-chr` more than two sources. (Why\n",
+ " ANNOT_0 vs L2_0: see the [make_annotation_files_ldscore]\n",
+ " header — ldsc's \"n_annot == 1 -> column name 'L2'\" quirk\n",
+ " vs compute_ldscores keeping the annot column name.)\n",
+ " [postprocess] auto-detects the target Category; if you\n",
+ " instead pass --target-categories, the names must match\n",
+ " this column exactly.\n",
+ " --all-traits (as list)\n",
+ " munge_sumstats_polyfun:\n",
+ " Workflow Options:\n",
+ " --sumstats VAL (as path, required)\n",
+ " --n 0 (as int)\n",
+ " --min-info 0.6 (as float)\n",
+ " --min-maf 0.001 (as float)\n",
+ " --[no-]keep-hla (default to False)\n",
+ " --chi2-cut 30 (as int)\n",
+ " postprocess:\n",
+ " Workflow Options:\n",
+ " --traits-file . (as path)\n",
+ " --heritability-cwd . (as path)\n",
+ " --target-categories (as list)\n",
+ " --target-categories-label (as list)\n",
+ " meta_subset:\n",
+ " Workflow Options:\n",
+ " --postprocess-rds . (as path)\n",
+ " --subset-traits-file . (as path)\n",
+ " --subset-name VAL (as str, required)\n",
+ " --target-categories (as list)\n",
+ "```"
]
},
{
@@ -730,7 +825,7 @@
"kernel": "Python 3 (ipykernel)"
},
"source": [
- "## Make Annotation File"
+ "### Make Annotation File"
]
},
{
@@ -961,7 +1056,7 @@
"kernel": "Python 3 (ipykernel)"
},
"source": [
- "## Calculate Functional Enrichment using Annotations"
+ "### Calculate Functional Enrichment using Annotations"
]
},
{
diff --git a/code/SoS/mnm_analysis/mnm_methods/colocboost.ipynb b/code/SoS/mnm_analysis/mnm_methods/colocboost.ipynb
index a1d30209e..38f326a4e 100644
--- a/code/SoS/mnm_analysis/mnm_methods/colocboost.ipynb
+++ b/code/SoS/mnm_analysis/mnm_methods/colocboost.ipynb
@@ -6,27 +6,9 @@
"kernel": "SoS"
},
"source": [
- "# Multi-trait colocalization using ColocBoost"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "## Prerequisites\n",
+ "# Multi-trait colocalization using ColocBoost\n",
"\n",
- "Requires fine-mapping outputs (`.susie.rds` files) from `mnm_regression` or `rss_analysis`."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "## Description"
+ "Multi-trait colocalization across molecular contexts and against GWAS, using ColocBoost."
]
},
{
@@ -35,7 +17,26 @@
"kernel": "SoS"
},
"source": [
- "We use ColocBoost to perform colocalization analyses [cf. Cao et al (2025)](https://www.medrxiv.org/content/10.1101/2025.04.17.25326042v1). In short, ColocBoost performs colocalization while accounting for multiple causal variants within a genomic region of interest and can scale to hundreds of traits. We allow users to include or omit GWAS summary statistics. Required inputs are individual xQTL data from the same cohort (multiple phenotypes, same genotype). Linkage disequilibrium reference data is required if GWAS summary statistics are used."
+ "## Overview\n",
+ "\n",
+ "Two signals at the same locus - a QTL and a GWAS hit, or QTLs in two cell types - may share\n",
+ "a causal variant or merely sit in the same LD block. Colocalization tries to tell those\n",
+ "apart. Classical pairwise methods assume one causal variant per trait and compare traits\n",
+ "two at a time, which loses power when a region has several independent signals or when the\n",
+ "shared signal is weak in any single pair.\n",
+ "\n",
+ "ColocBoost treats it as a multi-task problem instead: a gradient boosting framework that\n",
+ "couples traits as it selects causal variants, so evidence that is weak in isolation can\n",
+ "still support a shared signal across many contexts. It scales to hundreds of traits and\n",
+ "allows multiple causal variants per region ([Cao et al., 2025](https://doi.org/10.1101/2025.04.17.25326042)).\n",
+ "\n",
+ "Requires fine-mapping outputs (`.susie.rds` files) from `mnm_regression` or `rss_analysis`.\n",
+ "\n",
+ "ColocBoost performs colocalization analyses ([Cao et al., 2025](https://doi.org/10.1101/2025.04.17.25326042)). In short, ColocBoost performs colocalization while accounting for multiple causal variants within a genomic region of interest and can scale to hundreds of traits. GWAS summary statistics may be included or omitted. Required inputs are individual xQTL data from the same cohort (multiple phenotypes, same genotype). Linkage disequilibrium reference data is required if GWAS summary statistics are used.\n",
+ "\n",
+ "**When to run it.** After QTL association testing, when you have several contexts to\n",
+ "compare, or a GWAS to integrate against. Colocalization is integration, not fine-mapping:\n",
+ "run `mnm_regression` if what you want is credible sets for one trait."
]
},
{
@@ -47,50 +48,38 @@
"source": [
"## Input\n",
"\n",
- "1. A list of regions to be analyzed (optional); the last column of this file should be region name.\n",
- "2. Either a list of per chromosome genotype files, or one file for genotype data of the entire genome. Genotype data has to be in PLINK `bed` format. \n",
- "3. Vector of lists of phenotype files per region to be analyzed, in UCSC `bed.gz` with index in `bed.gz.tbi` formats.\n",
- "4. Vector of covariate files corresponding to the lists above.\n",
- "5. Customized association windows file for variants (cis or trans). If it is not provided, a fixed sized window will be used around the region (a cis-window)\n",
- "6. Optionally a vector of names of the phenotypic conditions in the form of `cond1 cond2 cond3` separated with whitespace.\n",
- "7. Optionally summary statistics meta-data file and LD reference meta-data file.\n",
+ "Both entry points read per-region fine-mapping outputs (`.susie.rds`) produced by `mnm_regression` or `rss_analysis`. Individual-level genotype data must be in PLINK `bed` format; summary statistics come in through the GWAS and LD meta-data files instead.\n",
"\n",
- "Input 2 and 3 should be outputs from `genotype_per_region` and `annotate_coord` modules in previous preprocessing steps. 4 should be output of `covariate_preprocessing` pipeline that contains genotype PC, phenotypic hidden confounders and fixed covariates."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS",
- "tags": []
- },
- "source": [
- "### Example genotype, phenotype and association analysis windows \n",
+ "- `--name colocboost_xqtl` (tag used in output filenames and as the study name)\n",
+ "- `--cwd output/colocboost_xqtl` (working directory all outputs are written under. Defaults to `output`.)\n",
"\n",
- "See [this page](mnm_regression) for example inputs of these information."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "### Example summary statistics and LD reference\n",
+ "**Individual-level xQTL inputs.**\n",
"\n",
- "See [rss_analysis](rss_analysis) for LD reference formats (pre-computed blocks and PLINK genotype files),\n",
- "per-study LD reference via the `ld_meta_data` column in GWAS meta-data, and mixture LD panels.\n"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "### About indels\n",
+ "- `--genoFile` **`input/colocboost/example.chr22.bed`**\n",
+ "(genotype data in PLINK `bed` format; either one file for the whole genome or a list of per-chromosome files)\n",
+ "- `--phenoFile` **`input/colocboost/pheno_manifest_multicontext.tsv`**\n",
+ "(a manifest of phenotype files per region, one row per region and context, in UCSC `bed.gz` format with a `bed.gz.tbi` index)\n",
+ "- `--covFile` **`input/colocboost/example_covariates.tsv`**\n",
+ "(covariate files matching the phenotype list)\n",
+ "- `--transpose-covariates` (set when the covariate file has samples in columns rather than rows)\n",
+ "\n",
+ "**Region selection.**\n",
+ "\n",
+ "- `--customized-association-windows` **`input/colocboost/association_windows.bed`**\n",
+ "(association windows for variants, cis or trans. Without it a fixed-size window of `--cis-window` (default 1000000) is used around each region.)\n",
+ "- `--region-name ENSG00000130538` (restrict the analysis to named regions. If dropped, the union of all regions across all phenotype region lists is analyzed.)\n",
+ "- `--region-list` (a file of regions to analyze; the last column must be the region name)\n",
+ "\n",
+ "**Summary-statistics inputs (GWAS integration).**\n",
+ "\n",
+ "- `--gwas-meta-data` (GWAS summary statistics meta-data file)\n",
+ "- `--ld-meta-data` (LD reference meta-data file. See [rss_analysis](rss_analysis) for the expected formats.)\n",
"\n",
- "Use `--no-indel` to remove indel variants from analysis. This applies to both individual-level genotype loading and summary statistics QC. Default keeps indels.\n"
+ "**Analysis mode.**\n",
+ "\n",
+ "- `--xqtl-coloc` (colocalize across the xQTL contexts; on by default)\n",
+ "- `--separate-gwas` / `--no-separate-gwas` (treat each GWAS as its own outcome, or pool them. `--separate-gwas` is the default; the xQTL-only example passes `--no-separate-gwas`.)\n",
+ "- `--no-indel` (drop indel variants. Applies to both individual-level genotype loading and summary-statistics QC; indels are kept by default.)"
]
},
{
@@ -103,44 +92,75 @@
"source": [
"## Output\n",
"\n",
- "For each analysis region the pipeline fits ColocBoost and saves the model in RDS format \u2014 one RDS per gene of interest (and per GWAS, when GWAS integration is used). Each RDS contains:\n",
+ "- **`/qtl_dataset/.qtl_dataset.rds`** (`colocboost_1`)\n",
+ "(the assembled QTL dataset for the study, built once and reused by the colocalization steps. Not present in the example output.)\n",
+ "- **`/colocboost/.colocboost_manifest.tsv`** (`colocboost_2`)\n",
+ "(manifest listing the per-region result files produced for this run. Not present in the example output.)\n",
+ "- **`/colocboost/..colocboost.rds`** (`colocboost_3`)\n",
+ "(the fitted ColocBoost model, one RDS per analysis region -- per gene of interest, and per GWAS when GWAS integration is used)\n",
"\n",
- "- `ucos_summary` \u2014 a summary table of the colocalization events.\n",
- "- `vpa` \u2014 the variable colocalized probability for each variant (the probability of a variant being colocalized with at least one trait).\n",
- "- `data_info` \u2014 information on the input data.\n",
- "- `model_info` \u2014 information on the fitted ColocBoost model.\n",
- "- `ucos_details` \u2014 trait-specific (uncolocalized) effects information.\n",
- "- `region_info` \u2014 information on the region(s) of interest.\n",
- "- `computing_time` \u2014 time taken for each step (loading, QC and analysis).\n",
+ "```\n",
+ "List of 1\n",
+ "$ ENSG00000283047: List of 7 \n",
+ " ..$ ucos_summary: NULL\n",
+ " ..$ vpa: NULL\n",
+ " ..$ data_info: List of 6\n",
+ " .. ..$ n_outcomes: integer [1:1] 1\n",
+ " .. ..$ n_variables: integer [1:1] 184\n",
+ " .. ..$ outcome_info: List of 4 \n",
+ " .. .. ..$ outcome_names: ...\n",
+ " .. .. ..$ sample_size: ...\n",
+ " .. .. ..$ is_sumstats: ...\n",
+ " .. .. ..$ is_focal: ...\n",
+ " .. ..$ variables: character [1:184] chr22:10685239:C:T chr22:10761227:G:A chr22:11285239:C:T chr22:11601805:G:A chr22:11791698:G:T ...\n",
+ " .. ..$ coef: List of 1\n",
+ " .. .. ..$ trait_A_ENSG00000283047: ...\n",
+ " .. ..$ z: List of 1\n",
+ " .. .. ..$ trait_A_ENSG00000283047: ...\n",
+ " ..$ model_info: List of 9\n",
+ " .. ..$ model_coveraged: logical [1:1] TRUE\n",
+ " .. ..$ n_updates: integer [1:1] 352\n",
+ " .. ..$ profile_loglik: numeric [1:352] 1 0.973943746514459 0.964750650763505 0.955751398118438 0.946916816050365 ...\n",
+ " .. ..$ outcome_profile_loglik: List of 1\n",
+ " .. .. ..$ trait_A_ENSG00000283047: ...\n",
+ " .. ..$ outcome_proximity_obj: List of 1\n",
+ " .. .. ..$ trait_A_ENSG00000283047: ...\n",
+ " .. ..$ outcome_coupled_best_update_obj: List of 1\n",
+ " .. .. ..$ trait_A_ENSG00000283047: ...\n",
+ " .. ..$ outcome_model_coveraged: List of 1\n",
+ " .. .. ..$ trait_A_ENSG00000283047: ...\n",
+ " .. ..$ outcome_n_updates: integer [1:1] 352\n",
+ " .. ..$ jk_star: matrix [351, 1] 125 125 125 131 125 ...\n",
+ " ..$ ucos_details: NULL\n",
+ " ..$ region_info: List of 3\n",
+ " .. ..$ region_coord: List of 3 \n",
+ " .. .. ..$ chrom: ...\n",
+ " .. .. ..$ start: ...\n",
+ " .. .. ..$ end: ...\n",
+ " .. ..$ grange: List of 3 \n",
+ " .. .. ..$ chrom: ...\n",
+ " .. .. ..$ start: ...\n",
+ " .. .. ..$ end: ...\n",
+ " .. ..$ region_name: character [1:2] ENSG00000283047 ENSG00000283047\n",
+ " ..$ computing_time: List of 3\n",
+ " .. ..$ Loading: difftime [1:1] 10.3424470424652 \n",
+ " .. ..$ QC: difftime [1:1] 0.0479960441589355 \n",
+ " .. ..$ Analysis: List of 3\n",
+ " .. .. ..$ xqtl_coloc: ...\n",
+ " .. .. ..$ joint_gwas: ...\n",
+ " .. .. ..$ separate_gwas: ...\n",
+ "```\n",
"\n",
- "**Note:** the suggested output naming convention is `cohort_...`.\n"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "## Minimal Working Example Steps"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "Timing: ~X min"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "### xQTL only analysis"
+ "The example files on disk are named `..cb_xqtl.rds`, not `.colocboost.rds` as the step declares. The code is authoritative; the example data predates the current naming. In this example `ucos_summary` and `vpa` are `NULL` because a single outcome yields no colocalization events.\n",
+ "\n",
+ "Each RDS contains:\n",
+ "\n",
+ "- `ucos_summary` -- a summary table of the colocalization events.\n",
+ "- `vpa` -- the variable colocalized probability for each variant (the probability of a variant being colocalized with at least one trait).\n",
+ "- `data_info` -- information on the input data.\n",
+ "- `model_info` -- information on the fitted ColocBoost model.\n",
+ "- `ucos_details` -- trait-specific (uncolocalized) effects information.\n",
+ "- `region_info` -- information on the analysis region."
]
},
{
@@ -149,9 +169,13 @@
"kernel": "SoS"
},
"source": [
- "Below we duplicate the examples for phenotype and covariates to demonstrate that when there are multiple phenotypes for the same genotype it is possible to use this pipeline to analyze all of them (more than two is accepted as well).\n",
+ "## Minimal Working Example\n",
+ "\n",
+ "The phenotype and covariate examples are duplicated below to show that when there are multiple phenotypes for the same genotype it is possible to use this pipeline to analyze all of them (more than two is accepted as well).\n",
"\n",
- "Here using `--region-name` we focus the analysis on 3 genes. In practice if this parameter is dropped, the union of all regions in all phenotype region lists will be analyzed. It is possible for some of the regions there are no genotype data, in which case the pipeline will output RDS files with a warning message to indicate the lack of genotype data to analyze."
+ "Here `--region-name` focuses the analysis on 3 genes. In practice if this parameter is dropped, the union of all regions in all phenotype region lists will be analyzed. It is possible for some of the regions there are no genotype data, in which case the pipeline will output RDS files with a warning message to indicate the lack of genotype data to analyze.\n",
+ "\n",
+ "**Note:** Suggested output naming convention is cohort_modality, eg ROSMAP_snRNA_pseudobulk."
]
},
{
@@ -160,7 +184,9 @@
"kernel": "SoS"
},
"source": [
- "**Note:** Suggested output naming convention is cohort_modality, eg ROSMAP_snRNA_pseudobulk."
+ "### ColocBoost with xQTL only\n",
+ "\n",
+ "This is the minimal working example and **runs end-to-end on the toy gene-expression data**, producing a `*.cb_xqtl.rds` colocalization result for the region. The same command also accepts peaks or any other molecular phenotype - just swap in the phenotype `.bed.gz`/manifest."
]
},
{
@@ -169,10 +195,9 @@
"kernel": "SoS"
},
"source": [
- "### **Step 1.** ColocBoost with xQTL only\n",
- "\n",
- "This is the minimal working example and **runs end-to-end on the toy gene-expression data**, producing a `*.cb_xqtl.rds` colocalization result for the region. The same command also accepts peaks or any other molecular phenotype - just swap in the phenotype `.bed.gz`/manifest."
- ]
+ "**Timing**: TBD (on toy dataset)"
+ ],
+ "id": "1e51346c"
},
{
"cell_type": "code",
@@ -185,9 +210,6 @@
},
"outputs": [],
"source": [
- "# Toy gene-expression example (run from the toy_example root).\n",
- "# xQTL-only ColocBoost: builds one QtlDataset, then per-gene ColocBoost into\n",
- "# {name}.{gene}.colocboost.rds.\n",
"sos run pipeline/colocboost.ipynb colocboost \\\n",
" --name colocboost_xqtl --cwd output/colocboost_xqtl \\\n",
" --genoFile input/colocboost/example.chr22.bed \\\n",
@@ -204,11 +226,21 @@
"kernel": "SoS"
},
"source": [
- "### **Step 2.** ColocBoost with GWAS\n",
+ "### ColocBoost with GWAS\n",
"\n",
"Adds GWAS summary statistics (via `--gwas-meta-data`) and an LD reference (via `--ld-meta-data`). The xQTL ColocBoost completes, but the separate GWAS-xQTL colocalization is **still under development** with the installed package (see the note in the command cell)."
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ],
+ "id": "73abb479"
+ },
{
"cell_type": "code",
"execution_count": null,
@@ -217,9 +249,6 @@
},
"outputs": [],
"source": [
- "# Toy example with GWAS (separate-focal coloc per study), from toy_example root.\n",
- "# Per gene a region GwasSumStats is built, then xQTL + separate-GWAS coloc are\n",
- "# written to one combined {name}.{gene}.colocboost.rds.\n",
"sos run pipeline/colocboost.ipynb colocboost \\\n",
" --name colocboost_gwas --cwd output/colocboost_gwas \\\n",
" --genoFile input/colocboost/example.chr22.bed \\\n",
@@ -238,11 +267,20 @@
"kernel": "SoS"
},
"source": [
- "### **Step 3.** ColocBoost with multiple LD references (multi-ancestry)\n",
- "\n",
+ "### ColocBoost with multiple LD references (multi-ancestry)\n",
"Uses a per-study LD reference declared in the GWAS meta-data. It shares the separate-GWAS code path with Step 2 and is therefore **still under development** on the toy dataset."
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ],
+ "id": "85dd4373"
+ },
{
"cell_type": "code",
"execution_count": null,
@@ -251,10 +289,6 @@
},
"outputs": [],
"source": [
- "# Multi-ancestry template: each study uses the per-study LD reference named in\n",
- "# the gwas meta ld_meta_data column (gwas_sumstats_construct.R resolves a single\n",
- "# shared panel per region, so genuinely distinct per-study panels are not merged\n",
- "# here). Same separate-GWAS code path as the example above.\n",
"sos run pipeline/colocboost.ipynb colocboost \\\n",
" --name colocboost_multi_ld --cwd output/colocboost_multi_ld \\\n",
" --genoFile input/colocboost/example.chr22.bed \\\n",
@@ -264,7 +298,7 @@
" --gwas-meta-data input/colocboost/gwas_meta.txt \\\n",
" --ld-meta-data input/ld_reference/protocol_example.ld_meta_file.tsv \\\n",
" --region-name ENSG00000130538 \\\n",
- " --separate-gwas --xqtl-coloc -j1\n"
+ " --separate-gwas --xqtl-coloc -j1"
]
},
{
@@ -273,7 +307,7 @@
"kernel": "SoS"
},
"source": [
- "## Command interface"
+ "## Command Interface"
]
},
{
@@ -284,18 +318,17 @@
},
"outputs": [],
"source": [
- "sos run pipeline/colocboost.ipynb -h\n"
+ "sos run pipeline/colocboost.ipynb -h"
]
},
{
"cell_type": "markdown",
"metadata": {
- "kernel": "SoS",
- "tags": []
+ "kernel": "SoS"
},
"source": [
"```\n",
- "usage: sos run /restricted/projectnb/xqtl/xqtl_protocol/xqtl-protocol/code/mnm_analysis/mnm_methods/colocboost.ipynb\n",
+ "usage: sos run pipeline/colocboost.ipynb\n",
" [workflow_name | -t targets] [options] [workflow_options]\n",
" workflow_name: Single or combined workflows defined in this script\n",
" targets: One or more targets to generate\n",
@@ -303,102 +336,105 @@
" workflow_options: Double-hyphen workflow-specific parameters\n",
"\n",
"Workflows:\n",
- " get_analysis_regions\n",
- " get_rss_analysis_regions\n",
" colocboost\n",
"\n",
"Global Workflow Options:\n",
+ " --modular-script-dir code/script (as path)\n",
" --name VAL (as str, required)\n",
" It is required to input the name of the analysis\n",
" --cwd output (as path)\n",
" --genoFile VAL (as path, required)\n",
- " A list of file paths for genotype data, or the genotype data itself.\n",
+ " PLINK1 genotype: pass the .bed file (.bim/.fam are read\n",
+ " automatically).\n",
" --phenoFile paths\n",
"\n",
- " One or multiple lists of file paths for phenotype data.\n",
+ " QtlDataset phenotype manifest TSV. Columns: ID, #chr,\n",
+ " start, end, path, cond, and an optional cov_path (per-\n",
+ " context covariates). The first path is used. Replaces\n",
+ " the legacy list-of-phenotype-BEDs input.\n",
" --phenoIDFile paths()\n",
"\n",
- " One or multiple lists of file paths for phenotype ID mapping file. The first column should be the original ID, the 2nd column should be the ID to be mapped to.\n",
- " --covFile paths\n",
+ " Legacy phenotype ID-mapping file(s); accepted for CLI\n",
+ " compatibility but not consumed by the manifest-based\n",
+ " flow (the manifest carries the IDs).\n",
+ " --covFile paths()\n",
"\n",
- " Covariate file path\n",
+ " Uniform covariate file applied across all contexts\n",
+ " (QTLtools-format rows=covariates supported via\n",
+ " --transpose-covariates). Per-context covariates instead\n",
+ " come from the manifest cov_path column.\n",
" --gwas-meta-data . (as path)\n",
- " Summary statistics interface, see `rss_analysis.ipynb` for details\n",
+ " GWAS summary-statistics meta TSV. Columns: study_id,\n",
+ " chrom, file_path; optional n_sample, n_case, n_control,\n",
+ " ld_meta_data, column_mapping_file.\n",
" --ld-meta-data . (as path)\n",
- " --gwas-name (as list)\n",
- " --gwas-data (as list)\n",
- " --column-mapping (as list)\n",
+ " Default LD-reference meta (#chr, start, end, path). Per-\n",
+ " study overrides come from the gwas meta ld_meta_data\n",
+ " column.\n",
" --region-list . (as path)\n",
- " Optional: if a region list is provide the analysis will be focused on provided region. The LAST column of this list will contain the ID of regions to focus on when\n",
- " region_name is given Otherwise, all regions with both genotype and phenotype files will be analyzed\n",
+ " Optional region list whose LAST column lists region/gene\n",
+ " IDs to analyze.\n",
" --region-name (as list)\n",
- " Optional: if a region name is provided the analysis would be focused on the union of provides region list and region names\n",
- " --keep-samples . (as path)\n",
- " Only focus on a subset of samples\n",
- " --keep-variants . (as path)\n",
- " Only focus on a subset of variants\n",
+ " Region/gene IDs to analyze (space separated on the CLI).\n",
" --customized-association-windows . (as path)\n",
- " An optional list documenting the custom association window for each region to analyze, with four column, chr, start, end, region ID (eg gene ID). If this list is not\n",
- " provided, the default `window` parameter (see below) will be used.\n",
- " --cis-window -1 (as int)\n",
- " Specify the cis window for the up and downstream radius to analyze around the region of interest in units of bp When this is set to negative, we will rely on using\n",
- " customized_association_windows\n",
+ " Optional per-gene association windows (BED: #chr start\n",
+ " end ID) overriding the default cis_window for the GWAS\n",
+ " extraction window.\n",
+ " --cis-window 1000000 (as int)\n",
+ " cis window (bp) on each side of the gene for fine-\n",
+ " mapping and the default GWAS extraction window.\n",
" --[no-]save-data (default to False)\n",
- " save data object or not\n",
- " --phenotype-names [f'{x:bn}' for x in phenoFile]\n",
- "\n",
- " Name of phenotypes\n",
- " --[no-]trans-analysis (default to False)\n",
- " And indicator whether it is trans-analysis, ie, not using phenotypic coordinate information\n",
+ " Save the per-gene colocboost input bundle (currently\n",
+ " unused; reserved).\n",
+ " --study study\n",
+ " QtlDataset study label and covariate handling (legacy\n",
+ " CLI retained).\n",
+ " --[no-]transpose-covariates (default to False)\n",
+ " --genotype-covariates . (as path)\n",
" --imiss 1.0 (as float)\n",
- " remove a variant if it has more than imiss missing individual level data\n",
+ " Remove a variant if it has more than imiss fraction\n",
+ " missing genotypes.\n",
" --maf 0.0025 (as float)\n",
- " MAF and variance of X cutoff\n",
+ " MAF and variance-of-X cutoffs.\n",
" --xvar-cutoff 0.0 (as float)\n",
" --mac 5 (as int)\n",
- " MAC cutoff, on top of MAF cutoff\n",
+ " MAC cutoff, on top of the MAF cutoff.\n",
" --[no-]indel (default to True)\n",
- " Remove indels if indel = False\n",
- " --event-filter-rules . (as path)\n",
+ " Keep indels when True.\n",
+ " --keep-samples . (as path)\n",
+ " Only keep a subset of samples / variants\n",
+ " (whitespace/newline-separated IDs).\n",
+ " --keep-variants . (as path)\n",
" --skip-analysis-pip-cutoff (as list)\n",
- " If this value is not 0, then an initial single effect analysis will be performed to determine if follow up analysis will be continued or to simply return NULL If this is\n",
- " negative we use a default way to determine this cutoff which is conservative but still useful\n",
+ " Per-context single-effect skip cutoff passed to\n",
+ " colocboost.R (--pip-cutoff-to-skip): a scalar applied to\n",
+ " every context, or one or more context=value items\n",
+ " (negative -> data-driven 3/ncol(X); empty -> 0).\n",
" --skip-sumstats-analysis-pip-cutoff -1.0 (as float)\n",
+ " Summary-stats single-effect skip cutoff passed to\n",
+ " summaryStatsQc via gwas_sumstats_construct.R.\n",
" --[no-]xqtl-coloc (default to True)\n",
- " Perform xQTL colocalization\n",
+ " ColocBoost variants to run.\n",
" --[no-]joint-gwas (default to False)\n",
- " Perform joint colocalization with many traits\n",
" --[no-]separate-gwas (default to True)\n",
- " Perform separate GWAS targeted anlaysis one at a time\n",
- " --[no-]impute (default to False)\n",
- " Whether to impute the sumstat for all the snp in LD but not in sumstat.\n",
- " --rcond 0.01 (as float)\n",
- " --lamb 0.01 (as float)\n",
- " --R2-threshold 0.6 (as float)\n",
- " --minimum-ld 5 (as int)\n",
- " --qc-method NULL\n",
- " summary stats QC methods: slalom, dentist\n",
- " --extract-sumstats-region-name NULL\n",
- " --sumstats-region-name-col NULL\n",
- " Either an index number in str format or \"NULL\" as a string\n",
- " --comment-string NULL\n",
- " --ld-data-name (as list)\n",
+ " --qc-method none\n",
+ " Summary-stats LD-mismatch QC method (none | slalom |\n",
+ " dentist).\n",
" --container ''\n",
- " Analysis environment settings\n",
+ " Analysis environment settings.\n",
+ " --entrypoint ('micromamba run -a \"\" -n' + ' ' + re.sub(r'(_apptainer:latest|_docker:latest|\\.sif)$', '', container.split('/')[-1])) if container else \"\"\n",
+ "\n",
" --job-size 200 (as int)\n",
- " For cluster jobs, number commands to run per job\n",
+ " For cluster jobs, number of commands to run per job.\n",
" --walltime 1h\n",
- " Wall clock time expected\n",
" --mem 20G\n",
- " Memory expected\n",
" --numThreads 1 (as int)\n",
- " Number of threads\n",
"\n",
"Sections\n",
- " get_analysis_regions:\n",
- " get_rss_analysis_regions:\n",
- " colocboost:\n",
- " ```"
+ " colocboost_1:\n",
+ " colocboost_2:\n",
+ " colocboost_3:\n",
+ "```"
]
},
{
@@ -565,7 +601,7 @@
"kernel": "SoS"
},
"source": [
- "## ColocBoost analysis"
+ "### ColocBoost analysis"
]
},
{
@@ -621,37 +657,6 @@
" --pip-cutoff-to-skip \"${\",\".join([str(x) for x in skip_analysis_pip_cutoff])}\" \\\n",
" --output ${_output}\n"
]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "## Troubleshooting"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "| Step | Substep | Problem | Possible Reason | Solution |\n",
- "|------|---------|---------|------------------|---------|\n",
- "| | | | | |\n",
- "\n",
- "\n"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "## Anticipated Results\n\nOutput file: `{cwd}/colocboost/{name}.{region_id}.colocboost.rds`\n\nEach RDS is a named list with one slot per analysis mode. The structure is the same for all steps \u2014 only which slots are populated differs:\n\n| Top-level key | Step 1 (xQTL-only) | Step 2 (xQTL + GWAS) |\n|--------------|---------------------|----------------------|\n| `xqtl_coloc` | `colocboost`-class object | `colocboost`-class object |\n| `joint_gwas` | `NULL` (not run) | `NULL` (off by default) |\n| `separate_gwas` | `NULL` (not run) | empty `list()` (run, no events) |\n| `computing_time` | per-step wall time | per-step wall time |\n\n```r\nres <- readRDS(\"colocboost_xqtl_positive.GENE.colocboost.rds\")\nnames(res) # [1] \"xqtl_coloc\" \"joint_gwas\" \"separate_gwas\" \"computing_time\"\n```\n\nEach active mode slot is a `colocboost`-class object:\n\n```r\ncb <- res$xqtl_coloc\nclass(cb) # \"colocboost\"\nnames(cb) # \"cos_summary\" \"vcp\" \"cos_details\" \"data_info\" \"model_info\" \"ucos_details\"\n\n# data_info: input summary\ncb$data_info$outcome_info\n# outcome_names sample_size is_sumstats is_focal\n# y1 Y1 200 FALSE FALSE\n# y2 Y2 200 FALSE FALSE\n\n# model_info: convergence\ncb$model_info$model_coveraged # TRUE\ncb$model_info$n_updates # 450\n```\n\n**`cos_summary`** \u2014 one row per colocalization event detected:\n\n```r\ncb$cos_summary\n# focal_outcome colocalized_outcomes cos_id purity top_variable top_variable_vcp\n# 1 FALSE Y1; Y2 cos1:y1_y2 1 chr1:10250:A:T 1\n# cos_npc min_npc_outcome n_variables colocalized_index colocalized_variables\n# 1 1 1 1 250 chr1:10250:A:T\n# colocalized_variables_vcp\n# 1 0.99999999997293\n```\n\n| Column | Description |\n|--------|-------------|\n| `colocalized_outcomes` | Traits sharing this colocalization event (`Y1; Y2` = both contexts) |\n| `cos_id` | Event ID (e.g. `cos1:y1_y2` = 1st event, shared by y1 and y2) |\n| `purity` | Purity of the credible set (1 = a single dominant variant) |\n| `top_variable` | Variant with highest VCP in this event |\n| `top_variable_vcp` | VCP of the top variant (1.0 = near-certain) |\n| `cos_npc` | CoS non-null probability (\u2265 0.2 threshold) |\n| `colocalized_variables` | All variants in the credible set for this event |\n| `focal_outcome` | `TRUE` if a GWAS focal trait is involved |\n\n**`vcp`** \u2014 named numeric vector, one entry per variant:\n\n```r\n# vcp = per-variant colocalization probability (named numeric vector)\nlength(cb$vcp) # 500 (one per variant in the region)\nsort(cb$vcp, decreasing = TRUE)[1:3]\n# chr1:10250:A:T chr1:10103:A:T chr1:10148:A:T\n# 1.000000e+00 1.260847e-11 6.503242e-12\n```\n\nThe causal variant (`chr1:10250:A:T`) has VCP = 1.0; all others are essentially 0. In real data, multiple variants may share non-trivial VCP within a credible set.\n\n> **Note**: The toy pipeline data (ENSG00000130538, n=49) produces `cos_summary = NULL` and `vcp = NULL` due to insufficient sample size. The example above uses a simulated dataset (n=200, 2 contexts, strong shared causal signal) to illustrate the output format."
- ]
}
],
"metadata": {
diff --git a/code/SoS/mnm_analysis/mnm_methods/mnm_regression.ipynb b/code/SoS/mnm_analysis/mnm_methods/mnm_regression.ipynb
index ad326982f..183b96bb0 100644
--- a/code/SoS/mnm_analysis/mnm_methods/mnm_regression.ipynb
+++ b/code/SoS/mnm_analysis/mnm_methods/mnm_regression.ipynb
@@ -8,7 +8,7 @@
"source": [
"# Advanced regression models for association analysis with individual-level data\n",
"\n",
- "This is a new-user-friendly minimal working example (MWE) of the `mnm_regression` pipeline. It runs end-to-end on the bundled chr22 toy dataset (60 samples) and shows how to compute SuSiE fine-mapping results and multi-method TWAS weights for a list of molecular-phenotype regions, then build the ensemble (multi-context) TWAS weights."
+ "Fits univariate and multi-context fine-mapping models to individual-level genotype and molecular phenotype data, and derives TWAS weights."
]
},
{
@@ -17,7 +17,37 @@
"kernel": "SoS"
},
"source": [
- "## Prerequisites\n",
+ "## Overview\n",
+ "\n",
+ "An association scan tells you a region matters; it does not tell you which variant in it is\n",
+ "responsible, because variants in linkage disequilibrium carry nearly the same signal.\n",
+ "\n",
+ "SuSiE\n",
+ "reframes the question as variable selection: it fits a sum of single effects and returns\n",
+ "credible sets - small groups of variants, each likely to contain one causal variant -\n",
+ "together with posterior inclusion probabilities ([Wang et al., 2020](https://doi.org/10.1111/rssb.12388)).\n",
+ "\n",
+ "When the same locus is measured across several contexts - tissues, cell types, conditions -\n",
+ "fine-mapping each separately throws away the shared structure. mvSuSiE learns the patterns\n",
+ "of sharing from the data and uses them to sharpen credible sets ([Zou et al., 2026](https://doi.org/10.1038/s41588-025-02486-7)).\n",
+ "\n",
+ "The same fit also yields **TWAS weights**, the per-variant effects used later to predict\n",
+ "expression into a GWAS cohort - which is why fine-mapping and weights come from one command\n",
+ "rather than two, with `skip_twas_weights` defaulting to `False`. In practice the pipeline\n",
+ "runs per region: `susie_twas` performs univariate fine-mapping and trains weights with\n",
+ "several methods, writing one `*.univariate_twas_weights.rds` per region; run it with\n",
+ "`--save-data` so the combined fine-mapping object is kept. `mnm` then consumes that output\n",
+ "through a `--fine-mapping-meta` table and produces the multi-context ensemble weights, one\n",
+ "`*.multicontext_bvsr.rds` per gene.\n",
+ "\n",
+ "Two input modes take identical commands: a bulk RNA-seq matrix keyed by Ensembl gene ID\n",
+ "with association windows at TSS +/- 500 kb, or a list of peaks or any other molecular\n",
+ "phenotype - swap the phenotype BED and its manifest. `mnm_genes` (multi-gene),\n",
+ "`fsusie`/`mvfsusie` (functional) and `mvsusie` (multivariate) are supported as well ([Wang et al., 2020](https://doi.org/10.1111/rssb.12388); [Zou et al., 2026](https://doi.org/10.1038/s41588-025-02486-7)).\n",
+ "\n",
+ "\n",
+ "**When to run it.** After QTL association testing, with individual-level genotypes,\n",
+ "phenotypes and covariates in hand. From summary statistics only, use `rss_analysis`.\n",
"\n",
"Requires processed genotype (PLINK/VCF) and phenotype files from `data_preprocessing`. For TWAS, also requires the LD reference generated by `rss_ld_sketch`."
]
@@ -28,21 +58,94 @@
"kernel": "SoS"
},
"source": [
- "## Description\n",
+ "## Input\n",
+ "\n",
+ "**Gene-expression mode (`input/colocboost/`) - the example below:**\n",
+ "\n",
+ "- `--genoFile` **`input/colocboost/example.chr22.bed`**\n",
+ "(PLINK1 binary genotype set. Pass the `.bed`; the `.bim` and `.fam` are read automatically. chr22, 1,995 variants and 49 samples.)\n",
+ "\n",
+ "- `--phenoFile` **`input/colocboost/pheno_manifest_multicontext.tsv`**\n",
+ "(phenotype list, one row per region and context. Columns `#chr start end ID path cond cov_path`: `path` is the bgzipped and tabixed phenotype matrix, `cond` names the context, `cov_path` the per-context covariates. 32 rows in the toy file.)\n",
+ "\n",
+ "```\n",
+ "#chr\tstart\tend\tID\tpath\tcond\tcov_path\n",
+ "chr22\t10939387\t10961338\tENSG00000283047\tinput_geneexpr/example_geneexpr.bed.gz\tcontext1\tinput_geneexpr/example_covariates.tsv\n",
+ "chr22\t10939387\t10961338\tENSG00000283047\tinput_geneexpr/example_geneexpr_ctx2.bed.gz\tcontext2\tinput_geneexpr/example_covariates.tsv\n",
+ "```\n",
+ "\n",
+ "- `--covFile` **`input/colocboost/example_covariates.tsv`**\n",
+ "(covariates, one row per covariate and one column per sample; passed with `--transpose-covariates`.)\n",
+ "\n",
+ "```\n",
+ "ID\tSAMPLE_001\tSAMPLE_002\tSAMPLE_003\t...\n",
+ "sex\t1\t1\t1\t...\n",
+ "age\t67.06\t82.92\t87.76\t...\n",
+ "```\n",
"\n",
- "This pipeline fits advanced statistical models for QTL association analysis using individual-level genotype and molecular-phenotype data. For each region in a phenotype list it runs univariate SuSiE fine-mapping and trains TWAS prediction weights with several methods, then combines them into an **ensemble** weight.\n",
+ "- `--customized-association-windows` **`input/colocboost/association_windows.bed`**\n",
+ "(analysis window per region, columns `#chr start end ID`: the gene TSS +/- 500 kb, clamped at the chromosome edges. 16 regions.)\n",
"\n",
- "**The primary example below uses gene expression** as the molecular phenotype, running end-to-end on the same chr22 toy genotype:\n",
- "- **Gene-expression input** (`input/colocboost/`, Mode A): the phenotype is a bulk RNA-seq matrix with Ensembl `gene_id`s; association windows are gene TSS +/- 500 kb. This is the main, fully verified example.\n",
- "- **Peak / chromatin input** (`input/`, Mode B): the *same* pipeline also accepts a list of chr22 peaks (or any other molecular phenotype) - the command is identical, you just swap in the phenotype BED and its manifest.\n",
+ "```\n",
+ "#chr\tstart\tend\tID\n",
+ "chr22\t10439387\t11461338\tENSG00000283047\n",
+ "chr22\t15028191\t16029139\tENSG00000130538\n",
+ "```\n",
"\n",
- "**Workflows used here:**\n",
- "- `susie_twas` -- univariate SuSiE fine-mapping plus multi-method TWAS weights (one `*.univariate_twas_weights.rds` per region). Run with `--save-data` so the combined fine-mapping object is written for the next step.\n",
- "- `mnm` -- multi-context fine-mapping (mvSuSiE + mr.mash) that consumes the `susie_twas --save-data` output through a `--fine-mapping-meta` table and produces the ensemble weights (one `*.multicontext_twas_weights.rds` per gene).\n",
+ "**Peak mode (`input/`) - same interface, alternative input:**\n",
"\n",
- "The notebook also documents the `mnm_genes` (multi-gene), `mvfsusie`/`fsusie` (functional), and `mvsusie` (multivariate) workflows that the pipeline supports, but the MWE focuses on the univariate (`susie_twas`) + multi-context (`mnm`) path that is runnable on the toy data.\n",
+ "- `--genoFile` **`input/genotype/protocol_example.genotype.chr22.bed`**\n",
+ "(same PLINK1 layout, a different cohort: chr22, 1,995 variants and 1,287 samples with `S####` identifiers.)\n",
"\n",
- "**Note on data:** this example uses a small synthetic chr22 toy dataset (60 samples, 1,995 variants after subsetting). No access-controlled individual-level human data is used."
+ "- `--phenoFile` **`input/phenotype/protocol_example.pheno_manifest.tsv`**\n",
+ "(peak list, 5 columns `#chr start end ID path`; one row per peak, `path` pointing at the per-peak bgzipped matrix.)\n",
+ "\n",
+ "```\n",
+ "#chr\tstart\tend\tID\tpath\n",
+ "chr22\t10662152\t10664678\tC22P107555\tinput/proteomics/protocol_example.peaks_split/C22P107555.bed.gz\n",
+ "chr22\t16094161\t16096165\tC22P107557\tinput/proteomics/protocol_example.peaks_split/C22P107557.bed.gz\n",
+ "```\n",
+ "\n",
+ "- `--covFile` **`input/covariate/protocol_example.covariates.tsv`**\n",
+ "(same shape as the gene-expression covariates, keyed by `#id`.)\n",
+ "\n",
+ "```\n",
+ "#id\tSAMPLE_001\tSAMPLE_002\tSAMPLE_003\t...\n",
+ "sex\t1\t1\t1\t...\n",
+ "age\t90.97\t80.24\t83.9\t...\n",
+ "```\n",
+ "\n",
+ "- `--customized-association-windows` **`input/finemapping/protocol_example.association_windows.bed`**\n",
+ "(cis window per peak, same four columns and no header row.)\n",
+ "\n",
+ "```\n",
+ "chr22\t0\t19959512\tC22P107555\n",
+ "chr22\t0\t19959512\tC22P107557\n",
+ "```\n",
+ "\n",
+ "**Key requirement:** the 4th-column `ID` of the association-windows file must match the 4th-column `ID` of the phenotype list, otherwise the window for that region is not found and the region is skipped.\n",
+ "\n",
+ "The key is that the 4th column ID should match with the 4th column ID in the phenotype list. Otherwise the association-window to analyze will not be found.\n",
+ "\n",
+ "### About indels\n",
+ "\n",
+ "Option `--no-indel` will remove indel from analysis.\n",
+ "\n",
+ "These apply across the workflows:\n",
+ "\n",
+ "- `--cwd output/mnm` (working directory all outputs are written under. Defaults to `output`.)\n",
+ "- `--name protocol_example` (tag used in every output filename)\n",
+ "- `--customized-association-windows` **`input/colocboost/association_windows.bed`**\n",
+ "(association windows per region. Without it a fixed window of `--cis-window` is used.)\n",
+ "- `--cis-window 1000000` (half-width of the fixed association window used when no window file is given)\n",
+ "- `--region-name ENSG00000283047` (restrict the run to named regions; the union of all regions is used if omitted)\n",
+ "- `--save-data` (keep the assembled per-region dataset alongside the fine-mapping result)\n",
+ "- `--transpose-covariates` (set when the covariate file has samples in columns rather than rows)\n",
+ "- `--keep-samples` (a file of sample IDs to retain before fine-mapping)\n",
+ "- `--max-cv-variants 5000` (cap on the variants carried into cross-validated TWAS weights; `-1` for no cap)\n",
+ "- `--pheno-id-map-file` (maps phenotype IDs to the IDs used in the region list, when the two differ)\n",
+ "- `--fine-mapping-meta` **`output/mnm/fine_mapping_meta_mnm.tsv`**\n",
+ "(the manifest of per-region fine-mapping results, consumed by the multi-gene and fSuSiE workflows)"
]
},
{
@@ -51,40 +154,109 @@
"kernel": "SoS"
},
"source": [
- "## Input Files\n",
+ "## Output\n",
+ "**qtl_dataset_construct** \n",
+ "\n",
+ "- **`output/mnm/qtl_dataset/protocol_example.qtl_dataset.rds`** (the assembled `QtlDataset`: a genotype handle plus per-region phenotype and covariate data, reused by every method downstream.)\n",
"\n",
- "All paths below are relative to the `toy_example/` root (where you launch the commands).\n",
+ "```\n",
+ "S4 object \n",
+ "$ keepIndel: logical [1:1] TRUE\n",
+ "$ study: character [1:1] protocol_example\n",
+ "$ genotypes: S4 object \n",
+ " ..$ chromPaths: character [1:0] \n",
+ " ..$ path: character [1:1] input/colocboost/example.chr22\n",
+ " ..$ format: character [1:1] plink1\n",
+ " ..$ snpInfo: List of 5 \n",
+ " .. ..$ SNP: character [1:1995] chr22:10414272:A:T chr22:10416111:A:T chr22:10416669:A:T chr22:10420511:A:T chr22:10424460:A:T ...\n",
+ " .. ..$ CHR: character [1:1995] 22 22 22 22 22 ...\n",
+ " .. ..$ BP: integer [1:1995] 10414272 10416111 10416669 10420511 10424460 ...\n",
+ " .. ..$ A1: character [1:1995] A A A A A ...\n",
+ "... (truncated)\n",
+ "```\n",
"\n",
- "**Shared genotype (both modes):**\n",
- "- **Genotype** (`--genoFile`): PLINK1 binary set. Pass the `.bed` file; the `.bim`/`.fam` are read automatically. Gene-expression mode (`input/colocboost/`) uses `example.chr22.{bed,bim,fam}`; peak mode (`input/`) uses `protocol_example.genotype.chr22.{bed,bim,fam}` (chr22, 1,995 variants).\n",
- "- **Covariates** (`--covFile`): a TSV with covariate rows and one column per sample (gene-expression mode: `input/colocboost/example_covariates.tsv`; peak mode: `input/covariate/protocol_example.covariates.tsv`).\n",
+ "**susie_twas**\n",
+ "- **`output/mnm/fine_mapping/protocol_example.ENSG00000130538.univariate_bvsr.rds`** (from `susie_twas`, the SuSiE fine-mapping result)\n",
"\n",
- "**Gene-expression mode (`input/colocboost/`) - primary example:**\n",
- "- **Phenotype list** (`--phenoFile`): `input/colocboost/pheno_manifest_1gene.tsv` (a single gene, fastest) or `input/colocboost/pheno_manifest.tsv` (16 in-range chr22 genes). The `path` column points to the bgzipped + tabixed RNA-seq matrix `input/colocboost/example/colocboost.bed.gz` whose 4th column is the Ensembl `gene_id`.\n",
- "- **Association windows** (`--customized-association-windows`): `input/colocboost/association_windows.bed` (or `_1gene.bed`) -- gene TSS +/- 500 kb, clamped to the genotype variant range.\n",
+ "```\n",
+ "QtlFineMappingResult: S4 DataFrame, 2 rows x 5 columns\n",
+ "$ study: character [1:2] protocol_example protocol_example\n",
+ "$ context: character [1:2] context1 context2\n",
+ "$ trait: character [1:2] ENSG00000130538 ENSG00000130538\n",
+ "$ method: character [1:2] susie susie\n",
+ "$ entry: S4 object \n",
+ " ..$ listData: List of 2\n",
+ " .. ..$ : S4 object \n",
+ " .. .. ..$ variantIds: ...\n",
+ " .. .. ..$ susieFit: ...\n",
+ " .. .. ..$ topLoci: ...\n",
+ " .. .. ..$ cvResult: ...\n",
+ "... (truncated)\n",
+ "```\n",
"\n",
- "**Peak mode (`input/`) - same format, alternative input:**\n",
- "- **Phenotype list** (`--phenoFile`): `input/phenotype/protocol_example.pheno_manifest.tsv` -- 5 columns `#chr start end ID path`, one row per peak (10 peaks). Each `path` points to a bgzipped per-peak phenotype BED under `input/proteomics/protocol_example.peaks_split/` (samples use the anonymized genotype IDs, e.g. `S0561`, matching the shared PLINK `.fam`).\n",
- "- **Association windows** (`--customized-association-windows`): `input/finemapping/protocol_example.association_windows.bed` -- the cis window to analyze for each peak `ID`.\n",
+ "- **`output/mnm/twas_weights/protocol_example.ENSG00000130538.univariate_twas_weights.rds`** (from `susie_twas`, per-variant TWAS weights from each method, with cross-validation performance)\n",
"\n",
- "**Key requirement:** the 4th-column `ID` of the association-windows file must match the 4th-column `ID` of the phenotype list, otherwise the window for that region is not found and the region is skipped."
+ "```\n",
+ "TwasWeights: S4 DataFrame, 22 rows x 5 columns\n",
+ "$ study: character [1:22] protocol_example protocol_example protocol_example protocol_example protocol_example ...\n",
+ "$ context: character [1:22] context1 context1 context1 context1 context1 ...\n",
+ "$ trait: character [1:22] ENSG00000130538 ENSG00000130538 ENSG00000130538 ENSG00000130538 ENSG00000130538 ...\n",
+ "$ method: character [1:22] mrash susie susie_inf enet lasso ...\n",
+ "$ entry: S4 object \n",
+ " ..$ listData: List of 22\n",
+ " .. ..$ : S4 object \n",
+ " .. .. ..$ variantIds: ...\n",
+ " .. .. ..$ weights: ...\n",
+ " .. .. ..$ fits: ...\n",
+ " .. .. ..$ cvPerformance: ...\n",
+ "... (truncated)\n",
+ "```\n",
+ "\n",
+ "**mnm** \n",
+ "- **`multivariate_fine_mapping/{name}.{region}.multicontext_bvsr.rds`** (the multi-context fit, one per region.)\n",
+ "\n",
+ "```\n",
+ "QtlFineMappingResult: S4 DataFrame, 2 rows x 5 columns\n",
+ "$ study: character [1:2] protocol_example protocol_example\n",
+ "$ context: character [1:2] context1 context2\n",
+ "$ trait: character [1:2] ENSG00000130538 ENSG00000130538\n",
+ "$ method: character [1:2] mvsusie mvsusie\n",
+ "$ entry: S4 object \n",
+ " ..$ listData: List of 2\n",
+ " .. ..$ : S4 object \n",
+ " .. .. ..$ variantIds: ...\n",
+ " .. .. ..$ susieFit: ...\n",
+ " .. .. ..$ topLoci: ...\n",
+ " .. .. ..$ cvResult: ...\n",
+ " .. ..$ : S4 object \n",
+ "... (truncated)\n",
+ "```\n",
+ "\n",
+ "**mnm_genes**\n",
+ "- **`multivariate_fine_mapping/{name}.mnm_genes_region_manifest.tsv`**\n",
+ "- **`multivariate_fine_mapping/{name}.{region_id}.multigene_bvsr.rds`**\n",
+ "\n",
+ "**fsusie**\n",
+ "- **`fsusie/{name}.fsusie_region_manifest.tsv`**\n",
+ "- **`fsusie/{name}.{region_id}.fsusie.rds`**\n",
+ "\n",
+ "**mvfsusie**\n",
+ "- **`{name}.{region}.mvfsusie_{prior}.rds`**"
]
},
{
- "cell_type": "markdown",
+ "cell_type": "code",
+ "execution_count": null,
"metadata": {
"kernel": "SoS"
},
+ "outputs": [],
"source": [
- "## Steps\n",
- "\n",
- "Run the two steps in order. **Step 1** (`susie_twas`) produces the per-region univariate TWAS weights; **Step 2** (`mnm`) reads those weights and adds the multi-context ensemble. The pipeline notebook is referenced as `../../pipeline/mnm_regression.ipynb`.\n",
- "\n",
- "**Step 1.** SuSiE fine-mapping + multi-method TWAS weights for every region in the phenotype list. Run with `--save-data` so the combined fine-mapping object is written for Step 2.\n",
- "\n",
- "**Step 2.** Build the ensemble (multi-context) TWAS weight from the Step-1 output with `mnm`. The ensemble is computed by default on the toy data (the `ensemble_r2_threshold` is relaxed so no method is silently dropped).\n",
- "\n",
- "_Memory note:_ run multi-region loops with `-j1`. Running many regions in parallel (`-j4`) -- or even all of them serially in one process -- can exhaust memory on heavier regions and trigger `ERROR: One of the local workers has been killed`. If that happens, re-run the remaining heavy regions one at a time using a single-region phenotype list."
+ "library(pecotmr)\n",
+ "w <- readRDS('output/mnm/multivariate_fine_mapping/protocol_example.ENSG00000130538.multicontext_bvsr.rds')\n",
+ "w$context # contexts fitted, one row each\n",
+ "w$method # e.g. \"mvsusie\"\n",
+ "str(w$entry[[1]], max.level = 1) # variantIds / susieFit / topLoci / cvResult"
]
},
{
@@ -93,11 +265,10 @@
"kernel": "SoS"
},
"source": [
- "### Mode A: gene-expression input (primary example)\n",
- "\n",
- "This is the main example: cis xQTL fine-mapping on gene expression. Two steps, pointed at the `input/colocboost/` files. Use the 1-gene phenotype list for the fastest run; swap in `pheno_manifest.tsv` / `association_windows.bed` to analyze all 16 in-range genes.\n",
+ "## Minimal Working Example\n",
+ "### Univariate SuSiE fine-mapping and TWAS weights (`susie_twas`) - gene expression\n",
"\n",
- "**Step 1 (gene expression): SuSiE + TWAS weights.**"
+ "This is the main example: cis xQTL fine-mapping on gene expression. Two steps, pointed at the `input/colocboost/` files. Use the 1-gene phenotype list for the fastest run; swap in `pheno_manifest.tsv` / `association_windows.bed` to analyze all 16 in-range genes."
]
},
{
@@ -106,9 +277,7 @@
"kernel": "SoS"
},
"source": [
- "**Timing** (toy chr22 dataset, 60 samples):\n",
- "- `susie_twas`: ~3 min (chr22 toy, 1 gene)\n",
- "- `mnm`: ~30 sec"
+ "**Timing**: ~3 min (on toy dataset)"
]
},
{
@@ -134,9 +303,20 @@
"kernel": "SoS"
},
"source": [
- "**Step 2 (gene expression): multi-context fine-mapping + ensemble TWAS weights (mnm).**"
+ "#### Step 2. Multi-context fine-mapping and ensemble TWAS weights\n",
+ "This reads those weights and adds the multi-context ensemble. "
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ],
+ "id": "08156b31"
+ },
{
"cell_type": "code",
"execution_count": null,
@@ -161,29 +341,18 @@
"kernel": "SoS"
},
"source": [
- "### Mode B: peak / other molecular phenotypes (same pipeline, different input)\n",
- "\n",
- "The same pipeline also accepts peaks (chromatin/epigenomic) or any other molecular phenotype - just swap in the phenotype `.bed.gz` and its manifest; the command is otherwise identical to Mode A. Example below uses the 10 toy chr22 peaks.\n",
- "\n",
- "**Step 1 (peak): SuSiE + TWAS weights for the 10 peaks.**"
+ "### Univariate SuSiE fine-mapping and TWAS weights (`susie_twas`) - peak"
]
},
{
- "cell_type": "code",
- "execution_count": null,
+ "cell_type": "markdown",
"metadata": {
"kernel": "SoS"
},
- "outputs": [],
"source": [
- "sos run pipeline/mnm_regression.ipynb susie_twas \\\n",
- " --name protocol_example \\\n",
- " --cwd output_10peaks \\\n",
- " --genoFile input/genotype/protocol_example.genotype.chr22.bed \\\n",
- " --phenoFile input/phenotype/protocol_example.pheno_manifest.tsv \\\n",
- " --covFile input/covariate/protocol_example.covariates.tsv \\\n",
- " --customized-association-windows input/finemapping/protocol_example.association_windows.bed \\\n",
- " -j1"
+ "The same pipeline also accepts peaks (chromatin/epigenomic) or any other molecular phenotype - just swap in the phenotype `.bed.gz` and its manifest; the command is otherwise identical to Mode A. Example below uses the 10 toy chr22 peaks.\n",
+ "\n",
+ "#### Step 1. SuSiE + TWAS weights"
]
},
{
@@ -192,8 +361,9 @@
"kernel": "SoS"
},
"source": [
- "**Step 2 (peak): multi-context fine-mapping + ensemble TWAS weights (mnm).**"
- ]
+ "**Timing**: TBD (on toy dataset)"
+ ],
+ "id": "44f99ba1"
},
{
"cell_type": "code",
@@ -203,16 +373,13 @@
},
"outputs": [],
"source": [
- "# Step 2 (10 peaks): multi-context fine-mapping + ensemble TWAS weights (mnm).\n",
- "# As above, mnm consumes the Step-1 univariate outputs via --fine-mapping-meta.\n",
- "sos run pipeline/mnm_regression.ipynb mnm \\\n",
+ "sos run pipeline/mnm_regression.ipynb susie_twas \\\n",
" --name protocol_example \\\n",
" --cwd output_10peaks \\\n",
" --genoFile input/genotype/protocol_example.genotype.chr22.bed \\\n",
" --phenoFile input/phenotype/protocol_example.pheno_manifest.tsv \\\n",
" --covFile input/covariate/protocol_example.covariates.tsv \\\n",
" --customized-association-windows input/finemapping/protocol_example.association_windows.bed \\\n",
- " --fine-mapping-meta output_10peaks/fine_mapping_meta.tsv \\\n",
" -j1"
]
},
@@ -222,40 +389,7 @@
"kernel": "SoS"
},
"source": [
- "## Output Files\n",
- "\n",
- "Under `--cwd` (e.g. `output_10peaks/` or `output_uni/`):\n",
- "- `twas_weights/{name}..univariate_twas_weights.rds` -- Step 1 (`susie_twas`) output, one per region: the SuSiE fine-mapping result plus TWAS weights from each method (e.g. lasso, enet, mrash, susie), variant names, region info and CV performance.\n",
- "- `multivariate_twas_weights/{name}..multicontext_twas_weights.rds` -- Step 2 (`mnm`) output, one per gene: the multi-context model with the added ensemble. The ensemble is stored under an `ensemble` entry alongside `method_coef`, the ensemble weights, and `method_performance`.\n",
- "\n",
- "Fine-mapping objects are also written under the `fine_mapping/` subfolder, and per-region `.stdout`/`.stderr` logs accompany each run."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "## Anticipated Results\n",
- "\n",
- "For the 10-peak run you should get 10 `*.univariate_twas_weights.rds` (one per peak) and 10 `*.multicontext_twas_weights.rds` from the `mnm` step. For the 1-gene gene-expression run you get one file of each type.\n",
- "\n",
- "You can sanity-check a multi-context ensemble weight in R:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "kernel": "SoS"
- },
- "outputs": [],
- "source": [
- "# In R: inspect one multi-context (mnm) ensemble output\n",
- "w <- readRDS('output_uni/multivariate_twas_weights/protocol_example.ENSG00000130538.multicontext_twas_weights.rds')\n",
- "names(w) # available genes / contexts\n",
- "str(w, max.level = 2) # weights + per-method performance"
+ "#### Step 2. Multi-context fine-mapping and ensemble TWAS weights"
]
},
{
@@ -264,10 +398,9 @@
"kernel": "SoS"
},
"source": [
- "## Command interface\n",
- "\n",
- "Show all workflows and options of the pipeline:"
- ]
+ "**Timing**: TBD (on toy dataset)"
+ ],
+ "id": "6abc252f"
},
{
"cell_type": "code",
@@ -277,7 +410,15 @@
},
"outputs": [],
"source": [
- "sos run pipeline/mnm_regression.ipynb -h"
+ "sos run pipeline/mnm_regression.ipynb mnm \\\n",
+ " --name protocol_example \\\n",
+ " --cwd output_10peaks \\\n",
+ " --genoFile input/genotype/protocol_example.genotype.chr22.bed \\\n",
+ " --phenoFile input/phenotype/protocol_example.pheno_manifest.tsv \\\n",
+ " --covFile input/covariate/protocol_example.covariates.tsv \\\n",
+ " --customized-association-windows input/finemapping/protocol_example.association_windows.bed \\\n",
+ " --fine-mapping-meta output_10peaks/fine_mapping_meta.tsv \\\n",
+ " -j1"
]
},
{
@@ -286,14 +427,9 @@
"kernel": "SoS"
},
"source": [
- "## Other workflows\n",
+ "### Multivariate analysis with mvSuSiE and mr.mash (`mnm`)\n",
"\n",
- "The same pipeline notebook also provides:\n",
- "- `mvsusie` -- multivariate SuSiE with mr.mash TWAS weights (for jointly analyzing multiple phenotypes that share a genotype).\n",
- "- `mnm_genes` -- multi-gene multivariate fine-mapping across genes sharing a window.\n",
- "- `fsusie` -- functional SuSiE for functional/high-resolution modalities (e.g. methylation), with a `--fsusie-post-processing` option (`TI`, `HMM`, or `none`; `TI` is recommended).\n",
- "\n",
- "These follow the same `--genoFile` / `--phenoFile` / `--covFile` interface; they are documented here for completeness but the verified MWE above uses `susie_twas` + `mnm`."
+ "Fine-maps multiple molecular phenotypes jointly across a shared window using mvSuSiE with mr.mash priors. This step needs **multiple contexts** (e.g. the same genes measured under two or more conditions). The toy set ships a single context, so this MWE adds a **second, synthetic context** (`example/colocboost_ctx2.bed.gz`, derived from context 1 with added noise \u2014 *demo data only, not a real measurement*) and a multi-context phenotype manifest. With that in place the `mnm` step now runs end-to-end on the toy data and produces `multicontext_bvsr.rds`, `multicontext_bvsr.rds`, and `multicontext_data.rds` per gene."
]
},
{
@@ -302,19 +438,9 @@
"kernel": "SoS"
},
"source": [
- "The notebook also bundles the original protocol's multivariate (`mnm`, `mnm_genes`) and functional (`fsusie`, `mvfsusie`) fine-mapping workflows, which operate on the same `regional_data` produced by `get_analysis_regions`. Of these, **`susie_twas` (univariate), `fsusie`, `mnm`, and `mnm_genes` now run end-to-end on the toy data** (`mnm` and `mnm_genes` use a synthetic second context, see below). `mnm_genes` consumes the univariate fine-mapping summary statistics (`combined_data_sumstats`) written by `susie_twas --save-data` together with the `mnm` outputs, wired together through a `--fine_mapping_meta` table; on the toy set it correctly returns NULL multigene results (no multi-gene/trans signal). **`mvfsusie`** remains **still under development** in the original protocol, so its command is given as a template for completeness."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "kernel": "SoS"
- },
- "source": [
- "### Multivariate analysis: mvSuSiE and mr.mash (`mnm`)\n",
- "\n",
- "Fine-maps multiple molecular phenotypes jointly across a shared window using mvSuSiE with mr.mash priors. This step needs **multiple contexts** (e.g. the same genes measured under two or more conditions). The toy set ships a single context, so for this MWE we add a **second, synthetic context** (`example/colocboost_ctx2.bed.gz`, derived from context 1 with added noise \u2014 *demo data only, not a real measurement*) and a multi-context phenotype manifest. With that in place the `mnm` step now runs end-to-end on the toy data and produces `multicontext_bvsr.rds`, `multicontext_twas_weights.rds`, and `multicontext_data.rds` per gene."
- ]
+ "**Timing**: TBD (on toy dataset)"
+ ],
+ "id": "c9ce1e11"
},
{
"cell_type": "code",
@@ -324,13 +450,6 @@
},
"outputs": [],
"source": [
- "# Runs end-to-end on the toy dataset using a synthetic 2-context manifest.\n",
- "# Notes for new users:\n",
- "# - mnm needs >=2 contexts; pheno_manifest_multicontext.tsv supplies context1 (real)\n",
- "# and context2 (example/colocboost_ctx2.bed.gz, synthetic demo data).\n",
- "# - Use the gene-expression genotype (input/colocboost/example.chr22.bed, sample IDs\n",
- "# SAMPLE_001..) so genotype and phenotype/covariate samples match.\n",
- "# - Requires the mr.mashr R package (provided by mr.mash.alpha in this env).\n",
"sos run pipeline/mnm_regression.ipynb mnm \\\n",
" --name protocol_example --cwd output_mnm3 \\\n",
" --genoFile input/colocboost/example.chr22.bed \\\n",
@@ -346,11 +465,21 @@
"kernel": "SoS"
},
"source": [
- "### xQTL fine-mapping across multiple genes (`mnm_genes`)\n",
+ "### Multi-gene / trans fine-mapping (`mnm_genes`)\n",
"\n",
- "Multivariate fine-mapping treating each gene as a separate condition across a shared window. **Verified end-to-end on the toy dataset** using the univariate (`susie_twas --save-data`) and `mnm` outputs wired together through a `--fine_mapping_meta` table. On the toy set every region returns a NULL `multigene_bvsr` (no multi-gene/trans signal), which is the correct, honest result for these independent single-gene regions."
+ "Multivariate fine-mapping treating each gene as a separate condition across a shared window. On the toy set every region returns a NULL `multigene_bvsr` (no multi-gene/trans signal), which is the correct, honest result for these independent single-gene regions."
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ],
+ "id": "0075bbe0"
+ },
{
"cell_type": "code",
"execution_count": null,
@@ -359,14 +488,6 @@
},
"outputs": [],
"source": [
- "# Multi-gene / trans fine-mapping (mnm_genes) - VERIFIED end-to-end on the toy data.\n",
- "# Run from the toy_example root. Requires:\n",
- "# --pheno_id_map_file : maps phenotype IDs across the two contexts\n",
- "# --fine_mapping_meta : per-region table pointing to the univariate (susie_twas, run above\n",
- "# with --save-data) and multivariate (mnm) fine-mapping outputs\n",
- "# --keep-samples : sample IDs to retain (matches the gene-expression genotype)\n",
- "# On this toy set all regions return a NULL multigene_bvsr (no multi-gene/trans signal),\n",
- "# which is the correct, honest result for independent single-gene toy regions.\n",
"sos run pipeline/mnm_regression.ipynb mnm_genes \\\n",
" --name protocol_example --cwd output_mnmgenes \\\n",
" --genoFile input/colocboost/example.chr22.bed \\\n",
@@ -385,11 +506,21 @@
"kernel": "SoS"
},
"source": [
- "### Functional regression fSuSiE for epigenomic QTL fine-mapping (`fsusie`)\n",
+ "### Functional regression fSuSiE for epigenomic QTL (`fsusie`)\n",
"\n",
- "Functional SuSiE for fine-mapping a functional phenotype across genomic positions in a region. This workflow **runs end-to-end on the toy peak data** and produces a `*.fsusie_mixture_normal_TI__top_pc_weights.rds` weight file."
+ "Functional SuSiE for fine-mapping a functional phenotype across genomic positions in a region. This workflow produces a `*.fsusie_mixture_normal_TI__top_pc_weights.rds` weight file."
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ],
+ "id": "c0c5fe72"
+ },
{
"cell_type": "code",
"execution_count": null,
@@ -398,8 +529,6 @@
},
"outputs": [],
"source": [
- "# Functional SuSiE (fsusie) - VERIFIED end-to-end on the toy peak data.\n",
- "# Run from the toy_example root. Produces *.fsusie_mixture_normal_TI__top_pc_weights.rds.\n",
"sos run pipeline/mnm_regression.ipynb fsusie \\\n",
" --name protocol_example --cwd output/fsusie \\\n",
" --genoFile input/genotype/protocol_example.genotype.chr22.bed \\\n",
@@ -415,11 +544,21 @@
"kernel": "SoS"
},
"source": [
- "### Functional regression fSuSiE with other modality (`mvfsusie`)\n",
+ "### Functional regression fSuSiE with another modality (`mvfsusie`)\n",
"\n",
"Multivariate functional SuSiE (mvfSuSiE). **Still under development** for the toy dataset."
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ],
+ "id": "62d2817e"
+ },
{
"cell_type": "code",
"execution_count": null,
@@ -428,8 +567,6 @@
},
"outputs": [],
"source": [
- "# Still under development on the toy dataset (the mvfSuSiE R step errors out).\n",
- "# Command template:\n",
"sos run pipeline/mnm_regression.ipynb mvfsusie \\\n",
" --name protocol_example --cwd output/mvfsusie \\\n",
" --genoFile input/genotype/protocol_example.genotype.chr22.bed \\\n",
@@ -445,17 +582,272 @@
"kernel": "SoS"
},
"source": [
- "## Troubleshooting\n",
+ "## Command Interface"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/mnm_regression.ipynb -h"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "```\n",
+ "usage: sos run pipeline/mnm_regression.ipynb\n",
+ " [workflow_name | -t targets] [options] [workflow_options]\n",
+ " workflow_name: Single or combined workflows defined in this script\n",
+ " targets: One or more targets to generate\n",
+ " options: Single-hyphen sos parameters (see \"sos run -h\" for details)\n",
+ " workflow_options: Double-hyphen workflow-specific parameters\n",
"\n",
- "| Step | Problem | Possible Reason | Solution |\n",
- "| --- | --- | --- | --- |\n",
- "| susie_twas | A region is skipped / no window found | 4th-column `ID` in the association-windows file does not match the phenotype list `ID` | Make the `ID` columns match exactly. |\n",
- "| susie_twas | `susie_twas: pass --region-name ` | No region selected | Pass `--region-name ` (e.g. `ENSG00000130538`) or a `--region-list`. |\n",
- "| susie_twas | `ERROR: One of the local workers has been killed` | Out-of-memory: too many regions in one process (`-j4`, or all 10 serially) | Use `-j1`; re-run heavy regions one at a time with a single-region phenotype list. |\n",
- "| mnm | `Target unavailable: ...qtl_dataset.rds` | `qtl_dataset_construct` not run first | Chain it: `qtl_dataset_construct+susie_twas`, then `mnm`. |\n",
- "| mnm | `--phenotype-manifest missing required column(s): cond` | Single-context manifest used | Use the multi-context manifest (`pheno_manifest_multicontext.tsv`). |\n",
- "| both | `No shared samples between phenotype and phenotype-covariate` | Covariates are in QTLtools (transposed) layout | Add `--transpose-covariates`. |\n",
- "| both | Sample IDs do not overlap between genotype/covariates and phenotype | Genotype `.fam` IDs differ from the phenotype matrix sample names | Rename one side so they match (e.g. the gene-expression mode renames genotype/covariate IDs to `SAMPLE_001..060`). |"
+ "Workflows:\n",
+ " qtl_dataset_construct\n",
+ " susie_twas\n",
+ " mnm\n",
+ " mnm_genes\n",
+ " fsusie\n",
+ " mvfsusie\n",
+ "\n",
+ "Global Workflow Options:\n",
+ " --modular-script-dir code/script (as path)\n",
+ " --name VAL (as str, required)\n",
+ " It is required to input the name of the analysis\n",
+ " --cwd output (as path)\n",
+ " --genoFile VAL (as path, required)\n",
+ " A list of file paths for genotype data, or the genotype\n",
+ " data itself.\n",
+ " --phenoFile paths\n",
+ "\n",
+ " One or multiple lists of file paths for phenotype data.\n",
+ " --phenoIDFile paths()\n",
+ "\n",
+ " One or multiple lists of file paths for phenotype ID\n",
+ " mapping file. The first column should be the original\n",
+ " ID, the 2nd column should be the ID to be mapped to.\n",
+ " --covFile paths\n",
+ "\n",
+ " Covariate file path\n",
+ " --region-list . (as path)\n",
+ " Optional: if a region list is provide the analysis will\n",
+ " be focused on provided region. The LAST column of this\n",
+ " list will contain the ID of regions to focus on\n",
+ " Otherwise, all regions with both genotype and phenotype\n",
+ " files will be analyzed\n",
+ " --region-name (as list)\n",
+ " Optional: if a region name is provided the analysis\n",
+ " would be focused on the union of provides region list\n",
+ " and region names\n",
+ " --keep-samples . (as path)\n",
+ " Only focus on a subset of samples\n",
+ " --keep-variants . (as path)\n",
+ " Only focus on a subset of variants\n",
+ " --customized-association-windows . (as path)\n",
+ " An optional list documenting the custom association\n",
+ " window for each region to analyze, with four column,\n",
+ " chr, start, end, region ID (eg gene ID). If this list is\n",
+ " not provided, the default `window` parameter (see below)\n",
+ " will be used.\n",
+ " --cis-window -1 (as int)\n",
+ " Specify the cis window for the up and downstream radius\n",
+ " to analyze around the region of interest in units of bp\n",
+ " When this is set to negative, we will rely on using\n",
+ " customized_association_windows\n",
+ " --[no-]save-data (default to False)\n",
+ " save data object or not\n",
+ " --phenotype-names [f'{x:bn}' for x in phenoFile]\n",
+ "\n",
+ " Name of phenotypes\n",
+ " --[no-]trans-analysis (default to False)\n",
+ " And indicator whether it is trans-analysis, ie, not\n",
+ " using phenotypic coordinate information\n",
+ " --seed 999 (as int)\n",
+ " --init-L 8 (as int)\n",
+ " association analysis paramters initial number of single\n",
+ " effects for SuSiE\n",
+ " --L 30 (as int)\n",
+ " maximum number of single effects to use for SuSiE\n",
+ " --[no-]estimate-residual-variance (default to True)\n",
+ " --imiss 1.0 (as float)\n",
+ " remove a variant if it has more than imiss missing\n",
+ " individual level data\n",
+ " --maf 0.0025 (as float)\n",
+ " MAF and variance of X cutoff\n",
+ " --xvar-cutoff 0.0 (as float)\n",
+ " --mac 5 (as int)\n",
+ " MAC cutoff, on top of MAF cutoff\n",
+ " --[no-]indel (default to True)\n",
+ " Remove indels if indel = False\n",
+ " --pip-cutoff 0.025 (as float)\n",
+ " --coverage 0.95 0.7 0.5 (as list)\n",
+ " --skip-analysis-pip-cutoff (as list)\n",
+ " If this value is not 0, then an initial single effect\n",
+ " analysis will be performed to determine if follow up\n",
+ " analysis will be continued or to simply return NULL If\n",
+ " this is negative we use a default way to determine this\n",
+ " cutoff which is conservative but still useful\n",
+ " --[no-]skip-fine-mapping (default to False)\n",
+ " Skip fine-mapping\n",
+ " --[no-]skip-twas-weights (default to False)\n",
+ " Skip TWAS weights computation\n",
+ " --twas-cv-folds 5 (as int)\n",
+ " Perform K folds valiation CV for TWAS Set it to zero if\n",
+ " this is to be skipped\n",
+ " --twas-cv-threads twas_cv_folds\n",
+ "\n",
+ " --max-cv-variants -1 (as int)\n",
+ " maximum number of variants to consider for CV We will\n",
+ " randomly pick a subset of it for CV purpose We can set\n",
+ " it to eg 8000 to save computational burden althought may\n",
+ " risk overfitting for methods comparison purpose When set\n",
+ " to -1 we don't use this feature\n",
+ " --ld-reference-meta-file . (as path)\n",
+ " --container ''\n",
+ " Analysis environment settings\n",
+ " --entrypoint ('micromamba run -a \"\" -n' + ' ' + re.sub(r'(_apptainer:latest|_docker:latest|\\.sif)$', '', container.split('/')[-1])) if container else \"\"\n",
+ "\n",
+ " --job-size 200 (as int)\n",
+ " For cluster jobs, number commands to run per job\n",
+ " --walltime 1h\n",
+ " Wall clock time expected\n",
+ " --mem 20G\n",
+ " Memory expected\n",
+ " --numThreads 1 (as int)\n",
+ " Number of threads\n",
+ "\n",
+ "Sections\n",
+ " qtl_dataset_construct:\n",
+ " Workflow Options:\n",
+ " --study study\n",
+ " Build one pecotmr::QtlDataset from the phenotype\n",
+ " manifest + shared genotype and serialize to RDS,\n",
+ " replacing the legacy get_analysis_regions /\n",
+ " regional_data loader. No fan-out: downstream per-gene\n",
+ " fine-mapping / TWAS load this single RDS and select\n",
+ " contexts/genes at analysis time. Manifest schema\n",
+ " (qtl_dataset_construct.R): columns ID, path, cond and an\n",
+ " optional cov_path; extra columns (e.g. #chr/start/end)\n",
+ " are ignored. The toy MWE pheno_manifest_multicontext.tsv\n",
+ " already matches -- a single-context analysis just points\n",
+ " at a one-`cond` manifest (or selects the context\n",
+ " downstream). QTLtools-format covariate TSVs (rows =\n",
+ " covariates) need --transpose-covariates.\n",
+ " --[no-]transpose-covariates (default to False)\n",
+ " Set for QTLtools-format covariate TSVs (rows =\n",
+ " covariates, cols = samples); required for the toy MWE\n",
+ " covariates.\n",
+ " --genotype-covariates . (as path)\n",
+ " Optional genotype-derived covariates applied uniformly\n",
+ " across contexts; per-context covariates come from the\n",
+ " manifest cov_path column.\n",
+ " --maf-cutoff 0.0 (as float)\n",
+ " Construct-time variant/sample QC (stored on the\n",
+ " QtlDataset; cutoff 0 = off).\n",
+ " --mac-cutoff 0.0 (as float)\n",
+ " --imiss-cutoff 0.0 (as float)\n",
+ " --[no-]drop-indel (default to False)\n",
+ " Drop indel variants (default keeps them, matching\n",
+ " QtlDataset()).\n",
+ " susie_twas:\n",
+ " Workflow Options:\n",
+ " --fine-mapping-methods susie\n",
+ " --twas-methods 'susie,mrash,enet,lasso,mcp,scad,l0learn,bayes_r,bayes_c'\n",
+ " --fine-mapping-coverage 0.95 (as float)\n",
+ " --contexts ''\n",
+ " Comma-separated context names to restrict both passes\n",
+ " to; empty = all contexts.\n",
+ " --pip-cutoff-to-skip 0.0 (as float)\n",
+ " Fine-mapping SER pre-screen (0 = off, <0 = adaptive\n",
+ " 3/nVariants).\n",
+ " --twas-maf-cutoff 0.01 (as float)\n",
+ " TWAS weight-learning knobs. TWAS learns weights applied\n",
+ " out-of-sample; keep an explicit MAF / X-variance cutoff\n",
+ " for the TWAS pass (passed via the shared --maf-\n",
+ " cutoff/--xvar-cutoff).\n",
+ " --twas-xvar-cutoff 0.01 (as float)\n",
+ " --cv-folds 5 (as int)\n",
+ " --cv-threads 1 (as int)\n",
+ " --maf-cutoff -1.0 (as float)\n",
+ " Opt-in per-analysis overrides of the QtlDataset's\n",
+ " construct-time filters, applied to both passes.\n",
+ " Sentinels: a negative cutoff / drop_indel False / \".\"\n",
+ " path = leave the dataset's stored value untouched.\n",
+ " --mac-cutoff -1.0 (as float)\n",
+ " --imiss-cutoff -1.0 (as float)\n",
+ " --[no-]drop-indel (default to False)\n",
+ " --fine-mapping-method-args ''\n",
+ " Optional JSON of per-method kwargs forwarded to the\n",
+ " pipelines, e.g. '{\"susie\":{\"L\":10}}' for fine-mapping or\n",
+ " '{\"lasso\":{\"nfolds\":10}}' for TWAS.\n",
+ " --twas-method-args ''\n",
+ " mnm:\n",
+ " Workflow Options:\n",
+ " --mvsusie-max-iter 200 (as int)\n",
+ " --pip-cutoff-to-skip 0.0 (as float)\n",
+ " --contexts ''\n",
+ " Comma-separated context names to restrict to; empty =\n",
+ " all contexts.\n",
+ " --prior-twas-weights . (as path)\n",
+ " Optional TwasWeights RDS (preceding mr.mash run)\n",
+ " supplying the data-driven prior.\n",
+ " --prior-weights-min 1e-10 (as float)\n",
+ " --mvsusie-method-args ''\n",
+ " Optional JSON of extra mvsusie kwargs (overrides the\n",
+ " max_iter default), e.g. '{\"mvsusie\":{\"max_iter\":500}}'.\n",
+ " mnm_genes_1:\n",
+ " mnm_genes_2:\n",
+ " Workflow Options:\n",
+ " --mvsusie-max-iter 200 (as int)\n",
+ " Cross-gene (multi-trait) mvSuSiE per locus: region mode\n",
+ " + jointSpecification=\"trait\" joins the genes whose\n",
+ " coordinates overlap the locus. Optional mr.mash data-\n",
+ " driven prior via --prior-twas-weights (canonical\n",
+ " otherwise). Replaces the legacy multigene combine +\n",
+ " multigene_udr machinery.\n",
+ " --pip-cutoff-to-skip 0.0 (as float)\n",
+ " --contexts ''\n",
+ " --prior-twas-weights . (as path)\n",
+ " --prior-weights-min 1e-10 (as float)\n",
+ " --mvsusie-method-args ''\n",
+ " fsusie_1:\n",
+ " fsusie_2:\n",
+ " Workflow Options:\n",
+ " --prior 'mixture_normal'\n",
+ " Functional SuSiE per locus (fsusieR::susiF) over the\n",
+ " pre-built QtlDataset. fsusie-specific knobs ride on\n",
+ " --method-args; --susie-top-pc>0 additionally fine-maps\n",
+ " each context's top principal components with univariate\n",
+ " SuSiE (usePCA; ports the legacy fsusie.R\n",
+ " susie_on_top_pc). Replaces fsusie.R.\n",
+ " --max-scale 10 (as int)\n",
+ " --min-purity 0.5 (as float)\n",
+ " --post-processing TI\n",
+ " --max-SNP-EM 100 (as int)\n",
+ " --[no-]small-sample-correction (default to False)\n",
+ " --susie-top-pc 0 (as int)\n",
+ " >0 -> also fine-map the top `susie_top_pc` PCs per\n",
+ " context with univariate SuSiE.\n",
+ " --pip-cutoff-to-skip 0.0 (as float)\n",
+ " --contexts ''\n",
+ " --fsusie-method-args ''\n",
+ " Optional JSON overriding the assembled fsusie kwargs,\n",
+ " e.g. '{\"fsusie\":{\"prior\":\"mixture_normal_per_scale\"}}'.\n",
+ " mvfsusie:\n",
+ " Workflow Options:\n",
+ " --prior 'mixture_normal_per_scale'\n",
+ " prior can be either of [\"mixture_normal\",\n",
+ " \"mixture_normal_per_scale\"]\n",
+ " --max-SNP-EM 1000 (as int)\n",
+ "```"
]
},
{
@@ -974,6 +1366,25 @@
" data.format=\"list_df\")\n",
" saveRDS(fitted, ${_output:ar})"
]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### Troubleshooting\n",
+ "\n",
+ "| Step | Problem | Possible Reason | Solution |\n",
+ "| --- | --- | --- | --- |\n",
+ "| susie_twas | A region is skipped / no window found | 4th-column `ID` in the association-windows file does not match the phenotype list `ID` | Make the `ID` columns match exactly. |\n",
+ "| susie_twas | `susie_twas: pass --region-name ` | No region selected | Pass `--region-name ` (e.g. `ENSG00000130538`) or a `--region-list`. |\n",
+ "| susie_twas | `ERROR: One of the local workers has been killed` | Out-of-memory: too many regions in one process (`-j4`, or all 10 serially) | Use `-j1`; re-run heavy regions one at a time with a single-region phenotype list. |\n",
+ "| mnm | `Target unavailable: ...qtl_dataset.rds` | `qtl_dataset_construct` not run first | Chain it: `qtl_dataset_construct+susie_twas`, then `mnm`. |\n",
+ "| mnm | `--phenotype-manifest missing required column(s): cond` | Single-context manifest used | Use the multi-context manifest (`pheno_manifest_multicontext.tsv`). |\n",
+ "| both | `No shared samples between phenotype and phenotype-covariate` | Covariates are in QTLtools (transposed) layout | Add `--transpose-covariates`. |\n",
+ "| both | Sample IDs do not overlap between genotype/covariates and phenotype | Genotype `.fam` IDs differ from the phenotype matrix sample names | Rename one side so they match (e.g. the gene-expression mode renames genotype/covariate IDs to `SAMPLE_001..060`). |"
+ ]
}
],
"metadata": {
@@ -1016,4 +1427,4 @@
},
"nbformat": 4,
"nbformat_minor": 4
-}
+}
\ No newline at end of file
diff --git a/code/SoS/mnm_analysis/mnm_methods/rss_analysis.ipynb b/code/SoS/mnm_analysis/mnm_methods/rss_analysis.ipynb
index b1f523569..8bdca6b5b 100644
--- a/code/SoS/mnm_analysis/mnm_methods/rss_analysis.ipynb
+++ b/code/SoS/mnm_analysis/mnm_methods/rss_analysis.ipynb
@@ -8,55 +8,106 @@
"source": [
"# RSS Fine-mapping with GWAS Summary Statistics\n",
"\n",
- "End-to-end SuSiE-RSS fine-mapping over GWAS summary statistics, per study × per LD block, using\n",
- "the current pecotmr S4 API (`GwasSumStats` / `summaryStatsQc()` / `fineMappingPipeline()`). Four SoS\n",
- "steps:\n",
+ "End-to-end SuSiE-RSS fine-mapping over GWAS summary statistics, one fit per study per LD block."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "## Overview\n",
+ "\n",
+ "Fine-mapping asks which variants in a region are causal, not merely correlated with a causal one. Doing that properly needs individual-level genotypes, which for GWAS are usually unavailable. SuSiE-RSS fits the same sum-of-single-effects model to what can be obtained -- per-variant z-scores plus an LD matrix from a reference panel -- and returns credible sets and posterior inclusion probabilities rather than a single lead SNP per locus ([Zou et al., 2022](https://doi.org/10.1371/journal.pgen.1010299)).\n",
+ "\n",
+ "Because the LD reference stands in for the study own genotypes, results are only as good as the match between the two. Allele flips and z-score outliers relative to the panel are the usual source of spurious credible sets, which is why the QC options exist: SLALOM or DENTIST screening flags suspicious variants, and RAISS can impute variants that are missing from the study but present in the panel. The LD reference must be genotype-backed (PLINK2/PLINK1/GDS/VCF); a precomputed `.cor.xz` correlation matrix is not supported here, because the QC steps need the genotypes themselves.\n",
+ "\n",
+ "The module takes a list of LD reference blocks and a list of GWAS summary statistics files and runs four SoS steps, which chain in the order below:\n",
+ "\n",
+ "| Workflow | What it does |\n",
+ "|---|---|\n",
+ "| `generate_manifest` | Pair each study with the LD blocks it covers |\n",
+ "| `generate_gwas_sumstats` | Harmonise, QC and optionally impute summary statistics per block |\n",
+ "| `gwas_fine_mapping` | Fit SuSiE-RSS per block |\n",
+ "| `gwas_rss_plot` | Diagnostic plots for the fitted regions |\n",
+ "\n",
+ "`generate_manifest` resolves `--gwas-meta` or `--gwas-tsv-list` against `--region-list` or `--regions` into one manifest TSV with a row per study x region. `generate_gwas_sumstats` then builds one QC-d `GwasSumStats` RDS per study x LD block, harmonising variants to the LD-panel ID convention. `gwas_fine_mapping` dispatches to `pecotmr::fineMappingPipeline()`, and `gwas_rss_plot` renders a PIP plot unless `--no-plot` is given. The results feed TWAS and PRS weight construction downstream.\n",
+ "\n",
+ "This chain replaces the legacy two-step `get_analysis_regions` + `univariate_rss` workflow, which called the now-removed `rss_analysis_pipeline()`.\n",
+ "\n",
+ "**When to run it.** After GWAS summary statistics and a genotype-backed LD reference panel are in\n",
+ "place, and before TWAS or PRS weight construction, which read the `GwasFineMappingResult` objects\n",
+ "written here. This is the summary-statistics-only route: with individual-level genotypes and\n",
+ "phenotypes, use `mnm_regression` instead."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "## Input\n",
+ "\n",
+ "GWAS inputs -- supply either form:\n",
+ "\n",
+ "- `--gwas-meta input/rss_analysis/protocol_example.rss_mwe.gwas_meta.tsv` TSV with columns `study_id`, `path` (relative to the meta file, or absolute) and an optional `column_mapping` naming a per-study YAML. One row per study.\n",
+ "\n",
+ " ```\n",
+ " study_id\tpath\tcolumn_mapping\n",
+ " AD_Bellenguez_2022\tprotocol_example.gwas_sumstats.chr22.tsv.gz\tprotocol_example.gwas_column_mapping.fixed.yml\n",
+ " ```\n",
+ "\n",
+ " In this file `path` resolves to `input/rss_analysis/protocol_example.gwas_sumstats.chr22.tsv.gz`\n",
+ " and `column_mapping` to `input/rss_analysis/protocol_example.gwas_column_mapping.fixed.yml`.\n",
+ "\n",
+ "- `--gwas-tsv-list S1=path1 ...`: explicit `STUDY=PATH` pairs; no per-study column mapping is possible this way.\n",
"\n",
- "1. **`[generate_manifest]`** — `gwas_rss_manifest.R` resolves `--gwas-meta` + `--gwas-tsv-list` against\n",
- " `--region-list` + `--regions` into one manifest TSV (one row per study × region).\n",
- "2. **`[generate_gwas_sumstats]`** — `gwas_sumstats_construct.R` builds one `GwasSumStats` RDS per\n",
- " study × LD block, running `summaryStatsQc()` (optional SLALOM/DENTIST QC + RAISS imputation;\n",
- " harmonization re-keys variants to the LD-panel id convention).\n",
- "3. **`[gwas_fine_mapping]`** — `fine_mapping.R` dispatches to\n",
- " `pecotmr::fineMappingPipeline(gwasSumStats, methods = ...)` (SuSiE-RSS).\n",
- "4. **`[gwas_rss_plot]`** (opt-out via `--no-plot`) — `gwas_rss_plot.R` renders a PIP plot.\n",
+ "Each summary statistics file is tab-delimited, with `chrom`, `pos`, `A1`, `A2` as the first four columns. A column-mapping YAML lets other layouts be read: it must resolve `chrom`, `pos`, `A1`, `A2`, either `z` or both `beta` and `se`, and `n_sample` or the pair `n_case` / `n_control` (for a case-control study, give one or the other and fill the unused field with 0). `var_y` is optional.\n",
"\n",
- "This replaces the legacy two-step workflow (`get_analysis_regions` + `univariate_rss`, which called the\n",
- "removed `rss_analysis_pipeline()`). The LD reference must be **genotype-backed** (PLINK2/PLINK1/GDS/VCF);\n",
- "a precomputed `.cor.xz` LD matrix is not supported for the `GwasSumStats` path.\n",
+ "Region inputs -- supply either form:\n",
"\n",
- "## Inputs\n",
+ "- `--region-list`: BED-like TSV of LD blocks.\n",
+ "- `--regions chr:start-end ...`: explicit regions.\n",
"\n",
- "- `--gwas-meta` — TSV with columns `study_id`, `path` (relative to the meta file or absolute), and\n",
- " optional `column_mapping` (per-study YAML). One row per study.\n",
- "- `--gwas-tsv-list S1=path1 ...` — explicit STUDY=PATH pairs (alternative to `--gwas-meta`; no\n",
- " per-study column mapping).\n",
- "- `--region-list` — BED-like TSV of LD blocks; and/or `--regions chr:start-end ...`.\n",
- "- `--ld-meta` — LD-reference meta TSV (`#chr`, `start`, `end`, `path` -> genotype prefix). One LD block\n",
- " per row (or one row with `start=end=0` for a whole chromosome).\n",
+ "LD reference and genome:\n",
"\n",
- "## QC knobs (forwarded to `gwas_sumstats_construct.R` -> `summaryStatsQc()`)\n",
+ "- `--ld-meta` (required): LD-reference meta TSV with columns `#chr`, `start`, `end`, `path`, where `path` is a genotype prefix. One LD block per row, or a single row with `start=end=0` for a whole chromosome. The reference must be genotype-backed (PLINK2/PLINK1/GDS/VCF).\n",
+ " The protocol ships `input/ld_reference/protocol_example.ld_meta_file.tsv`, whose prefixes point\n",
+ " into `input/ld_reference/chr*/`:\n",
"\n",
- "- `--qc-method` — `none` (default), `slalom`, or `dentist`.\n",
- "- `--impute` — enable RAISS sumstat imputation.\n",
- "- `--qc-args '{\"key\":value,...}'` — JSON passthrough for any other `summaryStatsQc()` kwarg (e.g.\n",
- " `{\"mafCutoff\":0.01,\"nCutoff\":0,\"alleleFlipKriging\":true}`).\n",
- "- `--maf` — MAF cutoff (`mafCutoff`). Default **0.0025**; 0 disables.\n",
- "- `--skip-regions chr:start-end …` — drop variants overlapping these window(s) (`skipRegion`).\n",
- "- `--skip-analysis-pip-cutoff` — skip a region whose single-effect max PIP is below this (`pipCutoffToSkip`). Default **0.025**; 0 disables.\n",
+ " ```\n",
+ " #chr\tstart\tend\tpath\n",
+ " chr1\t16103\t2888443\tchr1/protocol_example.LD.chr1\n",
+ " chr1\t2888443\t4320284\tchr1/protocol_example.LD.chr1\n",
+ " ```\n",
"\n",
- "## Fine-mapping knobs (forwarded to `fine_mapping.R`)\n",
+ "- `--genome` (default `GRCh38`): genome build label carried into the output objects.\n",
"\n",
- "- `--methods` (default `susie`), `--coverage` (0.95), `--secondary-coverage` (`0.7,0.5`).\n",
- "- `--min-abs-corr` (0.8), `--median-abs-corr` (off; OR-logic purity), `--pip-cutoff` (0.025).\n",
- "- `--L` (20) / `--L-greedy` (5) — SuSiE fit defaults; `--method-args '{\"susie\":{...}}'` overrides per key.\n",
+ "QC knobs, forwarded to `gwas_sumstats_construct.R` and on to `summaryStatsQc()`:\n",
"\n",
- "## Outputs\n",
+ "- `--qc-method` (default `none`): `none`, `slalom` or `dentist`.\n",
+ "- `--impute` / `--no-impute` (default off): enable RAISS imputation of variants present in the panel but missing from the study.\n",
+ "- `--qc-args`: JSON passthrough for any other `summaryStatsQc()` argument, e.g. `{\"mafCutoff\":0.01,\"nCutoff\":0,\"alleleFlipKriging\":true}`.\n",
+ "- `--maf` (default `0.0025`; `0` disables): MAF cutoff, i.e. `mafCutoff`.\n",
+ "- `--allele-flip-kriging` / `--no-allele-flip-kriging` (default off) and `--effective-n` / `--no-effective-n` (default on): individual `summaryStatsQc()` switches.\n",
+ "- `--skip-regions chr:start-end ...`: drop variants overlapping these windows, i.e. `skipRegion`.\n",
+ "- `--skip-analysis-pip-cutoff` (default `0.025`; `0` disables): skip a region whose single-effect max PIP falls below this, i.e. `pipCutoffToSkip`.\n",
"\n",
- "- `{cwd}/{manifest_name}.manifest.tsv`\n",
- "- `{cwd}/sumstats/{study}.{chrN_S_E}.gwas_sumstats.rds`\n",
- "- `{cwd}/fine_mapping/{study}.{chrN_S_E}.gwas_finemap.rds` — one `GwasFineMappingResult` per (study, block).\n",
- "- `{cwd}/plots/{study}.{chrN_S_E}.pip_plot.png` (unless `--no-plot`).\n"
+ "Fine-mapping knobs, forwarded to `fine_mapping.R`:\n",
+ "\n",
+ "- `--methods` (default `susie`), `--coverage` (`0.95`), `--secondary-coverage` (`0.7,0.5`).\n",
+ "- `--min-abs-corr` (`0.8`), `--median-abs-corr` (off; sets OR-logic purity), `--pip-cutoff` (`0.025`).\n",
+ "- `--L` (`20`) and `--L-greedy` (`5`): SuSiE fit defaults.\n",
+ "- `--method-args`: JSON overriding any of the above per method, e.g. `{\"susie\":{\"L\":10}}`.\n",
+ "- `--ser-fallback` / `--no-ser-fallback` (default on), `--r-finite`, `--r-mismatch` (`eb_mix`), `--rss-control`: numerical safeguards for the RSS likelihood.\n",
+ "\n",
+ "Other:\n",
+ "\n",
+ "- `--no-plot`: skip `gwas_rss_plot`.\n",
+ "- `--manifest-name` (default `gwas_rss`): output prefix for the manifest file.\n",
+ "- `--cwd` (default `output`), `--modular-script-dir` (default `code/script`), `--container`, `--job-size`, `--walltime`, `--mem`, `--numThreads`."
]
},
{
@@ -65,9 +116,68 @@
"kernel": "SoS"
},
"source": [
- "## Minimal working example\n",
+ "## Output\n",
+ "\n",
+ "- `gwas_rss.manifest.tsv` -- one row per study x region, recording which summary statistics file and\n",
+ " column mapping were paired with which LD block. Every later step reads it; the `gwas_rss` prefix\n",
+ " comes from `--manifest-name`. The example command produces a single row, since it runs one study\n",
+ " over one LD block, and `gwas_tsv` / `column_mapping` hold absolute paths, abbreviated here:\n",
+ "\n",
+ " ```\n",
+ " study_id\tgwas_tsv\tcolumn_mapping\tchr\tstart\tend\tregion_id\tgwas_tsv_basename\n",
+ " AD_Bellenguez_2022\t.../input/rss_analysis/protocol_example.gwas_sumstats.chr22.tsv.gz\t.../input/rss_analysis/protocol_example.gwas_column_mapping.fixed.yml\tchr22\t49355984\t50799822\tchr22_49355984_50799822\tprotocol_example.gwas_sumstats.chr22\n",
+ " ```\n",
+ "\n",
+ "- `sumstats/..gwas_sumstats.rds` -- a `GwasSumStats` object per study x block: the\n",
+ " QC-d, optionally imputed summary statistics, with variant IDs harmonised to the LD panel. The\n",
+ " step reports how many variants survived into the object:\n",
+ "\n",
+ " ```\n",
+ " Wrote QC'd GwasSumStats for study 'AD_Bellenguez_2022' over chr22:49355984-50799822 (7609 variants in)\n",
+ " ```\n",
"\n",
- "#### Genotype-LD MWE (AD_Bellenguez chr22 toy data under `input/`). There is no single combined workflow"
+ "- `fine_mapping/..gwas_finemap.rds` -- a `GwasFineMappingResult` object per study x\n",
+ " block, holding the SuSiE-RSS fit under `MethodNames` (`susie` by default): credible sets, PIPs,\n",
+ " single-effect posterior summaries, and the QC provenance of the variants that went in:\n",
+ "\n",
+ " ```\n",
+ " Wrote fineMapping result for GwasSumStats (1 row(s))\n",
+ " ```\n",
+ "\n",
+ "- `plots/..pip_plot.png` -- diagnostic PIP plot, unless `--no-plot` was given.\n",
+ "- a `.stdout` and a `.stderr` log beside each of the targets above.\n",
+ "\n",
+ "Everything is written under `--cwd`. `` is the `study_id` from the GWAS meta file and\n",
+ "`` is the LD block written as `chrN_start_end`, which for the example command above are\n",
+ "`AD_Bellenguez_2022` and `chr22_49355984_50799822` -- one file of each kind."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "## Minimal Working Example\n",
+ "### Default fine-mapping, no summary-statistics QC"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "This fine-maps one study x one LD block and writes the manifest, the QC-d `GwasSumStats`, the `GwasFineMappingResult`, and a PIP plot under `output/rss_analysis/`."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: ~2 min (on toy dataset)"
]
},
{
@@ -78,7 +188,6 @@
},
"outputs": [],
"source": [
- "cd /restricted/projectnb/xqtl/jaempawi/xqtl-protocol\n",
"sos run pipeline/rss_analysis.ipynb \\\n",
" generate_manifest+generate_gwas_sumstats+gwas_fine_mapping+gwas_rss_plot \\\n",
" --cwd output/rss_analysis --modular-script-dir code/script \\\n",
@@ -93,10 +202,16 @@
"kernel": "SoS"
},
"source": [
- "This fine-maps one study × one LD block and writes the manifest, the QC'd `GwasSumStats`, the\n",
- "`GwasFineMappingResult`, and a PIP plot under `output/rss_analysis/`.\n",
- "\n",
- "#### With SLALOM QC + RAISS imputation + per-method tuning:"
+ "### SLALOM QC, RAISS imputation and per-method tuning"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: ~2 min (on toy dataset)"
]
},
{
@@ -123,7 +238,7 @@
"kernel": "SoS"
},
"source": [
- "### Command interface"
+ "## Command Interface"
]
},
{
@@ -133,7 +248,172 @@
"kernel": "SoS"
},
"outputs": [],
- "source": "[global]\nparameter: cwd = path('output')\nparameter: modular_script_dir = path('code/script')\n# --- per-study GWAS inputs (use either or both) ---------------------\nparameter: gwas_meta = path('.')\nparameter: gwas_tsv_list = [] # list of STUDY=PATH items\n# --- region inputs (use either or both) -----------------------------\nparameter: region_list = path('.')\nparameter: regions = [] # list of chr:start-end strings\n# --- LD reference + genome ------------------------------------------\nparameter: ld_meta = path\nparameter: genome = 'GRCh38'\n# --- QC knobs (forwarded to gwas_sumstats_construct.R) --------------\nparameter: qc_method = 'none' # none | slalom | dentist\nparameter: impute = False\nparameter: qc_args = '' # JSON object spliced into summaryStatsQc()\nparameter: maf = 0.0025 # MAF cutoff (summaryStatsQc mafCutoff); 0 disables\nparameter: skip_regions = [] # chr:start-end window(s) whose variants are dropped (skipRegion)\nparameter: skip_analysis_pip_cutoff = 0.025 # skip a region below this max PIP (pipCutoffToSkip); 0 disables\nparameter: allele_flip_kriging = False # kriging allele-flip QC: sign-flip switched z (logLR>2 & |z|>2); off by default\nparameter: effective_n = True # case/control: use effective sample size 4/(1/n_case+1/n_control) as N (summaryStatsQc effectiveN); False -> raw N\n# --- fine-mapping knobs (forwarded to fine_mapping.R) ---------------\nparameter: methods = 'susie'\nparameter: coverage = 0.95\nparameter: secondary_coverage = '0.7,0.5'\nparameter: min_abs_corr = 0.8\nparameter: median_abs_corr = '' # empty -> off (OR-logic purity; needs pecotmr step-1)\nparameter: pip_cutoff = 0.025\nparameter: L = 20\nparameter: L_greedy = 5\nparameter: method_args = '' # nested per-method JSON object\n# GWAS SuSiE-RSS fine-mapping: SER fallback is ON by default (reproduces the single-panel notebook):\n# fit finite-sample R + EB LD-mismatch SuSiE-RSS and fall back to the single-effect (SER) result for\n# regions susieR flags as unreliable. ser_fallback and r_mismatch are orthogonal: ser_fallback = False\n# only disables the SER fallback (EB LD-mismatch stays on); pass r_mismatch = 'none' too for the\n# pre-change plain multi-effect fit. Requires susieR >= 0.16.6 (which moved R_mismatch_method /\n# check_prior into susie_rss_control(); pass those and other control settings via rss_control).\nparameter: ser_fallback = True # fall back to single-effect (SER) when susieR flags R unreliable\nparameter: r_finite = '' # finite-sample R size; empty -> auto (LD-panel sample size)\nparameter: r_mismatch = 'eb_mix' # LD-mismatch correction: 'none' | 'eb' | 'eb_mix' (residual-mixture EB, default)\nparameter: rss_control = '' # JSON object of susie_rss_control() settings, e.g. '{\"check_prior\":true,\"mismatch_estimator\":\"map\"}'; empty -> defaults\n# --- plot opt-out ---------------------------------------------------\nparameter: no_plot = False\n# --- output prefix for the manifest file ----------------------------\nparameter: manifest_name = 'gwas_rss'\n# --- infrastructure -------------------------------------------------\nparameter: container = ''\nparameter: job_size = 1\nparameter: walltime = '2h'\nparameter: mem = '16G'\nparameter: numThreads = 1"
+ "source": [
+ "sos run pipeline/rss_analysis.ipynb -h"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "```\n",
+ "usage: sos run pipeline/rss_analysis.ipynb\n",
+ " [workflow_name | -t targets] [options] [workflow_options]\n",
+ " workflow_name: Single or combined workflows defined in this script\n",
+ " targets: One or more targets to generate\n",
+ " options: Single-hyphen sos parameters (see \"sos run -h\" for details)\n",
+ " workflow_options: Double-hyphen workflow-specific parameters\n",
+ "\n",
+ "Workflows:\n",
+ " generate_manifest\n",
+ " generate_gwas_sumstats\n",
+ " gwas_fine_mapping\n",
+ " gwas_rss_plot\n",
+ "\n",
+ "Global Workflow Options:\n",
+ " --cwd output (as path)\n",
+ " --modular-script-dir code/script (as path)\n",
+ " --gwas-meta . (as path)\n",
+ " --- per-study GWAS inputs (use either or both)\n",
+ " ---------------------\n",
+ " --gwas-tsv-list (as list)\n",
+ " --region-list . (as path)\n",
+ " --- region inputs (use either or both)\n",
+ " -----------------------------\n",
+ " --regions (as list)\n",
+ " --ld-meta VAL (as path, required)\n",
+ " --- LD reference + genome\n",
+ " ------------------------------------------\n",
+ " --genome GRCh38\n",
+ " --qc-method none\n",
+ " --- QC knobs (forwarded to gwas_sumstats_construct.R)\n",
+ " --------------\n",
+ " --[no-]impute (default to False)\n",
+ " --qc-args ''\n",
+ " --maf 0.0025 (as float)\n",
+ " --skip-regions (as list)\n",
+ " --skip-analysis-pip-cutoff 0.025 (as float)\n",
+ " --[no-]allele-flip-kriging (default to False)\n",
+ " --[no-]effective-n (default to True)\n",
+ " --methods susie\n",
+ " --- fine-mapping knobs (forwarded to fine_mapping.R)\n",
+ " ---------------\n",
+ " --coverage 0.95 (as float)\n",
+ " --secondary-coverage '0.7,0.5'\n",
+ " --min-abs-corr 0.8 (as float)\n",
+ " --median-abs-corr ''\n",
+ " --pip-cutoff 0.025 (as float)\n",
+ " --L 20 (as int)\n",
+ " --L-greedy 5 (as int)\n",
+ " --method-args ''\n",
+ " --[no-]ser-fallback (default to True)\n",
+ " GWAS SuSiE-RSS fine-mapping: SER fallback is ON by\n",
+ " default (reproduces the single-panel notebook): fit\n",
+ " finite-sample R + EB LD-mismatch SuSiE-RSS and fall back\n",
+ " to the single-effect (SER) result for regions susieR\n",
+ " flags as unreliable. ser_fallback and r_mismatch are\n",
+ " orthogonal: ser_fallback = False only disables the SER\n",
+ " fallback (EB LD-mismatch stays on); pass r_mismatch =\n",
+ " 'none' too for the pre-change plain multi-effect fit.\n",
+ " Requires susieR >= 0.16.6 (which moved R_mismatch_method\n",
+ " / check_prior into susie_rss_control(); pass those and\n",
+ " other control settings via rss_control).\n",
+ " --r-finite ''\n",
+ " --r-mismatch 'eb_mix'\n",
+ " --rss-control ''\n",
+ " --[no-]no-plot (default to False)\n",
+ " --- plot opt-out\n",
+ " ---------------------------------------------------\n",
+ " --manifest-name 'gwas_rss'\n",
+ " --- output prefix for the manifest file\n",
+ " ----------------------------\n",
+ " --container ''\n",
+ " --- infrastructure\n",
+ " -------------------------------------------------\n",
+ " --job-size 1 (as int)\n",
+ " --walltime 2h\n",
+ " --mem 16G\n",
+ " --numThreads 1 (as int)\n",
+ "\n",
+ "Sections\n",
+ " generate_manifest:\n",
+ " generate_gwas_sumstats:\n",
+ " gwas_fine_mapping:\n",
+ " gwas_rss_plot:\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "## Workflow implementation"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "outputs": [],
+ "source": [
+ "[global]\n",
+ "parameter: cwd = path('output')\n",
+ "parameter: modular_script_dir = path('code/script')\n",
+ "# --- per-study GWAS inputs (use either or both) ---------------------\n",
+ "parameter: gwas_meta = path('.')\n",
+ "parameter: gwas_tsv_list = [] # list of STUDY=PATH items\n",
+ "# --- region inputs (use either or both) -----------------------------\n",
+ "parameter: region_list = path('.')\n",
+ "parameter: regions = [] # list of chr:start-end strings\n",
+ "# --- LD reference + genome ------------------------------------------\n",
+ "parameter: ld_meta = path\n",
+ "parameter: genome = 'GRCh38'\n",
+ "# --- QC knobs (forwarded to gwas_sumstats_construct.R) --------------\n",
+ "parameter: qc_method = 'none' # none | slalom | dentist\n",
+ "parameter: impute = False\n",
+ "parameter: qc_args = '' # JSON object spliced into summaryStatsQc()\n",
+ "parameter: maf = 0.0025 # MAF cutoff (summaryStatsQc mafCutoff); 0 disables\n",
+ "parameter: skip_regions = [] # chr:start-end window(s) whose variants are dropped (skipRegion)\n",
+ "parameter: skip_analysis_pip_cutoff = 0.025 # skip a region below this max PIP (pipCutoffToSkip); 0 disables\n",
+ "parameter: allele_flip_kriging = False # kriging allele-flip QC: sign-flip switched z (logLR>2 & |z|>2); off by default\n",
+ "parameter: effective_n = True # case/control: use effective sample size 4/(1/n_case+1/n_control) as N (summaryStatsQc effectiveN); False -> raw N\n",
+ "# --- fine-mapping knobs (forwarded to fine_mapping.R) ---------------\n",
+ "parameter: methods = 'susie'\n",
+ "parameter: coverage = 0.95\n",
+ "parameter: secondary_coverage = '0.7,0.5'\n",
+ "parameter: min_abs_corr = 0.8\n",
+ "parameter: median_abs_corr = '' # empty -> off (OR-logic purity; needs pecotmr step-1)\n",
+ "parameter: pip_cutoff = 0.025\n",
+ "parameter: L = 20\n",
+ "parameter: L_greedy = 5\n",
+ "parameter: method_args = '' # nested per-method JSON object\n",
+ "# GWAS SuSiE-RSS fine-mapping: SER fallback is ON by default (reproduces the single-panel notebook):\n",
+ "# fit finite-sample R + EB LD-mismatch SuSiE-RSS and fall back to the single-effect (SER) result for\n",
+ "# regions susieR flags as unreliable. ser_fallback and r_mismatch are orthogonal: ser_fallback = False\n",
+ "# only disables the SER fallback (EB LD-mismatch stays on); pass r_mismatch = 'none' too for the\n",
+ "# pre-change plain multi-effect fit. Requires susieR >= 0.16.6 (which moved R_mismatch_method /\n",
+ "# check_prior into susie_rss_control(); pass those and other control settings via rss_control).\n",
+ "parameter: ser_fallback = True # fall back to single-effect (SER) when susieR flags R unreliable\n",
+ "parameter: r_finite = '' # finite-sample R size; empty -> auto (LD-panel sample size)\n",
+ "parameter: r_mismatch = 'eb_mix' # LD-mismatch correction: 'none' | 'eb' | 'eb_mix' (residual-mixture EB, default)\n",
+ "parameter: rss_control = '' # JSON object of susie_rss_control() settings, e.g. '{\"check_prior\":true,\"mismatch_estimator\":\"map\"}'; empty -> defaults\n",
+ "# --- plot opt-out ---------------------------------------------------\n",
+ "parameter: no_plot = False\n",
+ "# --- output prefix for the manifest file ----------------------------\n",
+ "parameter: manifest_name = 'gwas_rss'\n",
+ "# --- infrastructure -------------------------------------------------\n",
+ "parameter: container = ''\n",
+ "parameter: job_size = 1\n",
+ "parameter: walltime = '2h'\n",
+ "parameter: mem = '16G'\n",
+ "parameter: numThreads = 1"
+ ]
},
{
"cell_type": "code",
@@ -166,7 +446,39 @@
"kernel": "SoS"
},
"outputs": [],
- "source": "[generate_gwas_sumstats]\n# Fan out over the manifest's rows: one (study, region) GwasSumStats per\n# row. Manifest columns: study_id, gwas_tsv, column_mapping, chr, start,\n# n_case, n_control, n_sample (optional), end, region_id, gwas_tsv_basename.\n# n_sample is the study-level total N: the level-4 fallback used when a study\n# has no per-variant n_case/n_control and no per-variant N column.\nimport csv\njobs = list(csv.DictReader(open(f\"{cwd}/{manifest_name}.manifest.tsv\"), delimiter='\\t'))\ninput: for_each = 'jobs'\noutput: f\"{cwd}/sumstats/{_jobs['study_id']}.{_jobs['region_id']}.gwas_sumstats.rds\"\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f\"{step_name}_{_output:bn}\"\nbash: expand = '${ }', stderr = f\"{_output}.stderr\", stdout = f\"{_output}.stdout\", container = container\n Rscript ${modular_script_dir}/pecotmr_integration/gwas_sumstats_construct.R \\\n --study ${_jobs['study_id']} \\\n --gwas-tsv ${_jobs['gwas_tsv']} \\\n --ld-block ${_jobs['chr']}:${_jobs['start']}-${_jobs['end']} \\\n --ld-meta ${ld_meta} \\\n --genome ${genome} \\\n ${('--column-mapping ' + _jobs['column_mapping']) if _jobs['column_mapping'] else ''} \\\n --qc-method ${qc_method} \\\n --maf ${maf} \\\n --pip-cutoff-to-skip ${skip_analysis_pip_cutoff} \\\n ${('--skip-region ' + ','.join(str(x) for x in skip_regions)) if skip_regions else ''} \\\n ${'--impute' if impute else ''} \\\n ${('--qc-args ' + repr(qc_args)) if qc_args else ''} \\\n ${'--allele-flip-kriging' if allele_flip_kriging else ''} \\\n --effective-n ${'TRUE' if effective_n else 'FALSE'} \\\n ${('--n-case ' + _jobs['n_case']) if _jobs.get('n_case','') not in ('', 'NA') else ''} \\\n ${('--n-control ' + _jobs['n_control']) if _jobs.get('n_control','') not in ('', 'NA') else ''} \\\n ${('--n-sample ' + _jobs['n_sample']) if _jobs.get('n_sample','') not in ('', 'NA') else ''} \\\n --output ${_output}"
+ "source": [
+ "[generate_gwas_sumstats]\n",
+ "# Fan out over the manifest's rows: one (study, region) GwasSumStats per\n",
+ "# row. Manifest columns: study_id, gwas_tsv, column_mapping, chr, start,\n",
+ "# n_case, n_control, n_sample (optional), end, region_id, gwas_tsv_basename.\n",
+ "# n_sample is the study-level total N: the level-4 fallback used when a study\n",
+ "# has no per-variant n_case/n_control and no per-variant N column.\n",
+ "import csv\n",
+ "jobs = list(csv.DictReader(open(f\"{cwd}/{manifest_name}.manifest.tsv\"), delimiter='\\t'))\n",
+ "input: for_each = 'jobs'\n",
+ "output: f\"{cwd}/sumstats/{_jobs['study_id']}.{_jobs['region_id']}.gwas_sumstats.rds\"\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f\"{step_name}_{_output:bn}\"\n",
+ "bash: expand = '${ }', stderr = f\"{_output}.stderr\", stdout = f\"{_output}.stdout\", container = container\n",
+ " Rscript ${modular_script_dir}/pecotmr_integration/gwas_sumstats_construct.R \\\n",
+ " --study ${_jobs['study_id']} \\\n",
+ " --gwas-tsv ${_jobs['gwas_tsv']} \\\n",
+ " --ld-block ${_jobs['chr']}:${_jobs['start']}-${_jobs['end']} \\\n",
+ " --ld-meta ${ld_meta} \\\n",
+ " --genome ${genome} \\\n",
+ " ${('--column-mapping ' + _jobs['column_mapping']) if _jobs['column_mapping'] else ''} \\\n",
+ " --qc-method ${qc_method} \\\n",
+ " --maf ${maf} \\\n",
+ " --pip-cutoff-to-skip ${skip_analysis_pip_cutoff} \\\n",
+ " ${('--skip-region ' + ','.join(str(x) for x in skip_regions)) if skip_regions else ''} \\\n",
+ " ${'--impute' if impute else ''} \\\n",
+ " ${('--qc-args ' + repr(qc_args)) if qc_args else ''} \\\n",
+ " ${'--allele-flip-kriging' if allele_flip_kriging else ''} \\\n",
+ " --effective-n ${'TRUE' if effective_n else 'FALSE'} \\\n",
+ " ${('--n-case ' + _jobs['n_case']) if _jobs.get('n_case','') not in ('', 'NA') else ''} \\\n",
+ " ${('--n-control ' + _jobs['n_control']) if _jobs.get('n_control','') not in ('', 'NA') else ''} \\\n",
+ " ${('--n-sample ' + _jobs['n_sample']) if _jobs.get('n_sample','') not in ('', 'NA') else ''} \\\n",
+ " --output ${_output}"
+ ]
},
{
"cell_type": "code",
@@ -175,7 +487,29 @@
"kernel": "SoS"
},
"outputs": [],
- "source": "[gwas_fine_mapping]\n# Per-RDS fan-out: one fine-mapping task per (study, region) GwasSumStats.\noutput: f\"{cwd}/fine_mapping/{_input:bnn}.gwas_finemap.rds\", group_by = 1\ntask: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f\"{step_name}_{_output:bn}\"\nbash: expand = '${ }', stderr = f\"{_output}.stderr\", stdout = f\"{_output}.stdout\", container = container\n Rscript ${modular_script_dir}/pecotmr_integration/fine_mapping.R \\\n --gwas-sumstats ${_input} \\\n --methods ${methods} \\\n --coverage ${coverage} \\\n --secondary-coverage ${secondary_coverage} \\\n --min-abs-corr ${min_abs_corr} \\\n --pip-cutoff ${pip_cutoff} \\\n --L ${L} \\\n --L-greedy ${L_greedy} \\\n ${('--median-abs-corr ' + str(median_abs_corr)) if str(median_abs_corr) != '' else ''} \\\n ${('--method-args ' + repr(method_args)) if method_args else ''} \\\n --ser-fallback ${'TRUE' if ser_fallback else 'FALSE'} \\\n --r-mismatch ${r_mismatch} \\\n ${('--r-finite ' + str(r_finite)) if str(r_finite) != '' else ''} \\\n ${('--rss-control ' + repr(rss_control)) if rss_control else ''} \\\n --output ${_output}"
+ "source": [
+ "[gwas_fine_mapping]\n",
+ "# Per-RDS fan-out: one fine-mapping task per (study, region) GwasSumStats.\n",
+ "output: f\"{cwd}/fine_mapping/{_input:bnn}.gwas_finemap.rds\", group_by = 1\n",
+ "task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f\"{step_name}_{_output:bn}\"\n",
+ "bash: expand = '${ }', stderr = f\"{_output}.stderr\", stdout = f\"{_output}.stdout\", container = container\n",
+ " Rscript ${modular_script_dir}/pecotmr_integration/fine_mapping.R \\\n",
+ " --gwas-sumstats ${_input} \\\n",
+ " --methods ${methods} \\\n",
+ " --coverage ${coverage} \\\n",
+ " --secondary-coverage ${secondary_coverage} \\\n",
+ " --min-abs-corr ${min_abs_corr} \\\n",
+ " --pip-cutoff ${pip_cutoff} \\\n",
+ " --L ${L} \\\n",
+ " --L-greedy ${L_greedy} \\\n",
+ " ${('--median-abs-corr ' + str(median_abs_corr)) if str(median_abs_corr) != '' else ''} \\\n",
+ " ${('--method-args ' + repr(method_args)) if method_args else ''} \\\n",
+ " --ser-fallback ${'TRUE' if ser_fallback else 'FALSE'} \\\n",
+ " --r-mismatch ${r_mismatch} \\\n",
+ " ${('--r-finite ' + str(r_finite)) if str(r_finite) != '' else ''} \\\n",
+ " ${('--rss-control ' + repr(rss_control)) if rss_control else ''} \\\n",
+ " --output ${_output}"
+ ]
},
{
"cell_type": "code",
@@ -225,4 +559,4 @@
},
"nbformat": 4,
"nbformat_minor": 4
-}
\ No newline at end of file
+}
diff --git a/code/SoS/mnm_analysis/mnm_miniprotocol.ipynb b/code/SoS/mnm_analysis/mnm_miniprotocol.ipynb
index d434e9072..26646e90d 100644
--- a/code/SoS/mnm_analysis/mnm_miniprotocol.ipynb
+++ b/code/SoS/mnm_analysis/mnm_miniprotocol.ipynb
@@ -2,248 +2,207 @@
"cells": [
{
"cell_type": "markdown",
- "id": "3ee1c5f8-3015-41d3-bdcb-38b63861bffe",
+ "id": "019555d7",
"metadata": {},
"source": [
- "# Integrative Analysis with High-Dimensional Regression\n",
+ "# Fine-mapping and TWAS analysis\n",
"\n",
- "This notebook shows the various fine-mapping, prediction, multivariate analysis and colocalization methods available in our pipeline."
+ "This mini-protocol selects among univariate, multivariate, multigene, functional and summary-statistic fine-mapping routes according to the available data and analysis goal."
]
},
{
"cell_type": "markdown",
- "id": "e03da35b",
- "metadata": {},
- "source": [
- "## Input Files\n",
- "\n",
- "| File | Description |\n",
- "| --- | --- |\n",
- "| `output/genotype_by_chrom/wgs.merged.plink_qc.*.bed` | Per-chromosome QC-passed genotype PLINK files |\n",
- "| `output/phenotype/*.region_list.txt` | Phenotype region lists (one per molecular phenotype) |\n",
- "| `output/covariate/*.gz` | Covariate matrices (known + hidden factors) |\n",
- "| `reference_data/TAD/TADB_enhanced_cis.bed` | TAD-based association windows |\n"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "497a0361-7646-4bdc-967f-0cd3ad55b301",
+ "id": "722e60a4",
"metadata": {},
"source": [
"#### Miniprotocol Timing\n",
- "This represents the total duration for all miniprotocol phases. While module-specific timings are provided separately on their respective pages, they are also included in this overall estimate. \n",
"\n",
- "**Timing (toy data, chr22):** < 10 minutes total. Measured wall-clock: univariate SuSiE / SuSiE-RSS fine-mapping ~1.5 minutes per region; fSuSiE and multivariate (mvSuSiE/mr.mash) fine-mapping are comparable on the toy single-region data. On real data, fine-mapping scales with region size, number of variants, and number of conditions."
+ "Timing: TBD"
]
},
{
"cell_type": "markdown",
- "id": "b179d380-7ed1-48ba-9d94-39c1e83733f0",
+ "id": "b8153dee",
"metadata": {},
"source": [
- "## Description\n",
+ "## Overview\n",
"\n",
- "Each of these parts are independent of one another.\n",
+ "The [MNM regression module](https://statfungen.github.io/xqtl-protocol/code/mnm_analysis/mnm_methods/mnm_regression.html) analyzes individual-level genotype and molecular-phenotype data. `qtl_dataset_construct+susie_twas` performs univariate SuSiE fine-mapping and estimates TWAS weights; `mnm` jointly analyzes multiple molecular traits with multivariate priors; `mnm_genes` extends the multivariate model across genes; and `fsusie` fine-maps functional or epigenomic phenotypes.\n",
"\n",
- "1. `mnm_regression.ipynb susie_twas`: Univariate Fine-Mapping and TWAS with SuSiE\n",
- "2. `mnm_regression.ipynb mnm_genes`: Multivariate Fine-Mapping for multiple genes\n",
- "3. `mnm_regression.ipynb fsusie`: Univariate Fine-Mapping of Functional (Epigenomic) Data with fSuSiE\n",
- "4. `mnm_regression.ipynb mnm`: Multivariate Fine-Mapping with mvSuSiE and mr.mash\n",
- "5. `rss_analysis.ipynb univariate_rss`: Regression with Summary Statistics (RSS) Fine-Mapping and TWAS with SuSiE"
+ "The [RSS module](https://statfungen.github.io/xqtl-protocol/code/mnm_analysis/mnm_methods/rss_analysis.html) instead combines GWAS summary statistics with an external LD reference. These five commands are alternative analysis routes rather than a mandatory chain."
]
},
{
"cell_type": "markdown",
- "id": "f1e1cd1a-d341-45de-876e-773e04789244",
+ "id": "c68e6e0a",
"metadata": {},
"source": [
- "## Steps"
+ "## Steps\n",
+ "\n",
+ "| **Analysis goal** | **Commands to run, in order** | **Inputs** |\n",
+ "| --- | --- | --- |\n",
+ "| Univariate molecular-QTL fine-mapping and TWAS weights | 1 | Genotype BED set, phenotype manifest, covariates and association windows under `input/colocboost/` |\n",
+ "| Multivariate fine-mapping across molecular traits | 2 | Step 1 inputs plus `input/colocboost/fine_mapping_meta.tsv` |\n",
+ "| Multigene multivariate fine-mapping | 3 | Multivariate inputs plus phenotype-ID map and retained-sample list under `input/colocboost/` |\n",
+ "| Fine-map functional or epigenomic phenotypes | 4 | `input/genotype/protocol_example.genotype.chr22.bed`; phenotype manifest, covariates and windows |\n",
+ "| GWAS summary-statistic fine-mapping | 5 | `input/rss_analysis/protocol_example.rss_mwe.gwas_meta.tsv`; `input/ld_reference/protocol_example.ld_meta_file.tsv` |\n",
+ "\n",
+ "Run only the command matching the analysis goal."
]
},
{
"cell_type": "markdown",
- "id": "293e2b0c-fe45-4500-b5d8-98f84b4f1883",
+ "id": "14f54de1",
"metadata": {},
"source": [
- "### i. [Univariate Fine-Mapping and TWAS with SuSiE](https://statfungen.github.io/xqtl-protocol/code/mnm_analysis/univariate_fine_mapping_twas_vignette.html)"
+ "### [1. Univariate fine-mapping and TWAS](https://statfungen.github.io/xqtl-protocol/code/mnm_analysis/mnm_methods/mnm_regression.html)\n",
+ "\n",
+ "**What it does:** `qtl_dataset_construct+susie_twas` builds the regional dataset, fits SuSiE and saves fine-mapping results and cross-validated TWAS weights."
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "2af1a144-4717-456d-8b45-6962f4d68ea0",
+ "id": "38e33d9f",
"metadata": {},
"outputs": [],
"source": [
- "sos run pipeline/mnm_regression.ipynb susie_twas \\\n",
- " --name test_susie_twas \\\n",
- " --genoFile output/genotype_by_chrom/wgs.merged.plink_qc.1.bed \\\n",
- " --phenoFile output/phenotype/phenotype_by_chrom_for_cis/bulk_rnaseq.phenotype_by_chrom_files.region_list.txt \\\n",
- " --covFile output/covariate/bulk_rnaseq_tmp_matrix.low_expression_filtered.outlier_removed.tmm.expression.covariates.wgs.merged.plink_qc.plink_qc.prune.pca.Marchenko_PC.gz \\\n",
- " --customized-association-windows reference_data/TAD/TADB_enhanced_cis.bed \\\n",
- " --phenotype-names test_pheno \\\n",
- " --max-cv-variants 5000 --ld_reference_meta_file data/ld_meta_file_with_bim.tsv \\\n",
- " --region-name ENSG00000049246 ENSG00000054116 ENSG00000116678 \\\n",
- " --save-data \\\n",
- " --cwd output/mnm_regression/susie_twas"
+ "sos run pipeline/mnm_regression.ipynb qtl_dataset_construct+susie_twas --name protocol_example --cwd output/mnm/univariate --genoFile input/colocboost/example.chr22.bed --phenoFile input/colocboost/pheno_manifest_multicontext.tsv --covFile input/colocboost/example_covariates.tsv --customized-association-windows input/colocboost/association_windows.bed --region-name ENSG00000130538 --transpose-covariates --save-data -j1"
]
},
{
"cell_type": "markdown",
- "id": "4b91d378-e96f-4f20-a54a-7df411b73875",
+ "id": "bb824285",
"metadata": {},
"source": [
- "### ii. [Multivariate Fine-Mapping for multiple genes](https://statfungen.github.io/xqtl-protocol/code/mnm_analysis/multivariate_multigene_fine_mapping_vignette.html)"
+ "### [2. Multivariate fine-mapping](https://statfungen.github.io/xqtl-protocol/code/mnm_analysis/mnm_methods/mnm_regression.html)\n",
+ "\n",
+ "**What it does:** `mnm` jointly fine-maps multiple molecular traits using the analyses and prior settings specified by the fine-mapping metadata."
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "a2e60016-d265-4e53-bc4c-3b415f430c33",
+ "id": "5075e2e6",
"metadata": {},
"outputs": [],
"source": [
- "sos run pipeline/mnm_regression.ipynb mnm_genes \\\n",
- " --name ROSMAP_Ast_mega_eQTL \\\n",
- " --genoFile data/mnm_genes/ROSMAP_NIA_WGS.leftnorm.bcftools_qc.plink_qc.11.bed \\\n",
- " --phenoFile data/mnm_genes/snuc_pseudo_bulk.Ast.mega.normalized.log2cpm.region_list.txt \\\n",
- " --covFile data/mnm_genes/snuc_pseudo_bulk.Ast.mega.normalized.log2cpm.rosmap_cov.ROSMAP_NIA_WGS.leftnorm.bcftools_qc.plink_qc.snuc_pseudo_bulk_mega.related.plink_qc.extracted.pca.projected.Marchenko_PC.gz \\\n",
- " --customized-association-windows data/mnm_genes/extended_TADB.bed \\\n",
- " --phenotype-names Ast_mega_eQTL \\\n",
- " --max-cv-variants 5000 \\\n",
- " --ld_reference_meta_file data/ld_meta_file_with_bim.tsv \\\n",
- " --independent_variant_list data/mnm_genes/ld_pruned_variants.txt.gz \\\n",
- " --fine_mapping_meta data/mnm_genes/combined_data_updated.tsv \\\n",
- " --phenoIDFile data/mnm_genes/phenoIDFile_extended_TADB.bed \\\n",
- " --region-name chr11_77324757_82556425 \\\n",
- " --skip-analysis-pip-cutoff 0 \\\n",
- " --maf 0.01 \\\n",
- " --coverage 0.95 \\\n",
- " --pheno_id_map_file data/mnm_genes/pheno_id_map_file.txt \\\n",
- " --prior-canonical-matrices \\\n",
- " --twas-cv-folds 0 \\\n",
- " --trans-analysis \\\n",
- " --cwd output/mnm_regression/mnm_genes -s build"
+ "sos run pipeline/mnm_regression.ipynb mnm --name protocol_example --cwd output/mnm/multivariate --genoFile input/colocboost/example.chr22.bed --phenoFile input/colocboost/pheno_manifest_multicontext.tsv --covFile input/colocboost/example_covariates.tsv --customized-association-windows input/colocboost/association_windows.bed --fine-mapping-meta input/colocboost/fine_mapping_meta.tsv --transpose-covariates --save-data -j1"
]
},
{
"cell_type": "markdown",
- "id": "af4443c5-6534-4bc5-9c88-6ab444d17b25",
+ "id": "16a9100b",
"metadata": {},
"source": [
- "### iii. [Univariate Fine-Mapping of Functional (Epigenomic) Data with fSuSiE](https://statfungen.github.io/xqtl-protocol/code/mnm_analysis/univariate_fine_mapping_fsusie_vignette.html)"
+ "### [3. Multigene multivariate fine-mapping](https://statfungen.github.io/xqtl-protocol/code/mnm_analysis/mnm_methods/mnm_regression.html)\n",
+ "\n",
+ "**What it does:** `mnm_genes` coordinates multivariate fine-mapping across genes while preserving phenotype identifiers and a common retained-sample set."
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "932ef298-5c45-4e18-abb6-89c15253bbd6",
+ "id": "92a38f2b",
"metadata": {},
"outputs": [],
"source": [
- "sos run pipeline/mnm_regression.ipynb fsusie \\\n",
- " --cwd output/fsusie/ \\\n",
- " --name test_fsusie \\\n",
- " --genoFile output/genotype_by_chrom/wgs.merged.plink_qc.genotype_by_chrom_files.txt \\\n",
- " --phenoFile output/phenotype/phenotype_by_chrom_for_cis/bulk_rnaseq.phenotype_by_chrom_files.region_list.txt \\\n",
- " --covFile output/covariate/bulk_rnaseq_tpm_matrix.low_expression_filtered.outlier_removed.tmm.expression.covariates.wgs.merged.plink_qc.plink_qc.prune.pca.Marchenko_PC.gz \\\n",
- " --numThreads 8 \\\n",
- " --customized-association-windows reference_data/TAD/TADB_enhanced_cis.bed \\\n",
- " --save-data \\\n",
- " --region-name ENSG00000049246 ENSG00000054116 ENSG00000116678 ENSG00000073921 ENSG00000186891"
+ "sos run pipeline/mnm_regression.ipynb mnm_genes --name protocol_example --cwd output/mnm/multigene --genoFile input/colocboost/example.chr22.bed --phenoFile input/colocboost/pheno_manifest_multicontext.tsv --covFile input/colocboost/example_covariates.tsv --customized-association-windows input/colocboost/association_windows.bed --pheno-id-map-file input/colocboost/pheno_id_map.tsv --fine-mapping-meta input/colocboost/fine_mapping_meta.tsv --keep-samples input/colocboost/keep_samples.txt --save-data -j1"
]
},
{
"cell_type": "markdown",
- "id": "58229f81-0bdd-4d2f-bf8c-28f542b7e6d3",
+ "id": "30bd5556",
"metadata": {},
"source": [
- "### iv. [Multivariate Fine-Mapping with mvSuSiE and mr.mash](https://statfungen.github.io/xqtl-protocol/code/mnm_analysis/multivariate_fine_mapping_vignette.html)"
+ "### [4. Functional fine-mapping](https://statfungen.github.io/xqtl-protocol/code/mnm_analysis/mnm_methods/mnm_regression.html)\n",
+ "\n",
+ "**What it does:** `fsusie` applies functional SuSiE to epigenomic or other functional phenotypes and saves posterior fine-mapping results and residual data."
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "4dc2dfdc-e39b-49cb-8bd1-e1d4927d39d0",
+ "id": "93829c34",
"metadata": {},
"outputs": [],
"source": [
- "sos run pipeline/mnm_regression.ipynb mnm \\\n",
- " --name test_mnm --cwd output/mnm \\\n",
- " --genoFile output/genotype_by_chrom/wgs.merged.plink_qc.genotype_by_chrom_files.txt \\\n",
- " --phenoFile output/phenotype/phenotype_by_chrom_for_cis/bulk_rnaseq.phenotype_by_chrom_files.region_list.txt \\\n",
- " --covFile output/covariate/bulk_rnaseq_tpm_matrix.low_expression_filtered.outlier_removed.tmm.expression.covariates.wgs.merged.plink_qc.plink_qc.prune.pca.Marchenko_PC.gz \\\n",
- " --customized-association-windows reference_data/TAD/TADB_enhanced_cis.bed \\\n",
- " --region-name ENSG00000073921 --save-data --no-skip-twas-weights \\\n",
- " --phenotype-names test_pheno \\\n",
- " --mixture_prior output/multivariate_mixture/MWE_ed_bovy.EE.prior.rds \\\n",
- " --max_cv_variants 5000 \\\n",
- "\t--ld_reference_meta_file data/ld_meta_file.tsv "
+ "sos run pipeline/mnm_regression.ipynb fsusie --name protocol_example --cwd output/mnm/fsusie --genoFile input/genotype/protocol_example.genotype.chr22.bed --phenoFile input/phenotype/protocol_example.pheno_manifest.tsv --covFile input/covariate/protocol_example.covariates.tsv --customized-association-windows input/finemapping/protocol_example.association_windows.bed --cis-window 0 --max-cv-variants 5000 --save-data -j1"
]
},
{
"cell_type": "markdown",
- "id": "323e3000-dd8c-4e52-b371-95226789c791",
+ "id": "28ac41b4",
"metadata": {},
"source": [
- "### v. [Regression with Summary Statistics (RSS) Fine-Mapping and TWAS with SuSiE](https://statfungen.github.io/xqtl-protocol/code/mnm_analysis/summary_stats_finemapping_vignette.html)"
+ "### [5. Summary-statistic fine-mapping](https://statfungen.github.io/xqtl-protocol/code/mnm_analysis/mnm_methods/rss_analysis.html)\n",
+ "\n",
+ "**What it does:** the RSS chain harmonizes regional GWAS summary statistics with the LD reference, runs SuSiE-RSS fine-mapping and produces a regional diagnostic plot."
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "304423fc-7070-483c-9ff3-ef0e9f95e8b2",
+ "id": "fed95103",
"metadata": {},
"outputs": [],
"source": [
- "sos run pipeline/rss_analysis.ipynb univariate_rss \\\n",
- " --ld-meta-data data/ld_meta_file_with_bim.tsv \\\n",
- " --gwas-meta-data data/mnm_regression/gwas_meta_data.txt \\\n",
- " --qc_method \"rss_qc\" --impute \\\n",
- " --finemapping_method \"susie_rss\" \\\n",
- " --cwd output/rss_analysis \\\n",
- " --skip_analysis_pip_cutoff 0 \\\n",
- " --skip_regions 6:25000000-35000000 \\\n",
- " --region_name 22:49355984-50799822"
+ "sos run pipeline/rss_analysis.ipynb generate_manifest+generate_gwas_sumstats+gwas_fine_mapping+gwas_rss_plot --cwd output/mnm/rss --modular-script-dir code/script --gwas-meta input/rss_analysis/protocol_example.rss_mwe.gwas_meta.tsv --regions chr22:49355984-50799822 --ld-meta input/ld_reference/protocol_example.ld_meta_file.tsv"
]
},
{
"cell_type": "markdown",
- "id": "99ef76cd-184b-4107-a8fe-523daa10a09c",
+ "id": "9ad16d51",
"metadata": {},
"source": [
- "## Anticipated Results"
+ "## Output Files\n",
+ "\n",
+ "| Route | Output filename and relative path | Description |\n",
+ "| --- | --- | --- |\n",
+ "| Univariate | `output/mnm/univariate/**/*.univariate_bvsr.rds`; `output/mnm/univariate/**/*.twas_weights.rds` | SuSiE posterior and TWAS-weight objects |\n",
+ "| Multivariate | `output/mnm/multivariate/**/*.multivariate_bvsr.rds`; `output/mnm/multivariate/**/*.twas_weights.rds` | Joint multivariate posterior and weights |\n",
+ "| Multigene | `output/mnm/multigene/**/*` | Per-region, per-gene multivariate results and metadata |\n",
+ "| Functional | `output/mnm/fsusie/**/*.fsusie.rds`; saved residual objects | Functional fine-mapping posterior and residual data |\n",
+ "| RSS | `output/mnm/rss/fine_mapping/AD_Bellenguez_2022.chr22_49355984_50799822.gwas_finemap.rds`; regional plot under `output/mnm/rss/plots/` | Summary-statistic fine-mapping result and diagnostic plot |"
]
},
{
"cell_type": "markdown",
- "id": "c2211dbf",
+ "id": "5b52900e",
"metadata": {},
"source": [
- "## Output Files\n",
+ "## Anticipated Results\n",
"\n",
- "| File | Description |\n",
- "| --- | --- |\n",
- "| `output/mnm_regression/*.univariate_bvsr.rds` | Univariate fine-mapping results (SuSiE) |\n",
- "| `output/mnm_regression/*.twas_weights.rds` | TWAS weights per gene |\n",
- "| `output/mnm_genes/*` | Multivariate fine-mapping results across multiple genes |\n"
+ "Each route produces posterior inclusion probabilities and credible-set information for the selected region and analysis model. Routes that estimate prediction weights also save TWAS-weight objects. The RSS route additionally records the harmonized regional summary statistics and LD-based diagnostics."
]
},
{
"cell_type": "markdown",
- "id": "c46afb81-7a8e-4b9b-ad3c-ebf409b33824",
+ "id": "16144133",
"metadata": {},
"source": [
- "Univariate finemapping will produce a file containing results for the top hits and a file containing twas weights produced by susie. Multigene finemapping with mvSuSiE will produce a file for each gene and region containing results for the top hits and a file containing twas weights produced by susie. Univariate finemapping for functional data with fSuSiE will produce a file containing results for the top hits and a file containing residuals from SuSiE. Multivariate finemapping will produce a file containing results for the top hits for each gene and a file containing twas weights produced by susie. Summary statistics fine-mapping produces a results file for each region and gwas of interest."
+ "## Command interface"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "34729ce0-94f8-4f78-8961-e3c3dc365f2e",
+ "id": "e8df299c",
"metadata": {},
"outputs": [],
- "source": []
+ "source": [
+ "sos run pipeline/mnm_regression.ipynb -h"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "c9dc6ea2",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "sos run pipeline/rss_analysis.ipynb -h"
+ ]
}
],
"metadata": {
diff --git a/code/SoS/mnm_analysis/mnm_postprocessing.ipynb b/code/SoS/mnm_analysis/mnm_postprocessing.ipynb
index e495554d7..7ff38c7cf 100644
--- a/code/SoS/mnm_analysis/mnm_postprocessing.ipynb
+++ b/code/SoS/mnm_analysis/mnm_postprocessing.ipynb
@@ -2,199 +2,891 @@
"cells": [
{
"cell_type": "markdown",
- "id": "a5f6e2e3",
+ "id": "d8817355",
"metadata": {
"kernel": "SoS"
},
"source": [
"# Fine-mapping result post-processing\n",
"\n",
- "This is a new-user-friendly minimal working example (MWE) of the `mnm_postprocessing` pipeline (the original `SuSiE_post_processing` notebook). It post-processes SuSiE fine-mapping results into flat, human-readable TSV tables. It runs end-to-end on the bundled chr22 toy dataset."
+ "Turns per-region fine-mapping objects into flat TSV tables, diagnostic plots, VCFs and QTL-GWAS overlap summaries."
]
},
{
"cell_type": "markdown",
- "id": "a34af82b",
+ "id": "fba6b747",
"metadata": {
"kernel": "SoS"
},
"source": [
- "## Description\n",
+ "## Overview\n",
"\n",
- "The full pipeline consolidates fine-mapping output into text tables, generates PIP/UpSet plots, exports VCFs, and overlaps QTL with GWAS results. Most of those steps need GWAS summary statistics, region/plot-recipe files, and a singularity container that the toy dataset does not ship.\n",
+ "Fine-mapping leaves you with per-region R objects, which are the wrong shape for almost\n",
+ "everything that comes next - inspecting results, sharing them, or intersecting them with\n",
+ "a GWAS. This module reads those objects and re-expresses them as text: `susie_to_tsv`\n",
+ "writes a per-variant table (PIP, credible-set membership, posterior mean and sd), a\n",
+ "wide per-variant by per-effect log-Bayes-factor table, and a per-credible-set effect\n",
+ "table. The remaining workflows build on those tables - collapsing them across regions,\n",
+ "drawing PIP landscape and UpSet plots, annotating variants, exporting VCFs, and\n",
+ "overlapping QTL credible sets with GWAS signals.\n",
"\n",
- "This MWE focuses on the core, genuinely runnable step, **`susie_to_tsv`**, which reads each per-region SuSiE object and writes three tables: a per-variant table (PIP, credible-set membership, posterior mean/sd), a log-Bayes-factor table, and an effect table. The remaining workflows are kept as documented command templates."
+ "The export step consumes a `FineMappingResult` object rather than a raw `susieR` fit,\n",
+ "so the three tables are views onto that object: `variant.tsv` is its `topLoci` view,\n",
+ "`lbf.tsv` its `lbf` view and `effect.tsv` its `cs_summary` view. Because the object\n",
+ "carries its own region and trait, `--region-list` is accepted for CLI compatibility but\n",
+ "is no longer used.\n",
+ "\n",
+ "**When to run it.** After fine-mapping, on the outputs of `susie_twas` or `fsusie`, and\n",
+ "before any downstream reporting or GWAS integration. Most of the workflows beyond\n",
+ "`susie_to_tsv` additionally need GWAS summary statistics, plot-recipe files or a\n",
+ "singularity container."
]
},
{
"cell_type": "markdown",
- "id": "970bb4c9",
+ "id": "569ef74c",
"metadata": {
"kernel": "SoS"
},
"source": [
- "## Input Files\n",
+ "## Input\n",
+ "\n",
+ "- `--rds_path` **`input/mnm_postprocessing/protocol_example.ENSG00000283047.fine_mapping.rds`**\n",
+ "(one or more `FineMappingResult` RDS files written by `mnm_regression`; each carries its own region and trait, and the step runs once per file)\n",
+ "\n",
+ "```\n",
+ "QtlFineMappingResult: S4 DataFrame, 2 rows x 7 columns\n",
+ "$ study: character [1:2] test_study test_study\n",
+ "$ context: character [1:2] context1 context2\n",
+ "$ trait: character [1:2] ENSG00000283047 ENSG00000283047\n",
+ "$ method: character [1:2] susie susie\n",
+ "$ entry: S4 object \n",
+ " ..$ listData: List of 2\n",
+ " .. ..$ : S4 object \n",
+ " .. .. ..$ variantIds: ...\n",
+ " .. .. ..$ susieFit: ...\n",
+ " .. .. ..$ topLoci: ...\n",
+ " .. .. ..$ cvResult: ...\n",
+ " .. ..$ : S4 object \n",
+ " .. .. ..$ variantIds: ...\n",
+ " .. .. ..$ susieFit: ...\n",
+ " .. .. ..$ topLoci: ...\n",
+ " .. .. ..$ cvResult: ...\n",
+ " ..$ elementType: character [1:1] ANY\n",
+ " ..$ elementMetadata: name [1:1] \u0001NULL\u0001\n",
+ " ..$ metadata: List of 0\n",
+ "... (truncated)\n",
+ "```\n",
+ "\n",
+ "- `--study toy` (tag used as `name` in output filenames and in intermediate RDS names. Required.)\n",
+ "- `--cwd output_postproc` (working directory all outputs are written under. Defaults to `output`.)\n",
+ "- `--container` (an empty string runs the step natively in the project environment instead of through a singularity image)\n",
+ "- `--region-list` (accepted for CLI compatibility and ignored. The `FineMappingResult` carries its own region and trait, so no external region list is needed.)\n",
"\n",
- "All paths are relative to the working directory `data/example_10peaks_susie_twas/` (run commands from there).\n",
+ "The same directory also holds `protocol_example.fsusie.fine_mapping.rds` and\n",
+ "`protocol_example.mvsusie.fine_mapping.rds`, for the `fsusie_to_tsv` and `mv_susie_2`\n",
+ "workflows.\n",
"\n",
- "- **SuSiE objects** (`--rds_path`): per-region fine-mapping RDS files. The toy inputs in `input_postproc/cs..uni.bvsr.rds` are produced by the prep step below, which reshapes the genuine `output_uni/fine_mapping/*.univariate_bvsr.rds` results (from the `mnm_regression` MWE) into the flat `susie` object structure `susie_to_tsv` expects. This only reshapes real fine-mapping output; it does not fabricate any values.\n",
- "- **Region list** (`--region-list`): a TSV with columns `#chr, start, end, gene_id` describing each region. `input_postproc/region_list.tsv` is written by the same prep step."
+ "The `export_top_loci` workflow consolidates results across regions and takes a different\n",
+ "set of inputs, none of which the example data ships:\n",
+ "\n",
+ "- `--file_path` (directory holding the per-region fine-mapping RDS files)\n",
+ "- `--prefix`, `--suffix` (the filename parts before and after the region id, so each region resolves to `/..` - for example `ROSMAP_AC_eQTL.ENSG00000012779.univariate_bvsr.rds`)\n",
+ "- `--qtl_type cis` (one of `cis`, `trans` or `trans_`; appears in the combined output filename)\n",
+ "- `--region_file` (BED file listing the regions to consolidate)\n",
+ "\n",
+ "The remaining workflows take these additional flags:\n",
+ "\n",
+ "- `--tsv_path output_postproc/*.lbf.tsv` (per-region LBF tables to concatenate; `susie_tsv_collapse`)\n",
+ "- `--annot_tibble ` (annotation tibble used to label variants; `susie_pip_landscape_plot` and `fsusie_extract_effect`)\n",
+ "- `--trait_to_select 1` (how many traits the credible-set UpSet diagram shows; `susie_upsetR_cs_plot`)\n",
+ "- `--SNP_list ` (variants to annotate; `tmp_annotatation_of_snps`)\n",
+ "- `--annotation_rds ` (annotation reference the variants are looked up against; `tmp_annotatation_of_snps`)\n",
+ "- `--geno_ref ` (genotype reference used to resolve variant identifiers; `export_top_loci`)\n",
+ "- `--min_corr 0.8` (minimum correlation for a variant to join a top-loci block; `export_top_loci`)\n",
+ "- `--condition_meta ` (GWAS meta file naming the conditions to overlap; `overlap_qtl_gwas`)"
]
},
{
"cell_type": "markdown",
- "id": "ac6cd03c",
+ "id": "1f12093a",
"metadata": {
"kernel": "SoS"
},
"source": [
- "## Steps\n",
+ "## Output\n",
+ "**Per-region TSV export** (`susie_to_tsv`, `sQTL_susie_to_tsv`, `fsusie_to_tsv`), one set per input SuSiE object:\n",
+ "\n",
+ "- `.variant.tsv` -- one row per variant: PIP, credible-set order and id, posterior mean and sd.\n",
+ "- `.lbf.tsv` -- per-variant log Bayes factors for each single-effect.\n",
+ "- `.effect.tsv` -- per-effect summary (`susie_to_tsv` and `sQTL_susie_to_tsv` only).\n",
+ "- `.all_variants.tsv` -- variants pooled across regions (`fsusie_to_tsv` only).\n",
+ "\n",
+ "**Collapsed tables** (`susie_tsv_collapse`):\n",
+ "\n",
+ "- `.chr.unisusie_rss.lbf.tsv` -- per-region lbf tables concatenated by chromosome.\n",
+ "\n",
+ "**Diagnostic plots**:\n",
+ "\n",
+ "- `.pip_landscape_plot.pdf` (`susie_pip_landscape_plot`)\n",
+ "- `.UpSetR.pdf` and `.UpSetR_cs.pdf` (`susie_upsetR_plot`, `susie_upsetR_cs_plot`)\n",
+ "\n",
+ "**Variant annotation** (`tmp_annotatation_of_snps`):\n",
+ "\n",
+ "- `.annotated.rds` and `.annotated_rev.rds`\n",
+ "\n",
+ "**fSuSiE effect extraction**:\n",
+ "\n",
+ "- `.estimated_effect.tsv` (`fsusie_extract_effect`)\n",
+ "- `.affected_region.tsv` (`fsusie_affected_region`)\n",
"\n",
- "### Prep: build the toy inputs (one-off)\n",
+ "**VCF export** (`mv_susie_2`):\n",
"\n",
- "Reshape the univariate SuSiE results into flat per-region `susie` RDS files and a matching region list. Run from the toy_example root."
+ "- `..vcf.bgz`\n",
+ "\n",
+ "**cis and GWAS consolidation**:\n",
+ "\n",
+ "- `_cache/_.tsv`, collapsed into `.cis_results_db.tsv` (or `.block_results_db.tsv` for GWAS input) -- `cis_results_export`\n",
+ "- `.union_export.tsv.gz` -- `gwas_results_export`\n",
+ "- `summary/.top_loci.bed.gz`, collapsed into `summary/..top_loci.bed.gz` -- `export_top_loci`\n",
+ "\n",
+ "**QTL and GWAS overlap** (`overlap_qtl_gwas`):\n",
+ "\n",
+ "- `gwas_qtl/cache/_gwas_batch_meta_.tsv`\n",
+ "- `.overlapped.gwas.tsv`\n",
+ "\n",
+ "No example output ships with the repository: `output_postproc/` is empty and the pipeline cannot be re-run here, so no structure dumps are shown. The filenames above come from each step `output:` statement."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ab9e91bd",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "## Minimal Working Example\n",
+ "### Export fine-mapping results to per-variant, LBF and effect tables\n",
+ "\n",
+ "`susie_to_tsv` reads each `FineMappingResult` object and writes its three text views."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "08b3aad0",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "65c8d622-ac64-43f7-9eb9-9e0abba73704",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/mnm_postprocessing.ipynb susie_to_tsv \\\n",
+ " --cwd output_postproc --study toy \\\n",
+ " --rds_path input/mnm_postprocessing/protocol_example.ENSG00000283047.fine_mapping.rds \\\n",
+ " --container '' -j1"
]
},
{
"cell_type": "markdown",
- "id": "47df52aa",
+ "id": "24086557",
"metadata": {
"kernel": "SoS"
},
"source": [
- "**Timing:** Runtime varies by dataset size and compute resources. For the toy chr22 MWE dataset, most steps complete in under 10 minutes on a standard HPC node."
+ "### fSuSiE and sQTL exports\n",
+ "`fsusie_to_tsv` and `sQTL_susie_to_tsv` perform the same export for fSuSiE and sQTL objects."
]
},
{
"cell_type": "markdown",
- "id": "525edba0",
+ "id": "cb487f2a-b879-421e-8fb2-d07e23328ebd",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "727843ad-29bf-430c-b56b-4e8493ef71fe",
"metadata": {
- "kernel": "R"
+ "kernel": "SoS"
},
+ "outputs": [],
"source": [
- "# Required CLI args (substitute placeholders <...> with your real values):\n",
- "# --file_path dir containing fine-mapping RDS files\n",
- "# --prefix filename part BEFORE the region id\n",
- "# --suffix filename part AFTER the region id\n",
- "# expected per-region path:\n",
- "# {file_path}/{prefix}.{region}.{suffix}\n",
- "# example: ROSMAP_AC_eQTL.ENSG00000012779.univariate_bvsr.rds\n",
- "# \\__ prefix __/ \\___ region ___/ \\___ suffix ___/\n",
- "# --study tag used in output filenames and intermediate RDS names\n",
- "# (final output: {cwd}/summary/{name}.{qtl_type}.top_loci.bed.gz)\n",
- "# --qtl_type cis | trans | trans_ appears in the combined filename\n",
- "# --region_file bed of regions (chr, start, end, region_id) to enumerate\n",
- "# --geno_ref tabix-indexed bim.gz for allele-flipping checks\n",
- "#\n",
- "# Outputs produced:\n",
- "# {cwd}/{name}.{region}.cis_results_db.rds per-region intermediate RDS\n",
- "# {cwd}/summary/{name}.{region}.top_loci.bed.gz per-region top_loci\n",
- "# {cwd}/summary/{name}.{qtl_type}.top_loci.bed.gz combined public-facing file\n",
- "\n",
- "# Full run: export all regions via SLURM and combine into one study-level file.\n",
- "sos run ./mnm_postprocessing.ipynb export_top_loci \\\n",
- " --region_file ./TADB_enhanced_cis.coding.bed \\\n",
- " --file_path /path/to/fine_mapping \\\n",
- " --prefix \\\n",
- " --suffix \\\n",
- " --study \\\n",
- " --min_corr 0.8 \\\n",
- " --geno_ref ./Fungen_xQTL.ROSMAP_NIA_WGS.ROSMAP_NIA_WGS.MSBB_WGS_ADSP_hg38.MiGA.MAP_Brain-xQTL_Gwas_geno_0.STARNET.aligned.bim.gz \\\n",
- " --qtl_type cis \\\n",
- " --cwd ~/tmp/export_run \\\n",
- " --mem 14G --walltime 72h \\\n",
- " -q slurm -j 100 -s build\n"
+ "sos run pipeline/mnm_postprocessing.ipynb fsusie_to_tsv \\\n",
+ " --cwd output_postproc --study \\\n",
+ " --rds_path input/mnm_postprocessing/protocol_example.fsusie.fine_mapping.rds \\\n",
+ " --container '' -j1"
]
},
{
"cell_type": "markdown",
- "id": "c25affda",
+ "id": "7fd91dcf-e082-4be6-ad16-8829a70f2008",
"metadata": {
"kernel": "SoS"
},
"source": [
- "### Step 1: `susie_to_tsv` (VERIFIED end-to-end on the toy data)\n",
- "\n",
- "Convert the SuSiE objects to per-variant / lbf / effect TSV tables. The `--container` argument is left empty so the step runs natively in the project environment.\n",
+ "### Collapse per-region tables\n",
+ "`susie_tsv_collapse` concatenates the per-region LBF tables into one table per chromosome."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f877232b-7fdb-455c-9c9e-beba4f7e0a82",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "637d4821",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/mnm_postprocessing.ipynb susie_tsv_collapse \\\n",
+ " --cwd output_postproc --study \\\n",
+ " --tsv_path output_postproc/*.lbf.tsv \\\n",
+ " --container '' -j1"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9ca18e40-ace0-42a8-aa12-e4fc4246c8f6",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### Diagnostic plots\n",
+ "`susie_pip_landscape_plot` draws the PIP landscape; `susie_upsetR_plot` and `susie_upsetR_cs_plot` draw UpSet diagrams of credible-set overlap. The landscape plot needs an annotation tibble; `--trait_to_select` picks how many traits the credible-set diagram shows."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b7d407f7-acb4-4221-8f7b-950f2ae7bc71",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "bbf7b676",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/mnm_postprocessing.ipynb susie_pip_landscape_plot \\\n",
+ " --cwd output_postproc --study \\\n",
+ " --rds_path /*.fine_mapping.rds \\\n",
+ " --annot_tibble \\\n",
+ " --container '' -j1\n",
"\n",
- "*Note: the preserved `susie_to_tsv` code (last cell) was written for full, untrimmed SuSiE objects. Four small compatibility fixes were applied in this userfriendly copy so it runs on the trimmed `output_uni` objects: keep variant IDs as character when parsing chr/pos; derive ref/alt from the `:`-separated variant ID; align `coef.susie()` length to the variant count; and use a per-effect summary of `lbf_variable` when a true per-effect `$lbf` is absent. These only reshape/realign genuine values - no results are fabricated. The original notebook is unchanged.*"
+ "sos run pipeline/mnm_postprocessing.ipynb susie_upsetR_cs_plot \\\n",
+ " --cwd output_postproc --study \\\n",
+ " --rds_path /*.fine_mapping.rds \\\n",
+ " --trait_to_select 1 \\\n",
+ " --container '' -j1"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d7c58c86-c02d-4b54-8e00-930f3671caca",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### Variant annotation\n",
+ "`tmp_annotatation_of_snps` annotates a variant list against a reference RDS, then writes the reverse mapping."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "52422638-2f5d-4c94-9ce9-a7defa55baa4",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "76af8671",
+ "id": "6df77104",
"metadata": {
"kernel": "SoS"
},
"outputs": [],
"source": [
- "sos run pipeline/mnm_postprocessing.ipynb susie_to_tsv \\\n",
- " --cwd output_postproc --study toy \\\n",
- " --rds_path input_postproc/cs.*.uni.bvsr.rds \\\n",
- " --region-list input_postproc/region_list.tsv \\\n",
- " --container \"\" -j1"
+ "sos run pipeline/mnm_postprocessing.ipynb tmp_annotatation_of_snps \\\n",
+ " --cwd output_postproc --study \\\n",
+ " --SNP_list \\\n",
+ " --annotation_rds \\\n",
+ " --container '' -j1"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "7b0d420c",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### fSuSiE effect extraction\n",
+ "`fsusie_extract_effect` writes the estimated effect curve per region; `fsusie_affected_region` writes the intervals a signal spans."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "48bfb469-3d81-408c-a9ea-7d00022ddd6a",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8d777cc1-69ac-495f-ac90-87b24553fcd4",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/mnm_postprocessing.ipynb fsusie_extract_effect \\\n",
+ " --cwd output_postproc --study \\\n",
+ " --rds_path input/mnm_postprocessing/protocol_example.fsusie.fine_mapping.rds \\\n",
+ " --annot_tibble \\\n",
+ " --container '' -j1"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "47b3dbd5",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### VCF export\n",
+ "`mv_susie_2` writes multivariate results as bgzipped VCF, one file per region and context."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "867a26e2-3f0c-4f68-8565-bfadcb2cd5a4",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "9622aa82-9ac3-4de0-bef5-4ca2b0b292e4",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/mnm_postprocessing.ipynb mv_susie \\\n",
+ " --cwd output_postproc --study \\\n",
+ " --rds_path input/mnm_postprocessing/protocol_example.mvsusie.fine_mapping.rds \\\n",
+ " --container '' -j1"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "78adbaad",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### cis and GWAS consolidation\n",
+ "`cis_results_export` collects per-region results into a results database, `gwas_results_export` does the same for GWAS blocks, `combine_export_meta` merges the per-chunk caches, and `export_top_loci` writes the combined top-loci BED. All four resolve each region as `/..` and need a GWAS meta file."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9d73d793-0adb-408f-8177-32369315fe17",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8ac99a21-0b36-4b79-9292-d7b7f7d8017c",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/mnm_postprocessing.ipynb export_top_loci \\\n",
+ " --cwd output_postproc --study \\\n",
+ " --region_file \\\n",
+ " --file_path \\\n",
+ " --prefix --suffix \\\n",
+ " --qtl_type cis --min_corr 0.8 \\\n",
+ " --geno_ref \\\n",
+ " --container '' -j1"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "148f96af",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "### QTL and GWAS overlap\n",
+ "`overlap_qtl_gwas` intersects QTL credible sets with GWAS results, caching a batch meta file per chunk before writing the overlap table."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "380f0b5e-25cd-433f-8f39-07b6a4f783c2",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "source": [
+ "**Timing**: TBD (on toy dataset)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "7163df29-7096-4633-8706-c0bd282d3cb9",
+ "metadata": {
+ "kernel": "SoS"
+ },
+ "outputs": [],
+ "source": [
+ "sos run pipeline/mnm_postprocessing.ipynb overlap_qtl_gwas \\\n",
+ " --cwd output_postproc --study \\\n",
+ " --region_file \\\n",
+ " --file_path \\\n",
+ " --prefix --suffix