From 7c82b0efae81a7ff6cc8ea5a384ece1e39c99f24 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 11:00:37 +0100 Subject: [PATCH 01/45] test(vntyper): fetch VNtyper test BAMs with curl and gzip, no Python The Zenodo archive never changes, so the deflated bytes of each member sit at a fixed offset. Pin them, fetch one range per member, and let gzip check the CRC-32 and size the archive stores; the sha256 pin stays. Bead: pgp-bs9u.39 --- tests/e2e/vntyper-1.sh | 4 +- tests/smoke/commands.tsv | 2 +- tests/smoke/vntyper.sh | 4 +- tests/smoke/vntyper_testdata.py | 103 -------------------------------- tests/smoke/vntyper_testdata.sh | 65 ++++++++++++++++++++ 5 files changed, 70 insertions(+), 108 deletions(-) delete mode 100644 tests/smoke/vntyper_testdata.py create mode 100755 tests/smoke/vntyper_testdata.sh diff --git a/tests/e2e/vntyper-1.sh b/tests/e2e/vntyper-1.sh index 7cf376973..9e30e05dd 100755 --- a/tests/e2e/vntyper-1.sh +++ b/tests/e2e/vntyper-1.sh @@ -5,7 +5,7 @@ # over MUC1): VNtyper has nothing to read, and the step says "not # assessed" with that reason, never "negative"; # - two BAMs of VNtyper's own public test data (tests/smoke/ -# vntyper_testdata.py reads them out of its Zenodo archive, sha256 +# vntyper_testdata.sh reads them out of its Zenodo archive, sha256 # pinned): their reads over MUC1 carried onto the fixture's header and # added to case 20's BAM, as a whole-genome BAM would hold them. The # sample with a frameshift reads positive, with the variant, and the one @@ -38,7 +38,7 @@ check "the fixture: the message says the BAM has no read over MUC1" has 'no read # --- VNtyper's positive and negative samples on the fixture's header ------------------- check "VNtyper's two test BAMs download and match their sha256" \ - retry python3 -I "${REPO}/tests/smoke/vntyper_testdata.py" "$T" positive.bam negative.bam + retry sh "${REPO}/tests/smoke/vntyper_testdata.sh" "$T" positive.bam negative.bam H="${SAMPLE}muc" for s in positive negative; do dk "$SAMTOOLS_IMAGE" samtools index "${T}/${s}.bam" diff --git a/tests/smoke/commands.tsv b/tests/smoke/commands.tsv index 11e807eb3..7ff8d98b9 100644 --- a/tests/smoke/commands.tsv +++ b/tests/smoke/commands.tsv @@ -52,7 +52,7 @@ PARASCOPY_IMAGE net,also=PARASCOPY_DATA_VERSION fullref,slice sh /smoke/parascop PARAPHASE_IMAGE net fullref v=${PARAPHASE_IMAGE##*:} && u=https://raw.githubusercontent.com/PacificBiosciences/paraphase/v${v%%--*}/tests/data/bams/HG01175_smn1_extracted.bam && wget -q -O hg.bam "$u" && wget -q -O hg.bam.bai "${u}.bai" && { printf '%s hg.bam\n%s hg.bam.bai\n' c9659364ad9e60e41ee4771cf92dc017313b6b75a1e08df3e8c0e95ef38eadca 8b5dd238fdf1cc508e85105ff2a157a2c3d73aa3f441d08afdba9458beecca51 | sha256sum -c || { echo 'the demo BAM or its index is not the file Paraphase v4.1.0 ships'; exit 1; }; } && paraphase -b hg.bam -r /in/ref.fa -o pp -p HG01175 -g smn1 -t 4 && cp pp/HG01175.paraphase.json smn1.json && rm -rf pp hg.bam hg.bam.bai py "SMN1, SMN2 and SMN exon 7-8 deletion copy numbers of HG01175 as Paraphase's demo publishes them (2, 1, 1)" "import json; s = json.load(open('smn1.json'))['smn1']; print(json.dumps(s)[:3000]); r = s['region_specific_info']; c = (r['smn1_cn'], r['smn2_cn'], r['smn_del78_cn']); print(c); assert c == (2, 1, 1)" # MEGAnE's call needs a whole chromosome: on mini.fa's 500 kb it stops in its own code (IndexError). The e2e case mei-1 runs it on chr20. MEGANE_IMAGE - ref build_kmerset -fa /in/mini.fa -prefix mini -outdir /out/mk && { call_genotype_38 -h > help.txt 2>&1 || true; } at_least "repeated 32-mers in the k-mer set of mini.fa" "$(cat mk/mini.mi)" 1000; same "the .mk holds the 8-byte k-mers the .mi counts" "$(( $(wc -c < mk/mini.mk) / 8 ))" "$(cat mk/mini.mi)"; contains "call_genotype_38 takes the -mainchr step 50 passes" help.txt "-mainchr"; contains "and -no_sex_chr" help.txt "-no_sex_chr"; contains "and -only_ins" help.txt "-only_ins" -# Step 52 on VNtyper's own test data, two BAMs read out of its Zenodo archive by range requests (tests/smoke/vntyper_testdata.py, +# Step 52 on VNtyper's own test data, two BAMs read out of its Zenodo archive by range requests (tests/smoke/vntyper_testdata.sh, # sha256 pinned): a sample with a MUC1 VNTR frameshift must be called, and one without must be negative (the red control); # a BAM with no read over MUC1 must stop VNtyper with the message step 52 turns into "no read over MUC1". VNTYPER_IMAGE net - sh /smoke/vntyper.sh has "the positive sample: Kestrel calls a frameshift with high precision" "$(awk -F'\t' '$1 == "positive" {print $2}' calls.tsv)" "^High_Precision"; same "the negative sample: no call" "$(awk -F'\t' '$1 == "negative" {print $2}' calls.tsv)" Negative; at_least "the positive sample: mean depth over the VNTR" "$(awk -F'\t' '$1 == "positive" {print int($4)}' calls.tsv)" 100; at_least "the negative sample: mean depth over the VNTR" "$(awk -F'\t' '$1 == "negative" {print int($4)}' calls.tsv)" 100; same "a BAM with no read over MUC1: VNtyper stops with exit 1" "$(cat empty_rc.txt)" 1; contains "and its log names the empty read layout step 52 reads" empty.log "FASTQ layout .empty."; has "vntyper --version is the release of the image tag" "$(cat version.txt)" "${VNTYPER_IMAGE##*:}" diff --git a/tests/smoke/vntyper.sh b/tests/smoke/vntyper.sh index 0c3d7c7bb..cd4d6cec1 100644 --- a/tests/smoke/vntyper.sh +++ b/tests/smoke/vntyper.sh @@ -1,12 +1,12 @@ # shellcheck shell=sh # VNTYPER_IMAGE row: what step 52 runs, on VNtyper's own public test data -# (tests/smoke/vntyper_testdata.py fetches two BAMs out of its Zenodo archive): +# (tests/smoke/vntyper_testdata.sh fetches two BAMs out of its Zenodo archive): # a sample with a MUC1 VNTR frameshift and one without. The image's samtools # indexes them, then VNtyper runs with the step's flags (and no IGV view in its HTML) # from /opt/vntyper, # the folder its config.json paths are relative to. set -e -python3 /smoke/vntyper_testdata.py /out/data positive.bam negative.bam +sh /smoke/vntyper_testdata.sh /out/data positive.bam negative.bam for s in positive negative; do samtools index "/out/data/${s}.bam" (cd /opt/vntyper && vntyper pipeline --bam "/out/data/${s}.bam" -o "/out/${s}" --sample-name "$s" \ diff --git a/tests/smoke/vntyper_testdata.py b/tests/smoke/vntyper_testdata.py deleted file mode 100644 index b33731d94..000000000 --- a/tests/smoke/vntyper_testdata.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env python3 -"""Fetch members of VNtyper's public test data without the whole archive. - -VNtyper publishes its test BAMs as one 1.2 GB zip on Zenodo (record 19181821, -"VNtyper 2.0 test data", the archive_file of tests/test_data_config.json in -VNtyper v2.0.47). A zip keeps its file list at the end, so this script reads -that list with an HTTP range request, then fetches and inflates only the -members it is asked for, about 10 MB instead of 1.2 GB (the .bai files are -not fetched: samtools index writes them). Each member is checked -against the CRC-32 the archive stores and against the sha256 pinned below. - -Usage: vntyper_testdata.py OUTDIR NAME... (NAME: a key of MEMBERS) -Standard library only: it runs in VNTYPER_IMAGE's Python and on the host. -""" -import hashlib -import os -import struct -import sys -import time -import urllib.request -import zlib - -URL = "https://zenodo.org/records/19181821/files/data.zip?download=1" -SIZE = 1174975271 # bytes; Zenodo lists md5 fdd5958d58c733cdca452f3287898964 for the whole file - -# name -> (path in the archive, sha256 of the inflated file) -MEMBERS = { - # Positive: a MUC1 VNTR frameshift VNtyper calls High_Precision* (its - # integration test example_dfc3_GRCh38_fast runs the same reads on NCBI - # contig names). Reads realigned by VNtyper to GRCh38 chr1, UCSC names. - "positive.bam": ("data/remapped/bwa/hg38/example_dfc3_hg38_bwa.bam", - "5934330a8ac2144ac0cc6ae8c84d7d9262e90b28127417e4068f7d662b7c64bb"), - # Negative: no variant called (its integration test - # example_40cf_hg38_subset_fast_gdp_guard), mean VNTR depth about 148x. - "negative.bam": ("data/example_40cf_hg38_subset.bam", - "44aff8e3b2096b7ea3529989660052c61b29c8a26afd7f48de4988f7047d0652"), -} - - -def get(start, end): - """Bytes start..end (inclusive) of the archive, with retries.""" - for attempt in range(4): - try: - req = urllib.request.Request(URL, headers={"Range": f"bytes={start}-{end}"}) - with urllib.request.urlopen(req, timeout=120) as r: - if r.status != 206: - raise OSError(f"HTTP {r.status}, want 206 (a range)") - data = r.read() - if len(data) != end - start + 1: - raise OSError(f"got {len(data)} bytes, want {end - start + 1}") - return data - except OSError as e: - if attempt == 3: - raise - print(f"range {start}-{end}: {e}; again in {15 * (attempt + 1)} s", file=sys.stderr) - time.sleep(15 * (attempt + 1)) - raise AssertionError("unreachable") - - -def directory(): - """{path: (method, crc, compressed size, size, local header offset)}""" - tail = get(SIZE - 65536, SIZE - 1) - i = tail.rfind(b"PK\x05\x06") - if i < 0: - raise SystemExit("no end-of-central-directory record in the last 64 kB") - _, _, _, _, n, cd_size, cd_off, _ = struct.unpack("> 8 & 255)) $(($1 >> 16 & 255)) $(($1 >> 24 & 255)))" +} + +[ $# -ge 2 ] || { echo "usage: vntyper_testdata.sh OUTDIR NAME... (NAME of positive.bam, negative.bam)" >&2; exit 2; } +out=$1 +shift +for name in "$@"; do + member "$name" > /dev/null || { echo "vntyper_testdata.sh: unknown member ${name} (positive.bam, negative.bam)" >&2; exit 2; } +done +mkdir -p "$out" +for name in "$@"; do + read -r path off csize size crc want <&2; exit 1; } + # gzip member: magic, deflate, no flags, no time, no extra flags, unknown OS; + # then the raw deflate stream; then CRC-32 and size, both little-endian. + { printf '\037\213\010\000\000\000\000\000\000\377'; cat "${part}.raw"; le32 "$((0x$crc))"; le32 "$size"; } > "${part}.gz" + gzip -dc "${part}.gz" > "$part" || { echo "${path}: size or CRC-32 differs from the archive's directory" >&2; exit 1; } + got=$(sha256sum "$part" | cut -d' ' -f1) + [ "$got" = "$want" ] || { echo "${path}: sha256 ${got}, pinned ${want}" >&2; exit 1; } + rm -f "${part}.raw" "${part}.gz" + mv "$part" "${out}/${name}" + printf '%s\t%s\t%s\t%s\n' "$name" "$path" "$size" "$got" +done From 463305a90714986c75c043b356e29e5e72bf6912 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 11:08:09 +0100 Subject: [PATCH 02/45] feat(dev): og dev fixture and og dev evidence verbs og dev fixture comphet writes the compound-het fixture byte for byte (CPython's Mersenne Twister for the seeded contig); og dev fixture trim-pharmcat trims a PharmCAT report; og dev evidence clashes and hg002-gvcf replace the public-evidence workflow's inline Python, streaming, with the covered positions kept as merged spans. Parity cases against the Python, and a golden for the site past a contig's end. Bead: pgp-bs9u.39 --- rust/crates/og-dev/src/evidence.rs | 354 ++++++++++ rust/crates/og-dev/src/fixture.rs | 649 ++++++++++++++++++ rust/crates/og-dev/src/lib.rs | 10 +- tests/parity/cases.tsv | 10 + .../past-the-end/stdout | 1 + .../inputs/dev-evidence-clashes/chrA.tsv.gz | Bin 0 -> 58 bytes .../inputs/dev-evidence-clashes/chrB.tsv.gz | Bin 0 -> 45 bytes .../inputs/dev-evidence-clashes/chrZ.tsv.gz | Bin 0 -> 45 bytes .../inputs/dev-evidence-clashes/notes.tsv.gz | Bin 0 -> 37 bytes .../inputs/dev-evidence-clashes/raw.vcf.gz | Bin 0 -> 192 bytes .../inputs/dev-evidence-clashes/split.vcf.gz | Bin 0 -> 192 bytes .../sites-past-end.tsv | 4 + .../inputs/dev-evidence-hg002-gvcf/sites.tsv | 19 + .../truth-past-end.bed | 1 + .../inputs/dev-evidence-hg002-gvcf/truth.bed | 4 + .../dev-evidence-hg002-gvcf/truth.vcf.gz | Bin 0 -> 173 bytes tests/parity/oracle.ref | 4 + 17 files changed, 1055 insertions(+), 1 deletion(-) create mode 100644 rust/crates/og-dev/src/evidence.rs create mode 100644 rust/crates/og-dev/src/fixture.rs create mode 100644 tests/parity/golden/dev-evidence-hg002-gvcf/past-the-end/stdout create mode 100644 tests/parity/inputs/dev-evidence-clashes/chrA.tsv.gz create mode 100644 tests/parity/inputs/dev-evidence-clashes/chrB.tsv.gz create mode 100644 tests/parity/inputs/dev-evidence-clashes/chrZ.tsv.gz create mode 100644 tests/parity/inputs/dev-evidence-clashes/notes.tsv.gz create mode 100644 tests/parity/inputs/dev-evidence-clashes/raw.vcf.gz create mode 100644 tests/parity/inputs/dev-evidence-clashes/split.vcf.gz create mode 100644 tests/parity/inputs/dev-evidence-hg002-gvcf/sites-past-end.tsv create mode 100644 tests/parity/inputs/dev-evidence-hg002-gvcf/sites.tsv create mode 100644 tests/parity/inputs/dev-evidence-hg002-gvcf/truth-past-end.bed create mode 100644 tests/parity/inputs/dev-evidence-hg002-gvcf/truth.bed create mode 100644 tests/parity/inputs/dev-evidence-hg002-gvcf/truth.vcf.gz diff --git a/rust/crates/og-dev/src/evidence.rs b/rust/crates/og-dev/src/evidence.rs new file mode 100644 index 000000000..c9b76d7fc --- /dev/null +++ b/rust/crates/og-dev/src/evidence.rs @@ -0,0 +1,354 @@ +//! `og dev evidence`: the measurements of `.github/workflows/public-evidence.yml` +//! on public data (GIAB's HG002), which were Python inside the workflow. +//! +//! - `og dev evidence clashes RAW.vcf.gz SPLIT.vcf.gz DATA_DIR OUT.tsv`: the +//! positions with two records or more, one of them an SNV, in the VCF as +//! GIAB writes it and split with `bcftools norm -m-`, on the chromosomes +//! of the archaic tables in DATA_DIR (`chr*.tsv.gz`), and how many rows of +//! those tables sit at such a position. OUT holds `raw_positions`, +//! `raw_on_table`, `split_positions` and `split_on_table`. +//! - `og dev evidence hg002-gvcf TRUTH.vcf.gz TRUTH.bed REF.fasta SITES.tsv OUT.vcf`: +//! a gVCF of GIAB's genotypes plus a one-base reference block at every +//! panel site inside the benchmark regions that no GIAB record covers, +//! with the reference base from the FASTA (through its `.fai`). The +//! output is unsorted: the workflow sorts it with bcftools. +//! +//! Both stream their VCFs. hg002-gvcf keeps, per chromosome, the merged +//! spans the GIAB records cover instead of every covered position, and +//! reads one reference base per site with a positioned read. +//! +//! Exit 0 when written, 1 when an input cannot be read or parsed, 2 on a +//! usage error. + +use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::fs::{self, File}; +use std::io::{BufRead, BufWriter, Write}; +use std::os::unix::fs::FileExt; +use std::path::Path; + +const USAGE: &str = "usage: og dev evidence clashes RAW.vcf.gz SPLIT.vcf.gz DATA_DIR OUT.tsv\n og dev evidence hg002-gvcf TRUTH.vcf.gz TRUTH.bed REF.fasta SITES.tsv OUT.vcf\n"; + +/// Runs `og dev evidence ARGS`; returns the exit code. +pub fn cli(args: &[String], out: &mut dyn Write, err: &mut dyn Write) -> u8 { + let usage = |err: &mut dyn Write, msg: &str| { + let _ = writeln!(err, "{USAGE}og dev evidence: error: {msg}"); + 2 + }; + let rest = args.get(1..).unwrap_or(&[]); + let result = match args.first().map(String::as_str) { + Some("clashes") if rest.len() == 4 => clashes(&rest[0], &rest[1], &rest[2], &rest[3]), + Some("hg002-gvcf") if rest.len() == 5 => { + hg002_gvcf(&rest[0], &rest[1], &rest[2], &rest[3], &rest[4]).map(|(records, blocks)| { + let _ = writeln!( + out, + "gVCF: {records} GIAB records, {blocks} reference blocks at panel sites" + ); + }) + } + Some("clashes" | "hg002-gvcf") => return usage(err, "wrong number of arguments"), + Some("-h" | "--help") => { + let _ = out.write_all(USAGE.as_bytes()); + return 0; + } + Some(other) => return usage(err, &format!("unknown evidence '{other}'")), + None => return usage(err, "the following arguments are required: evidence"), + }; + match result { + Ok(()) => 0, + Err(e) => { + let _ = writeln!(err, "og dev evidence: {e}"); + 1 + } + } +} + +fn open(path: &str) -> Result, String> { + og_io::open(path).map_err(|e| format!("{path}: {e}")) +} + +/// Each line of `path` without its line end, to `f`. +fn lines(path: &str, mut f: impl FnMut(&[u8]) -> Result<(), String>) -> Result<(), String> { + let mut r = open(path)?; + let mut buf = Vec::new(); + loop { + buf.clear(); + let n = r + .read_until(b'\n', &mut buf) + .map_err(|e| format!("{path}: {e}"))?; + if n == 0 { + return Ok(()); + } + let line = buf.strip_suffix(b"\n").unwrap_or(&buf); + f(line.strip_suffix(b"\r").unwrap_or(line))?; + } +} + +fn pos(field: &[u8], path: &str) -> Result { + std::str::from_utf8(field) + .ok() + .and_then(|s| s.trim().parse().ok()) + .ok_or_else(|| { + format!( + "{path}: position '{}' is not a number", + String::from_utf8_lossy(field) + ) + }) +} + +/// The positions of `path` on `chroms` with two records or more, one of +/// them an SNV. The VCF is sorted, so a position's records are adjacent. +fn clash_positions( + path: &str, + chroms: &BTreeSet>, +) -> Result, u64)>, String> { + let mut hit = BTreeSet::new(); + let (mut key, mut n, mut snvs): (Option<(Vec, u64)>, u32, u32) = (None, 0, 0); + lines(path, |line| { + if line.first() == Some(&b'#') { + return Ok(()); + } + let c: Vec<&[u8]> = line.splitn(6, |&b| b == b'\t').collect(); + if c.len() < 5 || !chroms.contains(c[0]) { + return Ok(()); + } + let k = (c[0].to_vec(), pos(c[1], path)?); + if key.as_ref() != Some(&k) { + key = Some(k.clone()); + (n, snvs) = (0, 0); + } + n += 1; + let one = |a: &[u8]| std::str::from_utf8(a).map_or(a.len(), |s| s.chars().count()) == 1; + snvs += u32::from(one(c[3]) && one(c[4])); + if n >= 2 && snvs >= 1 { + hit.insert(k); + } + Ok(()) + })?; + Ok(hit) +} + +fn clashes(raw: &str, split: &str, data: &str, out: &str) -> Result<(), String> { + let mut tables = BTreeMap::new(); + for e in fs::read_dir(data).map_err(|e| format!("{data}: {e}"))? { + let name = e + .map_err(|e| format!("{data}: {e}"))? + .file_name() + .into_string() + .unwrap_or_default(); + if let Some(chrom) = name + .strip_suffix(".tsv.gz") + .filter(|c| c.starts_with("chr")) + { + tables.insert(chrom.as_bytes().to_vec(), Path::new(data).join(&name)); + } + } + let chroms: BTreeSet> = tables.keys().cloned().collect(); + let sets = [ + ("raw", clash_positions(raw, &chroms)?), + ("split", clash_positions(split, &chroms)?), + ]; + let mut on_table = [0u64; 2]; + for (chrom, table) in &tables { + let want: Vec> = sets + .iter() + .map(|(_, s)| { + s.iter() + .filter(|(c, _)| c == chrom) + .map(|(_, p)| *p) + .collect() + }) + .collect(); + if want.iter().all(BTreeSet::is_empty) { + continue; + } + let path = table.to_string_lossy().into_owned(); + lines(&path, |line| { + let c: Vec<&[u8]> = line.splitn(3, |&b| b == b'\t').collect(); + if c[0] != chrom.as_slice() || c.len() < 2 { + return Ok(()); + } + let p = pos(c[1], &path)?; + for (k, w) in want.iter().enumerate() { + on_table[k] += u64::from(w.contains(&p)); + } + Ok(()) + })?; + } + let mut text = String::new(); + for (k, (name, s)) in sets.iter().enumerate() { + text += &format!( + "{name}_positions\t{}\n{name}_on_table\t{}\n", + s.len(), + on_table[k] + ); + } + fs::write(out, text).map_err(|e| format!("{out}: {e}")) +} + +/// One `.fai` row: length, byte offset, bases per line, bytes per line. +struct Fai { + len: u64, + offset: u64, + line_bases: u64, + line_bytes: u64, +} + +fn read_fai(path: &str) -> Result, Fai>, String> { + let mut fai = HashMap::new(); + lines(path, |line| { + let f: Vec<&[u8]> = line.split(|&b| b == b'\t').collect(); + let n = |i: usize| { + f.get(i) + .ok_or_else(|| format!("{path}: a row has fewer than 5 columns")) + .and_then(|x| pos(x, path)) + }; + let row = Fai { + len: n(1)?, + offset: n(2)?, + line_bases: n(3)?, + line_bytes: n(4)?, + }; + if row.line_bases == 0 { + return Err(format!("{path}: a row has 0 bases per line")); + } + fai.insert(f[0].to_vec(), row); + Ok(()) + })?; + Ok(fai) +} + +/// Sorted, merged half-open spans `[start, end)` of 1-based positions. +fn merge(mut spans: Vec<(u64, u64)>) -> Vec<(u64, u64)> { + spans.sort_unstable(); + let mut out: Vec<(u64, u64)> = Vec::with_capacity(spans.len()); + for (s, e) in spans { + match out.last_mut() { + Some(last) if s <= last.1 => last.1 = last.1.max(e), + _ => out.push((s, e)), + } + } + out +} + +/// Is `p` inside one of the sorted, disjoint spans? +fn inside(spans: &[(u64, u64)], p: u64) -> bool { + let i = spans.partition_point(|&(s, _)| s <= p); + i > 0 && p < spans[i - 1].1 +} + +fn hg002_gvcf( + truth: &str, + bed: &str, + reference: &str, + sites: &str, + out: &str, +) -> Result<(u64, u64), String> { + let fai = read_fai(&format!("{reference}.fai"))?; + let fasta = File::open(reference).map_err(|e| format!("{reference}: {e}"))?; + // The benchmark regions, BED 0-based: (start, end) per chromosome, + // sorted. A site p (1-based) is confident when the region with the + // largest start below p ends at p or later, as the Python's bisect did. + let mut conf: HashMap, Vec<(u64, u64)>> = HashMap::new(); + lines(bed, |line| { + let f: Vec<&[u8]> = line + .split(|b| b.is_ascii_whitespace()) + .filter(|x| !x.is_empty()) + .collect(); + if f.is_empty() { + return Ok(()); + } + if f.len() < 3 { + return Err(format!("{bed}: a row has fewer than 3 columns")); + } + conf.entry(f[0].to_vec()) + .or_default() + .push((pos(f[1], bed)?, pos(f[2], bed)?)); + Ok(()) + })?; + for v in conf.values_mut() { + v.sort_unstable(); + } + let confident = |c: &[u8], p: u64| { + conf.get(c).is_some_and(|v| { + let i = v.partition_point(|&(s, _)| s < p); + i > 0 && v[i - 1].1 >= p + }) + }; + + let file = File::create(out).map_err(|e| format!("{out}: {e}"))?; + let mut w = BufWriter::with_capacity(1 << 20, file); + let werr = |e: std::io::Error| format!("{out}: {e}"); + let mut covered: HashMap, Vec<(u64, u64)>> = HashMap::new(); + let mut records = 0u64; + lines(truth, |line| { + if line.starts_with(b"##") { + } else if line.first() == Some(&b'#') { + w.write_all(b"##INFO=\n##ALT=\n") + .map_err(werr)?; + } else { + let f: Vec<&[u8]> = line.splitn(6, |&b| b == b'\t').collect(); + if f.len() < 5 { + return Err(format!("{truth}: a record has fewer than 5 columns")); + } + let p = pos(f[1], truth)?; + covered + .entry(f[0].to_vec()) + .or_default() + .push((p, p + f[3].len() as u64)); + records += 1; + } + w.write_all(line) + .and_then(|()| w.write_all(b"\n")) + .map_err(werr) + })?; + let covered: HashMap, Vec<(u64, u64)>> = + covered.into_iter().map(|(c, v)| (c, merge(v))).collect(); + + let mut blocks = 0u64; + let mut first = true; + let mut base = [0u8; 1]; + lines(sites, |line| { + if std::mem::take(&mut first) { + return Ok(()); + } + let f: Vec<&[u8]> = line.splitn(4, |&b| b == b'\t').collect(); + if f.len() < 3 { + return Err(format!("{sites}: a row has fewer than 3 columns")); + } + let (c, p) = (f[1], pos(f[2], sites)?); + let Some(row) = fai.get(c) else { return Ok(()) }; + if p == 0 || p > row.len || covered.get(c).is_some_and(|v| inside(v, p)) || !confident(c, p) + { + return Ok(()); + } + let at = + row.offset + ((p - 1) / row.line_bases) * row.line_bytes + (p - 1) % row.line_bases; + fasta + .read_exact_at(&mut base, at) + .map_err(|e| format!("{reference}: {e}"))?; + let b = base[0].to_ascii_uppercase(); + if !b"ACGT".contains(&b) { + return Ok(()); + } + blocks += 1; + w.write_all(c) + .and_then(|()| writeln!(w, "\t{p}\t.\t{}\t<*>\t.\t.\tEND={p}\tGT\t0/0", b as char)) + .map_err(werr) + })?; + w.flush().map_err(werr)?; + Ok((records, blocks)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn spans_merge_and_answer_inside() { + let m = merge(vec![(10, 12), (5, 6), (11, 15), (15, 16)]); + assert_eq!(m, [(5, 6), (10, 16)]); + assert!( + inside(&m, 5) && !inside(&m, 6) && inside(&m, 15) && !inside(&m, 16) && !inside(&m, 4) + ); + } +} diff --git a/rust/crates/og-dev/src/fixture.rs b/rust/crates/og-dev/src/fixture.rs new file mode 100644 index 000000000..bb163c9f9 --- /dev/null +++ b/rust/crates/og-dev/src/fixture.rs @@ -0,0 +1,649 @@ +//! `og dev fixture`: writers of committed or generated test fixtures. +//! +//! - `og dev fixture comphet OUT_DIR [--swap]`: the compound-het phasing +//! fixture of `tests/e2e/comphet-phasing-1-reads.sh` (made-up reads on a +//! made-up contig). It replaces `tests/fixtures/comphet/make_fixture.py` +//! and writes the same bytes: the contig is Python's `random.Random(31)` +//! choosing among `ACGT`, so [`Mt19937`] is CPython's generator. +//! - `og dev fixture trim-pharmcat REPORT OUT [MAX_ANNOTATIONS]`: a PharmCAT +//! `report.json` cut down to the fields `og pgx` reads. It replaces +//! `tests/fixtures/pharmcat/trim_report.py` and writes the same bytes. +//! +//! Exit 0 when written, 1 when an input cannot be read or has the wrong +//! shape, 2 on a usage error. + +use crate::py_os_error; +use indexmap::IndexMap; +use pycompat::json::{self, Dump, Value}; +use std::fmt::Write as _; +use std::fs; +use std::io::Write; +use std::path::Path; + +const USAGE: &str = "usage: og dev fixture comphet OUT_DIR [--swap]\n og dev fixture trim-pharmcat REPORT.json OUT.json [MAX_ANNOTATIONS]\n"; + +/// Runs `og dev fixture ARGS`; returns the exit code. +pub fn cli(args: &[String], out: &mut dyn Write, err: &mut dyn Write) -> u8 { + let usage = |err: &mut dyn Write, msg: &str| { + let _ = writeln!(err, "{USAGE}og dev fixture: error: {msg}"); + 2 + }; + let result = match args.first().map(String::as_str) { + Some("comphet") => { + let rest: Vec<&String> = args[1..].iter().filter(|a| *a != "--swap").collect(); + if rest.len() != 1 { + return usage(err, "comphet takes one OUT_DIR and an optional --swap"); + } + comphet::write(Path::new(rest[0]), args[1..].iter().any(|a| a == "--swap")) + } + Some("trim-pharmcat") => { + let rest = &args[1..]; + if !(2..=3).contains(&rest.len()) { + return usage( + err, + "trim-pharmcat takes REPORT.json OUT.json [MAX_ANNOTATIONS]", + ); + } + let max = match rest.get(2).map(|m| m.trim().parse::()) { + None => 4, + Some(Ok(n)) => n, + Some(Err(_)) => { + return usage( + err, + &format!("MAX_ANNOTATIONS '{}' is not a whole number", rest[2]), + ); + } + }; + trim_pharmcat(Path::new(&rest[0]), Path::new(&rest[1]), max) + } + Some("-h" | "--help") => { + let _ = out.write_all(USAGE.as_bytes()); + return 0; + } + Some(other) => return usage(err, &format!("unknown fixture '{other}'")), + None => return usage(err, "the following arguments are required: fixture"), + }; + match result { + Ok(()) => 0, + Err(e) => { + let _ = writeln!(err, "og dev fixture: {e}"); + 1 + } + } +} + +/// CPython's Mersenne Twister (`random.Random`), only as far as +/// `Random(int).choice(seq)` needs it. +pub struct Mt19937 { + state: [u32; 624], + index: usize, +} + +impl Mt19937 { + /// `random.Random(seed)` for a seed below 2^32: `init_by_array([seed])`. + pub fn new(seed: u32) -> Self { + let mut mt = Mt19937 { + state: [0; 624], + index: 624, + }; + mt.init_genrand(19_650_218); + let key = [seed]; + let (mut i, mut j) = (1usize, 0usize); + for _ in 0..624.max(key.len()) { + let prev = mt.state[i - 1] ^ (mt.state[i - 1] >> 30); + mt.state[i] = (mt.state[i] ^ prev.wrapping_mul(1_664_525)) + .wrapping_add(key[j]) + .wrapping_add(j as u32); + i += 1; + j += 1; + if i >= 624 { + mt.state[0] = mt.state[623]; + i = 1; + } + if j >= key.len() { + j = 0; + } + } + for _ in 0..623 { + let prev = mt.state[i - 1] ^ (mt.state[i - 1] >> 30); + mt.state[i] = (mt.state[i] ^ prev.wrapping_mul(1_566_083_941)).wrapping_sub(i as u32); + i += 1; + if i >= 624 { + mt.state[0] = mt.state[623]; + i = 1; + } + } + mt.state[0] = 0x8000_0000; + mt + } + + fn init_genrand(&mut self, s: u32) { + self.state[0] = s; + for i in 1..624 { + let prev = self.state[i - 1] ^ (self.state[i - 1] >> 30); + self.state[i] = 1_812_433_253u32.wrapping_mul(prev).wrapping_add(i as u32); + } + self.index = 624; + } + + /// `genrand_uint32`. + pub fn next_u32(&mut self) -> u32 { + if self.index >= 624 { + for k in 0..624 { + let y = (self.state[k] & 0x8000_0000) | (self.state[(k + 1) % 624] & 0x7fff_ffff); + let mag = if y & 1 == 1 { 0x9908_b0df } else { 0 }; + self.state[k] = self.state[(k + 397) % 624] ^ (y >> 1) ^ mag; + } + self.index = 0; + } + let mut y = self.state[self.index]; + self.index += 1; + y ^= y >> 11; + y ^= (y << 7) & 0x9d2c_5680; + y ^= (y << 15) & 0xefc6_0000; + y ^ (y >> 18) + } + + /// `_randbelow(n)` for `0 < n < 2^32`: rejection sampling on + /// `getrandbits(n.bit_length())`. + pub fn below(&mut self, n: u32) -> u32 { + let k = 32 - n.leading_zeros(); + loop { + let r = self.next_u32() >> (32 - k); + if r < n { + return r; + } + } + } +} + +pub mod comphet { + //! The compound-het phasing fixture: sample CHPH on contig chrP (80 kb + //! of seeded random sequence), read pairs of 100 bp from 500 bp + //! fragments, and three pairs of heterozygous variants: + //! + //! - GENEC: SNV at 1000 and a 2-base deletion at 1003, both on copy 1 + //! (cis); single reads cover both. `--swap` moves the deletion to + //! copy 2 (trans). + //! - GENET: SNVs at 5000 and 5400, one on each copy (trans); only a read + //! and its mate cover both. + //! - GENEF: SNVs at 20000 and 70000, one on each copy, 50 kb apart: no + //! read pair covers both (unknown). + //! + //! Files: `reads.sam`, `vep.vcf` (with a VEP CSQ, for step 31) and + //! `comphet.vcf` (the pairs as slivar names them, for `og comphet + //! phase`). + + use super::Mt19937; + use std::fmt::Write as _; + use std::fs; + use std::path::Path; + + pub const SAMPLE: &str = "CHPH"; + pub const CONTIG: &str = "chrP"; + pub const LENGTH: usize = 80_000; + const READ: usize = 100; + const FRAG: usize = 500; + const FIELDS: &str = "Allele|Consequence|IMPACT|SYMBOL|Gene|Feature_type|Feature|BIOTYPE|CANONICAL|MANE_SELECT|Existing_variation|MAX_AF|gnomADe_AF|gnomADg_AF|CLIN_SIG"; + + /// One variant: 1-based position, alleles, consequence and the copy + /// (0 or 1) that carries it. + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct Variant { + pub gene: &'static str, + pub pos: usize, + pub alt_ref: String, + pub alt: String, + pub consequence: &'static str, + pub copy: u8, + } + + /// The contig: `random.Random(31)` choosing among `ACGT` 80,000 times. + pub fn reference() -> Vec { + let mut rng = Mt19937::new(31); + (0..LENGTH) + .map(|_| b"ACGT"[rng.below(4) as usize]) + .collect() + } + + /// The base before `b` in `ACGT`, wrapping (`A` gives `T`). + fn other(b: u8) -> u8 { + let i = b"ACGT" + .iter() + .position(|&x| x == b) + .expect("a base of ACGT"); + b"ACGT"[(i + 3) % 4] + } + + pub fn variants(seq: &[u8], swap: bool) -> Vec { + let s = |a: usize, b: usize| String::from_utf8_lossy(&seq[a..b]).into_owned(); + let snv = |gene, pos: usize, consequence, copy| Variant { + gene, + pos, + alt_ref: s(pos - 1, pos), + alt: (other(seq[pos - 1]) as char).to_string(), + consequence, + copy, + }; + vec![ + snv("GENEC", 1000, "stop_gained", 0), + Variant { + gene: "GENEC", + pos: 1003, + alt_ref: s(1002, 1005), + alt: s(1002, 1003), + consequence: "frameshift_variant", + copy: u8::from(swap), + }, + snv("GENET", 5000, "stop_gained", 0), + snv("GENET", 5400, "stop_gained", 1), + snv("GENEF", 20000, "stop_gained", 0), + snv("GENEF", 70000, "stop_gained", 1), + ] + } + + /// The sequence and CIGAR of a read of `READ` bases from 0-based + /// `start` on one copy. + fn read_on(seq: &[u8], vs: &[Variant], copy: u8, start: usize) -> (String, String) { + let mine = || vs.iter().filter(|v| v.copy == copy); + let snv_at = |p: usize| { + mine() + .find(|v| v.alt_ref.len() == v.alt.len() && v.pos - 1 == p) + .map(|v| v.alt.as_bytes()[0]) + }; + // A deletion is keyed on the first deleted base. + let del_at = |p: usize| { + mine() + .find(|v| v.alt_ref.len() > v.alt.len() && v.pos + v.alt.len() - 1 == p) + .map(|v| v.alt_ref.len() - v.alt.len()) + }; + let (mut out, mut ops, mut p) = + (Vec::with_capacity(READ), Vec::<(u8, usize)>::new(), start); + while out.len() < READ { + if let Some(n) = del_at(p) { + ops.push((b'D', n)); + p += n; + continue; + } + out.push(snv_at(p).unwrap_or(seq[p])); + ops.push((b'M', 1)); + p += 1; + } + let mut cigar = String::new(); + let mut i = 0; + while i < ops.len() { + let (op, mut n) = ops[i]; + let mut j = i + 1; + while j < ops.len() && ops[j].0 == op { + n += ops[j].1; + j += 1; + } + let _ = write!(cigar, "{n}{}", op as char); + i = j; + } + (String::from_utf8_lossy(&out).into_owned(), cigar) + } + + pub fn sam(seq: &[u8], vs: &[Variant]) -> String { + let mut s = format!( + "@HD\tVN:1.6\tSO:unsorted\n@SQ\tSN:{CONTIG}\tLN:{LENGTH}\n@RG\tID:{SAMPLE}\tSM:{SAMPLE}\n" + ); + // Fragment starts (0-based) around each pair: read 1 covers the + // first position, its mate the second one when 400 bp apart. + let starts: [(&str, std::iter::StepBy>); 4] = [ + ("c", (920..991).step_by(7)), + ("t", (4905..4996).step_by(10)), + ("f", (19905..19996).step_by(10)), + ("g", (69905..69996).step_by(10)), + ]; + let q = "I".repeat(READ); + for (region, rs) in starts { + for copy in 0..2u8 { + for st in rs.clone() { + let m = st + FRAG - READ; + let (s1, c1) = read_on(seq, vs, copy, st); + let (s2, c2) = read_on(seq, vs, copy, m); + let name = format!("{region}{copy}_{st}"); + let _ = writeln!( + s, + "{name}\t99\t{CONTIG}\t{}\t60\t{c1}\t=\t{}\t{FRAG}\t{s1}\t{q}\tRG:Z:{SAMPLE}", + st + 1, + m + 1 + ); + let _ = writeln!( + s, + "{name}\t147\t{CONTIG}\t{}\t60\t{c2}\t=\t{}\t-{FRAG}\t{s2}\t{q}\tRG:Z:{SAMPLE}", + m + 1, + st + 1 + ); + } + } + } + s + } + + fn vcf_head(info: &str) -> String { + format!( + "##fileformat=VCFv4.2\n##contig=\n##FILTER=\n{info}\ + ##FORMAT=\n\ + ##FORMAT=\n\ + ##FORMAT=\n\ + ##FORMAT=\n\ + #CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\tFORMAT\t{SAMPLE}\n" + ) + } + + pub fn vep_vcf(vs: &[Variant]) -> String { + let mut out = vcf_head(&format!( + "##INFO=\n" + )); + for v in vs { + let (g, p) = (v.gene, v.pos); + let _ = writeln!( + out, + "{CONTIG}\t{p}\t.\t{}\t{}\t50\tPASS\tCSQ={}|{}|HIGH|{g}|ENSG{g}|Transcript|ENST{p}|protein_coding|YES|NM_{p}.1|rs{p}|0.0001|0.0001|0.0001|\tGT:GQ:DP:AD\t0/1:50:30:15,15", + v.alt_ref, v.alt, v.alt, v.consequence + ); + } + out + } + + pub fn comphet_vcf(vs: &[Variant]) -> String { + let mut out = vcf_head( + "##INFO=\n", + ); + for (i, v) in vs.iter().enumerate() { + let mate = if i % 2 == 0 { &vs[i + 1] } else { &vs[i - 1] }; + let pid = match v.gene { + "GENEC" => 1, + "GENET" => 2, + _ => 3, + }; + let _ = writeln!( + out, + "{CONTIG}\t{}\t.\t{}\t{}\t50\tPASS\tslivar_comphet={SAMPLE}/{}/{pid}/{CONTIG}/{}/{}/{}\tGT:GQ:DP:AD\t0/1:50:30:15,15", + v.pos, v.alt_ref, v.alt, v.gene, mate.pos, mate.alt_ref, mate.alt + ); + } + out + } + + /// Writes the three files into `dir`, made if missing. + pub fn write(dir: &Path, swap: bool) -> Result<(), String> { + let seq = reference(); + let vs = variants(&seq, swap); + fs::create_dir_all(dir).map_err(|e| format!("{}: {e}", dir.display()))?; + for (name, text) in [ + ("reads.sam", sam(&seq, &vs)), + ("vep.vcf", vep_vcf(&vs)), + ("comphet.vcf", comphet_vcf(&vs)), + ] { + let path = dir.join(name); + fs::write(&path, text).map_err(|e| format!("{}: {e}", path.display()))?; + } + Ok(()) + } +} + +/// `x.get(k)`: the value, or JSON null. +fn get(x: &IndexMap, k: &str) -> Value { + x.get(k).cloned().unwrap_or(Value::Null) +} + +/// Python's truth of a JSON value. +fn truthy(v: &Value) -> bool { + pycompat::value::truthy(v) +} + +/// `v or []` as a list; a truthy value that is not a list is an error. +fn list<'a>(v: Option<&'a Value>, what: &str) -> Result<&'a [Value], String> { + match v { + Some(Value::Array(a)) => Ok(a), + Some(x) if truthy(x) => Err(format!("{what} is not a list")), + _ => Ok(&[]), + } +} + +fn object<'a>(v: &'a Value, what: &str) -> Result<&'a IndexMap, String> { + match v { + Value::Object(o) => Ok(o), + _ => Err(format!("{what} is not an object")), + } +} + +/// The keys of `x` among `keys`, in that order. +fn pick(x: &IndexMap, keys: &[&str]) -> IndexMap { + keys.iter() + .filter_map(|k| x.get(*k).map(|v| ((*k).to_owned(), v.clone()))) + .collect() +} + +/// One listed diplotype with the fields `og pgx` reads. +fn diplotype(x: &Value) -> Result { + let Value::Object(x) = x else { + return Ok(x.clone()); + }; + let mut out = pick( + x, + &[ + "gene", + "label", + "phenotypes", + "activityScore", + "outsidePhenotype", + ], + ); + for a in ["allele1", "allele2"] { + let Some(v) = x.get(a) else { continue }; + let kept = match v { + Value::Object(o) if !o.is_empty() => { + Value::Object(pick(o, &["gene", "name", "function"])) + } + v if !truthy(v) => v.clone(), + _ => return Err(format!("diplotype {a} is not an object")), + }; + out.insert(a.to_owned(), kept); + } + Ok(Value::Object(out)) +} + +/// `items[:n]` with Python's slice rule for a negative `n`. +fn head(items: &[Value], n: i64) -> &[Value] { + let len = items.len() as i64; + let end = if n < 0 { (len + n).max(0) } else { n.min(len) }; + &items[..end as usize] +} + +/// The trimmed report: every gene with its listed diplotypes and related +/// drugs, and for each drug of each guidance source up to `max` of the +/// annotations PharmCAT matched, with only the fields `og pgx` reads. +pub fn trim(d: &Value, max: i64) -> Result { + let d = object(d, "the report")?; + let mut genes = IndexMap::new(); + for (g, v) in object(d.get("genes").unwrap_or(&Value::Null), "genes")? { + let v = object(v, &format!("gene {g}"))?; + let mut gene = pick(v, &["geneSymbol", "chr", "phased", "callSource"]); + let mut related = Vec::new(); + for r in list(v.get("relatedDrugs"), "relatedDrugs")? { + let r = object(r, "a related drug")?; + let name = r.get("name").ok_or("a related drug has no name")?.clone(); + related.push(Value::Object(IndexMap::from([ + ("name".to_owned(), name), + ("id".to_owned(), get(r, "id")), + ]))); + } + gene.insert("relatedDrugs".to_owned(), Value::Array(related)); + // sourceDiplotypes is what og pgx reads; recommendationDiplotypes + // only when sourceDiplotypes is absent, so it is kept only then. + let k = if v.get("sourceDiplotypes").is_some_and(truthy) { + "sourceDiplotypes" + } else { + "recommendationDiplotypes" + }; + if v.contains_key(k) { + let dips = list(v.get(k), k)? + .iter() + .map(diplotype) + .collect::>()?; + gene.insert(k.to_owned(), Value::Array(dips)); + } + genes.insert(g.clone(), Value::Object(gene)); + } + let mut drugs: IndexMap = IndexMap::new(); + for (source, by) in object(d.get("drugs").unwrap_or(&Value::Null), "drugs")? { + for (name, rep) in object(by, &format!("drugs of {source}"))? { + let rep = object(rep, &format!("drug {name}"))?; + let mut gls = Vec::new(); + for gl in list(rep.get("guidelines"), "guidelines")? { + let gl = object(gl, "a guideline")?; + let mut anns = Vec::new(); + for a in head(list(gl.get("annotations"), "annotations")?, max) { + let a = object(a, "an annotation")?; + let mut gts = Vec::new(); + for gt in list(a.get("genotypes"), "genotypes")? { + let gt = object(gt, "a genotype")?; + let dips = list(gt.get("diplotypes"), "diplotypes")? + .iter() + .filter_map(|x| match x { + Value::Object(x) => Some(Value::Object(IndexMap::from([ + ("gene".to_owned(), get(x, "gene")), + ("label".to_owned(), get(x, "label")), + ]))), + _ => None, + }) + .collect(); + gts.push(Value::Object(IndexMap::from([( + "diplotypes".to_owned(), + Value::Array(dips), + )]))); + } + anns.push(Value::Object(IndexMap::from([ + ("phenotypes".to_owned(), get(a, "phenotypes")), + ("classification".to_owned(), get(a, "classification")), + ( + "drugRecommendation".to_owned(), + get(a, "drugRecommendation"), + ), + ("genotypes".to_owned(), Value::Array(gts)), + ]))); + } + if !anns.is_empty() { + gls.push(Value::Object(IndexMap::from([ + ("name".to_owned(), get(gl, "name")), + ("source".to_owned(), get(gl, "source")), + ("annotations".to_owned(), Value::Array(anns)), + ]))); + } + } + if !gls.is_empty() { + let entry = drugs + .entry(source.clone()) + .or_insert_with(|| Value::Object(IndexMap::new())); + if let Value::Object(e) = entry { + e.insert( + name.clone(), + Value::Object(IndexMap::from([ + ("name".to_owned(), get(rep, "name")), + ("id".to_owned(), get(rep, "id")), + ("guidelines".to_owned(), Value::Array(gls)), + ])), + ); + } + } + } + } + let mut out = pick(d, &["title", "timestamp", "pharmcatVersion", "dataVersion"]); + out.insert("genes".to_owned(), Value::Object(genes)); + out.insert("drugs".to_owned(), Value::Object(drugs)); + let mut note = String::new(); + let _ = write!( + note, + "Trimmed by og dev fixture trim-pharmcat: every gene, and up to {max} matched annotations per drug guideline, with only the fields og pgx reads." + ); + out.insert("_fixture_note".to_owned(), Value::Str(note)); + Ok(Value::Object(out)) +} + +fn trim_pharmcat(src: &Path, dst: &Path, max: i64) -> Result<(), String> { + let text = fs::read(src).map_err(|e| py_os_error(&e, &src.to_string_lossy()))?; + let text = String::from_utf8(text).map_err(|_| format!("{}: not UTF-8", src.display()))?; + let d = json::loads(&text).map_err(|e| format!("{}: {e}", src.display()))?; + let out = trim(&d, max)?; + let opts = Dump { + separators: Some((",", ":")), + ..Dump::default() + }; + let text = json::dumps(&out, &opts).map_err(|e| e.to_string())?; + fs::write(dst, text).map_err(|e| py_os_error(&e, &dst.to_string_lossy())) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn mt19937_matches_cpython() { + // random.Random(31): [r.getrandbits(32) for _ in range(3)] and the + // first ten of "".join(r.choice("ACGT") ...) from a fresh seed. + let mut r = Mt19937::new(31); + let first: Vec = (0..3).map(|_| r.next_u32()).collect(); + assert_eq!(first, [52_734_659, 2_016_822_975, 482_749_413]); + assert_eq!(&comphet::reference()[..10], b"ATATCACACC"); + } + + #[test] + fn swap_moves_only_the_deletion() { + let seq = comphet::reference(); + let (a, b) = ( + comphet::variants(&seq, false), + comphet::variants(&seq, true), + ); + assert_eq!(a[1].copy, 0); + assert_eq!(b[1].copy, 1); + assert_eq!(a[0], b[0]); + assert_eq!(a[2..], b[2..]); + } + + #[test] + fn a_read_over_the_deletion_has_a_d_in_its_cigar() { + let seq = comphet::reference(); + let vs = comphet::variants(&seq, false); + let sam = comphet::sam(&seq, &vs); + let line = sam + .lines() + .find(|l| l.starts_with("c0_920\t99")) + .expect("read c0_920"); + assert!(line.contains("\t83M2D17M\t"), "{line}"); + let line = sam + .lines() + .find(|l| l.starts_with("c1_920\t99")) + .expect("read c1_920"); + assert!(line.contains("\t100M\t"), "{line}"); + } + + #[test] + fn trim_keeps_the_fields_og_pgx_reads() { + let d = json::loads( + r#"{"title":"t","extra":1,"genes":{"G":{"geneSymbol":"G","x":1,"relatedDrugs":[{"name":"d"}], + "sourceDiplotypes":[],"recommendationDiplotypes":[{"label":"*1/*2","allele1":{"name":"*1","y":2},"allele2":null,"z":3},"str"]}}, + "drugs":{"CPIC":{"d":{"name":"d","guidelines":[{"name":"g","annotations":[{"genotypes":[{"diplotypes":[{"gene":"G"},5]}]},{},{}]},{"annotations":[]}]}}}}"#, + ) + .expect("json"); + let out = trim(&d, 2).expect("trim"); + let text = json::dumps( + &out, + &Dump { + separators: Some((",", ":")), + ..Dump::default() + }, + ) + .expect("dumps"); + assert!(text.starts_with(r#"{"title":"t","genes":{"G":{"geneSymbol":"G","relatedDrugs":[{"name":"d","id":null}],"recommendationDiplotypes":[{"label":"*1/*2","allele1":{"name":"*1"},"allele2":null},"str"]}}"#), "{text}"); + assert!(text.contains(r#""annotations":[{"phenotypes":null,"classification":null,"drugRecommendation":null,"genotypes":[{"diplotypes":[{"gene":"G","label":null}]}]},{"phenotypes":null"#), "{text}"); + assert_eq!(text.matches("drugRecommendation").count(), 2, "{text}"); + // A negative count is Python's slice: all but the last. + let out = trim(&d, -2).expect("trim"); + let text = json::dumps(&out, &Dump::default()).expect("dumps"); + assert_eq!(text.matches("drugRecommendation").count(), 1, "{text}"); + } +} diff --git a/rust/crates/og-dev/src/lib.rs b/rust/crates/og-dev/src/lib.rs index 55c93fdf0..918237714 100644 --- a/rust/crates/og-dev/src/lib.rs +++ b/rust/crates/og-dev/src/lib.rs @@ -12,8 +12,14 @@ //! the report carries the report-only error strategy. //! - `og dev remnants` ([`remnants`]): does each Python file kept only for //! its importers (`tests/parity/remnants.tsv`) hold nothing else? +//! - `og dev fixture` ([`fixture`]): writers of test fixtures (the +//! compound-het reads, a trimmed PharmCAT report). +//! - `og dev evidence` ([`evidence`]): the measurements of the public +//! evidence workflow on GIAB's HG002. pub mod contract; +pub mod evidence; +pub mod fixture; pub mod parity; pub mod remnants; pub mod report_only; @@ -22,7 +28,7 @@ mod zip; use std::io::Write; -const USAGE: &str = "usage: og dev parity-compare KIND ORACLE NEW\n og dev parity-compare --self-test\n og dev contract --static [--repo DIR] [--contract FILE]\n og dev contract --outputs DIR --leg bash|nextflow --sample S [--observe]\n og dev schema SCHEMA.json DOC.json\n og dev report-only [--repo DIR]\n og dev remnants [--list FILE] [--root DIR]\n"; +const USAGE: &str = "usage: og dev parity-compare KIND ORACLE NEW\n og dev parity-compare --self-test\n og dev contract --static [--repo DIR] [--contract FILE]\n og dev contract --outputs DIR --leg bash|nextflow --sample S [--observe]\n og dev schema SCHEMA.json DOC.json\n og dev report-only [--repo DIR]\n og dev remnants [--list FILE] [--root DIR]\n og dev fixture comphet|trim-pharmcat [args]\n og dev evidence clashes|hg002-gvcf [args]\n"; /// Python's `str(OSError)`: `[Errno 2] No such file or directory: 'path'`. pub(crate) fn py_os_error(e: &std::io::Error, path: &str) -> String { @@ -57,6 +63,8 @@ pub fn main(args: &[String]) -> u8 { Some("contract") => run(contract::cli, &args[1..]), Some("schema") => run(schema::cli, &args[1..]), Some("report-only") => run(report_only::cli, &args[1..]), + Some("fixture") => run(fixture::cli, &args[1..]), + Some("evidence") => run(evidence::cli, &args[1..]), Some("-h" | "--help") => { print!("{USAGE}"); 0 diff --git a/tests/parity/cases.tsv b/tests/parity/cases.tsv index 795bb7945..692b0f540 100644 --- a/tests/parity/cases.tsv +++ b/tests/parity/cases.tsv @@ -874,3 +874,13 @@ family-ibd mapbp gen:ibd-oracle.sh,gen:ibd-cases.sh --pedigree mapbp/pedigree.ts family-ibd nomap gen:ibd-oracle.sh,gen:ibd-cases.sh --pedigree nomap/pedigree.tsv --chroms nomap/chroms.txt --maps nomap/maps --phased nomap/phased --ibd nomap/ibd --out-dir out - content PYTHON_IMAGE family-ibd short gen:ibd-oracle.sh,gen:ibd-cases.sh --pedigree short/pedigree.tsv --chroms short/chroms.txt --maps short/maps --phased short/phased --ibd short/ibd --out-dir out - content PYTHON_IMAGE family-ibd twice gen:ibd-oracle.sh,gen:ibd-cases.sh --pedigree twice/pedigree.tsv --chroms twice/chroms.txt --maps twice/maps --phased twice/phased --ibd twice/ibd --out-dir out - content PYTHON_IMAGE +dev-fixture-comphet plain - out out/reads.sam,out/vep.vcf,out/comphet.vcf bytes PYTHON_IMAGE +dev-fixture-comphet swap - out --swap out/reads.sam,out/vep.vcf,out/comphet.vcf bytes PYTHON_IMAGE +dev-fixture-trim-pharmcat report-3.2.0 gen:pgx-fixtures.sh report-3.2.0.json out.json out.json json:mask=_fixture_note PYTHON_IMAGE +dev-fixture-trim-pharmcat report-3.4.0 gen:pgx-fixtures.sh report-3.4.0.json out.json out.json json:mask=_fixture_note PYTHON_IMAGE +dev-fixture-trim-pharmcat pharmcat-docs-example gen:pgx-fixtures.sh pharmcat-docs-example.json out.json out.json json:mask=_fixture_note PYTHON_IMAGE +dev-fixture-trim-pharmcat one-annotation gen:pgx-fixtures.sh report-3.4.0.json out.json 1 out.json json:mask=_fixture_note PYTHON_IMAGE +dev-fixture-trim-pharmcat all-but-last gen:pgx-fixtures.sh pharmcat-docs-example.json out.json -1 out.json json:mask=_fixture_note PYTHON_IMAGE +dev-evidence-clashes synthetic dev-evidence-clashes/raw.vcf.gz,dev-evidence-clashes/split.vcf.gz,dev-evidence-clashes/chrA.tsv.gz,dev-evidence-clashes/chrB.tsv.gz,dev-evidence-clashes/chrZ.tsv.gz,dev-evidence-clashes/notes.tsv.gz raw.vcf.gz split.vcf.gz . affected.tsv affected.tsv bytes PYTHON_IMAGE +dev-evidence-hg002-gvcf synthetic dev-evidence-hg002-gvcf/truth.vcf.gz,dev-evidence-hg002-gvcf/truth.bed,dev-evidence-hg002-gvcf/ref.fasta,dev-evidence-hg002-gvcf/ref.fasta.fai,dev-evidence-hg002-gvcf/sites.tsv truth.vcf.gz truth.bed ref.fasta sites.tsv out.vcf out.vcf bytes PYTHON_IMAGE +dev-evidence-hg002-gvcf past-the-end dev-evidence-hg002-gvcf/truth.vcf.gz,dev-evidence-hg002-gvcf/truth-past-end.bed,dev-evidence-hg002-gvcf/ref.fasta,dev-evidence-hg002-gvcf/ref.fasta.fai,dev-evidence-hg002-gvcf/sites-past-end.tsv truth.vcf.gz truth-past-end.bed ref.fasta sites-past-end.tsv out.vcf out.vcf bytes PYTHON_IMAGE diff --git a/tests/parity/golden/dev-evidence-hg002-gvcf/past-the-end/stdout b/tests/parity/golden/dev-evidence-hg002-gvcf/past-the-end/stdout new file mode 100644 index 000000000..233285d28 --- /dev/null +++ b/tests/parity/golden/dev-evidence-hg002-gvcf/past-the-end/stdout @@ -0,0 +1 @@ +gVCF: 4 GIAB records, 1 reference blocks at panel sites diff --git a/tests/parity/inputs/dev-evidence-clashes/chrA.tsv.gz b/tests/parity/inputs/dev-evidence-clashes/chrA.tsv.gz new file mode 100644 index 0000000000000000000000000000000000000000..5dc766e8d450338fc6372800f67e9c3fc0ecd581 GIT binary patch literal 58 zcmb2|=3oE;rvKjOG)|p=s&hjBS;-kq&ofWF&S?}qF?nXvV92A$Q#_-CwI?x!VG1)t Lh}*wOc0e5fk|7n8 literal 0 HcmV?d00001 diff --git a/tests/parity/inputs/dev-evidence-clashes/chrB.tsv.gz b/tests/parity/inputs/dev-evidence-clashes/chrB.tsv.gz new file mode 100644 index 0000000000000000000000000000000000000000..2ff2824f7570b5fb663c33b4e8d667bcf2f245de GIT binary patch literal 45 ycmb2|=3oE;rvKjOG)|p=s&hjBS;-kq&ofWF&S?}qGI?t9fQg}K^O3odKt%wPJ`z9x literal 0 HcmV?d00001 diff --git a/tests/parity/inputs/dev-evidence-clashes/chrZ.tsv.gz b/tests/parity/inputs/dev-evidence-clashes/chrZ.tsv.gz new file mode 100644 index 0000000000000000000000000000000000000000..987e91c2ce6814ccb80b83d2cf042cfeb10c57d8 GIT binary patch literal 45 ycmb2|=3oE;rvKjOG)|p=s&hjBS;-kq&ofWF&S`W#GI?V1fQjMq66L>=Kt%wRY7%Gw literal 0 HcmV?d00001 diff --git a/tests/parity/inputs/dev-evidence-clashes/notes.tsv.gz b/tests/parity/inputs/dev-evidence-clashes/notes.tsv.gz new file mode 100644 index 0000000000000000000000000000000000000000..035133bbf944bc169e37959bc9f666d9342e05d7 GIT binary patch literal 37 qcmb2|=3oE;rvKjOG)|p=s&hjBnb$duqGu*gm>9BVi~JA-N&*1&xD8MM literal 0 HcmV?d00001 diff --git a/tests/parity/inputs/dev-evidence-clashes/raw.vcf.gz b/tests/parity/inputs/dev-evidence-clashes/raw.vcf.gz new file mode 100644 index 0000000000000000000000000000000000000000..386cc04141ddaee0666a6957d26e90295f313b42 GIT binary patch literal 192 zcmV;x06+g9iwFP!00002|9#Ct3xYrp1>p1RZ+Hz`E$NcMw7Dv1Te@`*Q4`8Qq~D(# z1wm`aneV+B%c`1dS>3zax%W?8JQlV)v8>|W_D$`t;;mm?I+|X~w(c*!$JRf%`QTh5 zk20BJFLT6OWPt!B3Wp;l5OJb{4Dn7#2$`j%5QU7n6a4^l*a)HEG3+VN^`yXN&X>{< uF!OSl34A(MciFJuqxCnFkvH)4b@iWhz{DKRi^IQa=sp1(Jr?Yk0RR9Ton2f2 literal 0 HcmV?d00001 diff --git a/tests/parity/inputs/dev-evidence-clashes/split.vcf.gz b/tests/parity/inputs/dev-evidence-clashes/split.vcf.gz new file mode 100644 index 0000000000000000000000000000000000000000..4a4e80c1e428b4fbd7e40229992c3d58421c08b6 GIT binary patch literal 192 zcmV;x06+g9iwFP!00002|9#EP3W7is1>p7ZHQa`cnzYGa932%jOQ-fB{(&io^!zzd z5Tq{e{mwmCRm-L=%6@p>=x@R19Qxvzu8~ancEjXPPgc^0RRAO6I%5E literal 0 HcmV?d00001 diff --git a/tests/parity/inputs/dev-evidence-hg002-gvcf/sites-past-end.tsv b/tests/parity/inputs/dev-evidence-hg002-gvcf/sites-past-end.tsv new file mode 100644 index 000000000..bb13404a5 --- /dev/null +++ b/tests/parity/inputs/dev-evidence-hg002-gvcf/sites-past-end.tsv @@ -0,0 +1,4 @@ +id chrom pos +s1 chrB 30 +s2 chrB 31 +s3 chrB 35 diff --git a/tests/parity/inputs/dev-evidence-hg002-gvcf/sites.tsv b/tests/parity/inputs/dev-evidence-hg002-gvcf/sites.tsv new file mode 100644 index 000000000..b6a517815 --- /dev/null +++ b/tests/parity/inputs/dev-evidence-hg002-gvcf/sites.tsv @@ -0,0 +1,19 @@ +id chrom pos ref +s0 chrA 3 X +s1 chrA 11 X +s2 chrA 12 X +s3 chrA 13 X +s4 chrA 15 X +s5 chrA 16 X +s6 chrA 19 X +s7 chrA 21 X +s8 chrA 27 X +s9 chrA 45 X +s10 chrA 46 X +s11 chrB 3 X +s12 chrB 8 X +s13 chrB 10 X +s14 chrB 30 X +s15 chrC 1 X +s16 chrD 4 X +s17 chrA 1 X diff --git a/tests/parity/inputs/dev-evidence-hg002-gvcf/truth-past-end.bed b/tests/parity/inputs/dev-evidence-hg002-gvcf/truth-past-end.bed new file mode 100644 index 000000000..d6806d6ff --- /dev/null +++ b/tests/parity/inputs/dev-evidence-hg002-gvcf/truth-past-end.bed @@ -0,0 +1 @@ +chrB 0 40 diff --git a/tests/parity/inputs/dev-evidence-hg002-gvcf/truth.bed b/tests/parity/inputs/dev-evidence-hg002-gvcf/truth.bed new file mode 100644 index 000000000..6a2ed2e11 --- /dev/null +++ b/tests/parity/inputs/dev-evidence-hg002-gvcf/truth.bed @@ -0,0 +1,4 @@ +chrA 18 45 +chrA 0 15 +chrB 0 30 +chrC 0 10 diff --git a/tests/parity/inputs/dev-evidence-hg002-gvcf/truth.vcf.gz b/tests/parity/inputs/dev-evidence-hg002-gvcf/truth.vcf.gz new file mode 100644 index 0000000000000000000000000000000000000000..f13c00115d4f489b2140634ee513945ddb31d804 GIT binary patch literal 173 zcmV;e08;-SiwFP!00002|8>pF3W7iw1>p7ZHD((&YC?-97)M71O&h27A>P0gM0);| zXk{(W`Th$bb=Oz*I6WUTy9@a`n6^PkISzBzuv6@_vYq&@uZCuBnM=K{r~^~M%hj3 literal 0 HcmV?d00001 diff --git a/tests/parity/oracle.ref b/tests/parity/oracle.ref index 226fc54b2..ca861a0ed 100644 --- a/tests/parity/oracle.ref +++ b/tests/parity/oracle.ref @@ -58,3 +58,7 @@ mito-copies 954ab367004d63ea639521982299b4976927ccfd file:bin/collect_summary.py lookup 0baddb45aa3871a57469cf20295bdd832f72f5c0 file:bin/genome_lookup.py python3 lookup lookup-query 0baddb45aa3871a57469cf20295bdd832f72f5c0 file:bin/genome_lookup.py bash ./oracle.sh lookup query family-ibd 0baddb45aa3871a57469cf20295bdd832f72f5c0 file:bin/ibd_report.py sh ./ibd_oracle.sh family ibd +dev-fixture-comphet fe6b67e09cef81c396c2277472df10407c56d7ac file:tests/fixtures/comphet/make_fixture.py python3 dev fixture comphet +dev-fixture-trim-pharmcat fe6b67e09cef81c396c2277472df10407c56d7ac file:tests/fixtures/pharmcat/trim_report.py python3 dev fixture trim-pharmcat +dev-evidence-clashes fe6b67e09cef81c396c2277472df10407c56d7ac file:.github/workflows/public-evidence.yml sh -c 'd=$(mktemp -d) && awk -v n=1 "/<<.PY.\$/ {k++; if (k == n) {on = 1; next}} on && /^ *PY\$/ {exit} on {sub(/^ /, \"\"); print}" "$0" > "$d/p.py" && exec python3 -I "$d/p.py" "$@"' dev evidence clashes +dev-evidence-hg002-gvcf fe6b67e09cef81c396c2277472df10407c56d7ac file:.github/workflows/public-evidence.yml sh -c 'd=$(mktemp -d) && awk -v n=2 "/<<.PY.\$/ {k++; if (k == n) {on = 1; next}} on && /^ *PY\$/ {exit} on {sub(/^ /, \"\"); print}" "$0" > "$d/p.py" && exec python3 -I "$d/p.py" "$@"' dev evidence hg002-gvcf From 2d60f679993627e65726faaf627a7c8d7d120bc1 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 11:11:50 +0100 Subject: [PATCH 03/45] refactor(ci): check the Renovate dry run with og dev renovate-check The 210-line checker the workflow wrote into a file is now an og dev verb; its pins table is data in scripts/ci/renovate-pins.tsv, where a missing file or a glob that matches nothing is an error. On the last saved log of main it prints the same lines as the Python, clean and with planted pins. Bead: pgp-bs9u.39 --- .github/workflows/renovate-dry-run.yml | 237 +------ rust/crates/og-dev/src/lib.rs | 6 +- rust/crates/og-dev/src/renovate.rs | 847 +++++++++++++++++++++++++ scripts/ci/renovate-pins.tsv | 25 + 4 files changed, 894 insertions(+), 221 deletions(-) create mode 100644 rust/crates/og-dev/src/renovate.rs create mode 100644 scripts/ci/renovate-pins.tsv diff --git a/.github/workflows/renovate-dry-run.yml b/.github/workflows/renovate-dry-run.yml index 5722cb025..155568c19 100644 --- a/.github/workflows/renovate-dry-run.yml +++ b/.github/workflows/renovate-dry-run.yml @@ -6,7 +6,8 @@ name: Renovate dry run # matcher that misses an image, or a versioning that cannot read a tag, shows # up here first. The full debug log is kept as an artifact for 14 days. # -# The checks (each one has a planted fault in the self-test step): +# The checks (og dev renovate-check; each one has a planted fault in its +# self-test, which runs after the checks): # count every *_IMAGE line of versions.env is a detected docker dependency # names no package name carries a ':' or '@' (a digest line once became # a package named '...@sha256') @@ -16,9 +17,10 @@ name: Renovate dry run # one-pr a tool gets the same proposal in every file it is in # pins every non-image pin (Nextflow, actionlint, gitleaks, ShellCheck, # the Renovate CLI, the HLA database, pgsc_calc, the demo's Python -# packages, Aldy, the docs workflow's Python) is detected in every -# place it is pinned, as often as the text has them. A setup-nextflow -# step counts as a pin only when its version: line is a literal +# packages, Aldy, the docs workflow's Python; one row each in +# scripts/ci/renovate-pins.tsv) is detected in every place it is +# pinned, as often as the text has them. A setup-nextflow step +# counts as a pin only when its version: line is a literal # version, quoted or not; a computed one (an expression) is not a pin # lookups no lookup failed and no tag was unreadable; the only skipped # dependencies are the ones a rule in renovate.json disables @@ -31,7 +33,8 @@ on: # Every file a manager in renovate.json reads (the workflows hold the # Nextflow, actionlint, gitleaks, ShellCheck, Renovate and docs Python pins, # the docs the NXF_VER lines and the Aldy pin, scripts/lib/common.sh the HLA - # database, the demo script its Python packages), plus the doc generator. + # database, the demo script its Python packages), plus the doc generator, + # the pins table and the checker. # The lookups take about a minute. pull_request: branches: [main] @@ -42,6 +45,8 @@ on: - 'docs/*.md' - '.github/workflows/**' - 'scripts/ci/gen-versions-doc.sh' + - 'scripts/ci/renovate-pins.tsv' + - 'rust/crates/og-dev/src/renovate.rs' - 'scripts/lib/common.sh' - 'tests/demo/retake-screenshots.sh' push: @@ -91,7 +96,7 @@ jobs: lookup: # Public repo: GitHub-hosted runners only. runs-on: ubuntu-latest - timeout-minutes: 20 + timeout-minutes: 40 env: RENOVATE_VERSION: 44.199.2 steps: @@ -129,222 +134,14 @@ jobs: path: ${{ runner.temp }}/renovate.log retention-days: 14 - - name: Write the checker + # The checks are og dev renovate-check (rust/crates/og-dev/src/renovate.rs), + # with the non-image pins in scripts/ci/renovate-pins.tsv. + - name: Build og as scripts/setup.sh --og does if: ${{ !cancelled() && steps.lookup.outcome == 'success' }} run: | - cat > "${RUNNER_TEMP}/check.py" <<'PY' - import collections, copy, glob, json, os, re, sys - - ENV = "versions.env" - TWINS = ("docs/versions.md", "conf/containers.config") - RULE_SKIPS = {"disabled", "package-rules"} - # The non-image pins: (dependency, files, how a pin of it looks in that text). - # A setup-nextflow step is a pin when a version: line with a literal - # version follows its uses: line inside the same step (the lines in - # between are blank or start with anything but '-', which opens the - # next step). - SETUP_NEXTFLOW = r'uses: ["\x27]?nf-core/setup-nextflow@[^\n]*\n(?:[ \t]*(?:[^\s-][^\n]*)?\n)*?[ \t]+version: ["\x27]?\d' - DEMO = "tests/demo/retake-screenshots.sh" - PINS = ( - ("nextflow-io/nextflow", [ENV], r'^NEXTFLOW_VERSION="'), - ("nextflow-io/nextflow", sorted(glob.glob(".github/workflows/*.yml")), SETUP_NEXTFLOW), - ("nextflow-io/nextflow", sorted(glob.glob("docs/*.md")), r"NXF_VER=\d"), - ("rhysd/actionlint", sorted(glob.glob(".github/workflows/*.yml")), r"ACTIONLINT_VERSION=\d"), - ("gitleaks/gitleaks", sorted(glob.glob(".github/workflows/*.yml")), r"GITLEAKS_VERSION=\d"), - ("koalaman/shellcheck", [".github/workflows/lint.yml"], r"^[ \t]+version: v\d"), - ("renovate", [".github/workflows/renovate-dry-run.yml"], r"RENOVATE_VERSION: \d"), - ("renovate", ["docs/updating.md"], r"renovate@\d"), - ("python", [".github/workflows/docs.yml"], r"python-version: ['\"]?\d"), - ("ANHIG/IMGTHLA", ["scripts/lib/common.sh"], r"^HLA_DB_RELEASE=\$\{HLA_DB_RELEASE:-\d"), - ("PGScatalog/pgsc_calc", [ENV], r'^PGSC_CALC_VERSION="'), - ("PGScatalog/pgsc_calc", ["conf/containers.config"], r"ext\.pipeline_version = '"), - ("playwright", [DEMO], r"playwright==\d"), - ("pillow", [DEMO], r"pillow==\d"), - ("pyobjc-framework-Vision", [DEMO], r"pyobjc-framework-Vision==\d"), - ("aldy", ["docs/21-cyrius.md"], r"aldy==\d|\[Aldy\]\([^)]*\) v\d"), - ) - # Only the files a pin is listed for are counted: python is also the - # docker image of versions.env, which the image checks cover. - PIN_FILES = collections.defaultdict(set) - for _name, _files, _ in PINS: - PIN_FILES[_name].update(_files) - - def pin_counts(): - want = collections.Counter() - for name, files, pattern in PINS: - for f in files: - n = len(re.findall(pattern, open(f).read(), re.M)) - if n: - want[(name, f)] += n - return want - - def load(log): - files = None - for line in open(log): - try: - rec = json.loads(line) - except ValueError: - continue - if rec.get("msg") == "packageFiles with updates": - files = rec["config"] - if files is None: - sys.exit("FAIL: no 'packageFiles with updates' record in %s" % log) - return [(pf["packageFile"], dep) for entries in files.values() for pf in entries for dep in pf.get("deps", [])] - - def env_images(text): - out = [] - for m in re.finditer(r'^[A-Z0-9_]+_IMAGE="([^"]*)"', text, re.M): - # name:tag@sha256:..., name@sha256:... or name:tag -> name - out.append(re.sub(r":[^:/]*$", "", m.group(1).split("@", 1)[0])) - return out - - def key(dep): - return (dep.get("depName"), dep.get("currentValue"), dep.get("currentDigest")) - - def proposals(dep): - return sorted((u.get("newValue") or "", u.get("newDigest") or "") for u in dep.get("updates", [])) - - def check(deps, env_text): - fails = [] - docker = [(f, d) for f, d in deps if d.get("datasource") == "docker"] - by_file = collections.defaultdict(list) - for f, d in docker: - by_file[f].append(d) - want = env_images(env_text) - got = [d.get("depName") for d in by_file[ENV]] - if len(got) != len(want): - fails.append("count: versions.env has %d *_IMAGE lines, Renovate found %d docker dependencies there" % (len(want), len(got))) - for name in sorted(set(want) - set(got)): - fails.append("count: %s (versions.env) is not a detected dependency" % name) - for f, d in docker: - if re.search(r"[:@]", d.get("depName") or ":"): - fails.append("names: %s: package name %r" % (f, d.get("depName"))) - env_keys = collections.Counter(key(d) for d in by_file[ENV]) - for f in TWINS: - keys = collections.Counter(key(d) for d in by_file[f]) - if f == "docs/versions.md" and keys != env_keys: - for k in sorted((keys - env_keys) | (env_keys - keys), key=str): - fails.append("twins: docs/versions.md and versions.env differ on %s" % (k,)) - else: - for k in sorted(set(keys) - set(env_keys), key=str): - fails.append("twins: %s has %s, which versions.env does not" % (f, k)) - seen = {} - for f, d in deps: - name = (d.get("datasource"), d.get("depName"), d.get("currentValue"), d.get("currentDigest")) - p = proposals(d) - if name in seen and seen[name][1] != p: - fails.append("one-pr: %s gets %s in %s but %s in %s" % (d.get("depName"), seen[name][1], seen[name][0], p, f)) - seen.setdefault(name, (f, p)) - for f, d in deps: - if d.get("warnings"): - fails.append("lookups: %s (%s): %s" % (d.get("depName"), f, "; ".join(w.get("message", str(w)) for w in d["warnings"]))) - if d.get("skipReason") and d["skipReason"] not in RULE_SKIPS: - fails.append("lookups: %s (%s) skipped: %s" % (d.get("depName"), f, d["skipReason"])) - got = collections.Counter((d.get("depName"), f) for f, d in deps - if f in PIN_FILES.get(d.get("depName"), ())) - for k in sorted(set(want_pins) | set(got)): - if want_pins[k] != got[k]: - fails.append("pins: %s in %s: the text pins it %d times, Renovate found %d" % (k[0], k[1], want_pins[k], got[k])) - return fails - - def self_test(deps, env_text): - ok = True - base = set(check(deps, env_text)) - def plant(label, fn, pattern): - nonlocal ok - d = copy.deepcopy(deps) - try: - fn(d) - except StopIteration: - print("self-test %-8s NOT PLANTED: no dependency to plant it on" % label) - ok = False - return - # Only a failure the plant added counts as caught. - hits = [x for x in check(d, env_text) if x.startswith(pattern) and x not in base] - print("self-test %-8s %s" % (label, "caught: " + hits[0] if hits else "NOT CAUGHT")) - ok = ok and bool(hits) - def first(d, f): - return next(x for g, x in d if g == f and x.get("datasource") == "docker") - def drop(d): - d.remove(next((g, x) for g, x in d if g == ENV and x.get("datasource") == "docker")) - def digest_name(d): - first(d, ENV)["depName"] += "@sha256" - def stale_twin(d): - first(d, "docs/versions.md")["currentValue"] = "0.0.0-planted" - def split_pr(d): - first(d, "conf/containers.config")["updates"] = [{"newValue": "9.9.9-planted"}] - def drop_pin(d): - d.remove(next((g, x) for g, x in d if g == ENV and x.get("depName") == "nextflow-io/nextflow")) - def broken_lookup(d): - first(d, ENV)["skipReason"] = "invalid-value" - plant("count", drop, "count:") - plant("names", digest_name, "names:") - plant("twins", stale_twin, "twins:") - plant("one-pr", split_pr, "one-pr:") - plant("pins", drop_pin, "pins:") - plant("lookups", broken_lookup, "lookups:") - # The setup-nextflow pattern itself, on planted workflow text. - step = (' - uses: %snf-core/setup-nextflow@%s%s # v3.0.1\n' - '%s' - ' if: steps.scope.outputs.run == \'true\'\n' - ' with:\n' - ' version: %s\n' - '\n' - ' - uses: actions/setup-java@%s # v6\n' - ' with:\n' - ' java-version: "17"\n') - for label, quote, gap, version, want in ( - ("computed", "", "", "$" + "{{ steps.nf.outputs.version }}", 0), - ("computed, uses quoted", '"', "", "$" + "{{ steps.nf.outputs.version }}", 0), - ("computed after a blank line", "", "\n", "$" + "{{ steps.nf.outputs.version }}", 0), - ("literal", "", "", '"25.10.8"', 1), - ("literal after a blank line", "", "\n", '"25.10.8"', 1), - ("single-quoted literal", "'", "", "'25.10.8'", 1), - ("bare literal, uses quoted", '"', "", "25.10.8", 1)): - text = step % (quote, "a" * 40, quote, gap, version, "b" * 40) - n = len(re.findall(SETUP_NEXTFLOW, text, re.M)) - print("self-test pin-text %-27s counted %d, want %d" % (label, n, want)) - ok = ok and n == want - clean = check(deps, env_text) - print("self-test clean %s" % ("passes" if not clean else "fails: %s" % clean[0])) - return ok - - def summary(deps): - rows, seen = [], set() - files = collections.defaultdict(set) - for f, d in deps: - files[(d.get("datasource"), d.get("depName"))].add(f) - for f, d in deps: - k = (d.get("datasource"), d.get("depName")) - if k in seen: - continue - seen.add(k) - cur = d.get("currentValue") or d.get("currentDigest") or "" - ups = ", ".join("%s (%s)" % (u.get("newValue") or u.get("newDigest"), u.get("updateType")) - for u in d.get("updates", [])) or "none" - note = d.get("skipReason") or "" - rows.append("| `%s` | `%s` | %s | %s | %d |" % (d.get("depName"), cur, ups, note, len(files[k]))) - return "\n".join(["| Dependency | Current | Proposed update | Skipped | Files |", "|---|---|---|---|---|"] + rows) - - deps = load(sys.argv[1]) - env_text = open(ENV).read() - want_pins = pin_counts() - table = summary(deps) - print(table) - if os.environ.get("GITHUB_STEP_SUMMARY"): - with open(os.environ["GITHUB_STEP_SUMMARY"], "a") as out: - out.write("## Renovate dry run\n\n" + table + "\n") - fails = check(deps, env_text) - for x in fails: - print("FAIL " + x) - if not self_test(deps, env_text): - fails.append("the self-test did not catch a planted fault") - print("FAIL the self-test did not catch a planted fault") - if fails: - sys.exit(1) - print("OK: %d dependencies; every check passed" % len(deps)) - PY + scripts/setup.sh --og + rust/og.sh check --required - name: Check what Renovate found if: ${{ !cancelled() && steps.lookup.outcome == 'success' }} - run: python3 "${RUNNER_TEMP}/check.py" "${RUNNER_TEMP}/renovate.log" + run: bin/og dev renovate-check "${RUNNER_TEMP}/renovate.log" diff --git a/rust/crates/og-dev/src/lib.rs b/rust/crates/og-dev/src/lib.rs index 918237714..ae345abbc 100644 --- a/rust/crates/og-dev/src/lib.rs +++ b/rust/crates/og-dev/src/lib.rs @@ -16,19 +16,22 @@ //! compound-het reads, a trimmed PharmCAT report). //! - `og dev evidence` ([`evidence`]): the measurements of the public //! evidence workflow on GIAB's HG002. +//! - `og dev renovate-check LOG` ([`renovate`]): what a Renovate dry run +//! found, against every pin in the repository. pub mod contract; pub mod evidence; pub mod fixture; pub mod parity; pub mod remnants; +pub mod renovate; pub mod report_only; pub mod schema; mod zip; use std::io::Write; -const USAGE: &str = "usage: og dev parity-compare KIND ORACLE NEW\n og dev parity-compare --self-test\n og dev contract --static [--repo DIR] [--contract FILE]\n og dev contract --outputs DIR --leg bash|nextflow --sample S [--observe]\n og dev schema SCHEMA.json DOC.json\n og dev report-only [--repo DIR]\n og dev remnants [--list FILE] [--root DIR]\n og dev fixture comphet|trim-pharmcat [args]\n og dev evidence clashes|hg002-gvcf [args]\n"; +const USAGE: &str = "usage: og dev parity-compare KIND ORACLE NEW\n og dev parity-compare --self-test\n og dev contract --static [--repo DIR] [--contract FILE]\n og dev contract --outputs DIR --leg bash|nextflow --sample S [--observe]\n og dev schema SCHEMA.json DOC.json\n og dev report-only [--repo DIR]\n og dev remnants [--list FILE] [--root DIR]\n og dev fixture comphet|trim-pharmcat [args]\n og dev evidence clashes|hg002-gvcf [args]\n og dev renovate-check LOG [--pins FILE] [--repo DIR]\n"; /// Python's `str(OSError)`: `[Errno 2] No such file or directory: 'path'`. pub(crate) fn py_os_error(e: &std::io::Error, path: &str) -> String { @@ -65,6 +68,7 @@ pub fn main(args: &[String]) -> u8 { Some("report-only") => run(report_only::cli, &args[1..]), Some("fixture") => run(fixture::cli, &args[1..]), Some("evidence") => run(evidence::cli, &args[1..]), + Some("renovate-check") => run(renovate::cli, &args[1..]), Some("-h" | "--help") => { print!("{USAGE}"); 0 diff --git a/rust/crates/og-dev/src/renovate.rs b/rust/crates/og-dev/src/renovate.rs new file mode 100644 index 000000000..f3355c766 --- /dev/null +++ b/rust/crates/og-dev/src/renovate.rs @@ -0,0 +1,847 @@ +//! `og dev renovate-check LOG [--pins FILE] [--repo DIR]`: checks what a +//! Renovate dry run (`renovate --platform=local --dry-run=lookup`, JSON +//! debug log) found against every pin in the repository. It replaces the +//! Python the `renovate-dry-run.yml` workflow wrote into a file and ran. +//! +//! The checks, each with a planted fault in the self-test that runs after +//! them: +//! +//! - `count`: every `*_IMAGE` line of `versions.env` is a detected docker +//! dependency; +//! - `names`: no docker package name carries a `:` or `@`; +//! - `twins`: `docs/versions.md` holds exactly the images of `versions.env`, +//! and `conf/containers.config` holds only those; +//! - `one-pr`: a dependency gets the same proposals in every file it is in; +//! - `pins`: every non-image pin of the pins table +//! (`scripts/ci/renovate-pins.tsv`) is detected as often as its file pins +//! it; +//! - `lookups`: no lookup failed, no tag was unreadable, and the only +//! skipped dependencies are the ones a rule in `renovate.json` disables. +//! +//! The pins table has three tab-separated columns: the dependency, a file +//! or a glob of files (relative to the repository; `*` and `?` in the last +//! part), and a regular expression for one pin of it in that text, read +//! with `^` and `$` at line ends. A file that is missing, or a glob that +//! matches nothing, is an error (exit 2): a pin counted in no file could +//! never fail. +//! +//! Output: the Markdown table of every dependency and its proposed update +//! (also appended to `$GITHUB_STEP_SUMMARY`), one `FAIL : ...` line +//! per problem, the self-test lines, then `OK: N dependencies; every check +//! passed`. Exit 0 when clean, 1 on a finding, a self-test miss, or a log +//! without its `packageFiles with updates` record, 2 on a usage error or a +//! pins table it cannot use. + +use indexmap::IndexMap; +use pycompat::json::{self, Value}; +use pycompat::value::{py_repr, py_str, truthy}; +use regex::Regex; +use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::fs; +use std::io::{BufRead, BufReader, Write}; +use std::path::Path; + +const USAGE: &str = "usage: og dev renovate-check LOG [--pins FILE] [--repo DIR]\n"; +const ENV: &str = "versions.env"; +const DOCS: &str = "docs/versions.md"; +const CONFIG: &str = "conf/containers.config"; +const TWINS: [&str; 2] = [DOCS, CONFIG]; +const RULE_SKIPS: [&str; 2] = ["disabled", "package-rules"]; +const PINS: &str = "scripts/ci/renovate-pins.tsv"; + +/// One dependency Renovate found: the file it is in and its record. +type Dep = (String, IndexMap); + +/// The pins table, expanded: per (dependency, file) how many pins the text +/// holds, and per dependency the files listed for it. +struct Pins { + want: BTreeMap<(String, String), usize>, + files: HashMap>, + setup_nextflow: Option, +} + +/// Runs `og dev renovate-check ARGS`; returns the exit code. +pub fn cli(args: &[String], out: &mut dyn Write, err: &mut dyn Write) -> u8 { + let (mut log, mut pins, mut repo) = (None, PINS.to_owned(), ".".to_owned()); + let mut it = args.iter(); + while let Some(a) = it.next() { + match a.as_str() { + "-h" | "--help" => { + let _ = out.write_all(USAGE.as_bytes()); + return 0; + } + "--pins" | "--repo" => { + let Some(v) = it.next() else { + let _ = writeln!( + err, + "{USAGE}og dev renovate-check: error: {a} needs a value" + ); + return 2; + }; + if a == "--pins" { + pins = v.clone(); + } else { + repo = v.clone(); + } + } + _ if log.is_none() => log = Some(a.clone()), + _ => { + let _ = writeln!( + err, + "{USAGE}og dev renovate-check: error: unexpected argument '{a}'" + ); + return 2; + } + } + } + let Some(log) = log else { + let _ = writeln!( + err, + "{USAGE}og dev renovate-check: error: the following arguments are required: LOG" + ); + return 2; + }; + let repo = Path::new(&repo); + let pins = match read_pins(repo, &repo.join(&pins)) { + Ok(p) => p, + Err(e) => { + let _ = writeln!(err, "og dev renovate-check: {e}"); + return 2; + } + }; + let env_text = match fs::read_to_string(repo.join(ENV)) { + Ok(t) => t, + Err(e) => { + let _ = writeln!(err, "og dev renovate-check: {ENV}: {e}"); + return 2; + } + }; + let deps = match load(&log) { + Ok(d) => d, + Err(e) => { + let _ = writeln!(err, "{e}"); + return 1; + } + }; + let table = summary(&deps); + let _ = writeln!(out, "{table}"); + if let Some(path) = std::env::var_os("GITHUB_STEP_SUMMARY").filter(|p| !p.is_empty()) { + let appended = fs::OpenOptions::new() + .append(true) + .create(true) + .open(&path) + .and_then(|mut f| write!(f, "## Renovate dry run\n\n{table}\n")); + if let Err(e) = appended { + let _ = writeln!(err, "og dev renovate-check: GITHUB_STEP_SUMMARY: {e}"); + } + } + let mut fails = check(&deps, &env_text, &pins); + for x in &fails { + let _ = writeln!(out, "FAIL {x}"); + } + if !self_test(&deps, &env_text, &pins, out) { + fails.push("the self-test did not catch a planted fault".to_owned()); + let _ = writeln!(out, "FAIL the self-test did not catch a planted fault"); + } + if !fails.is_empty() { + return 1; + } + let _ = writeln!(out, "OK: {} dependencies; every check passed", deps.len()); + 0 +} + +/// `fnmatch` for `*` and `?`, case-sensitive. +fn fnmatch(pat: &[u8], name: &[u8]) -> bool { + match pat.split_first() { + None => name.is_empty(), + Some((b'*', rest)) => (0..=name.len()).any(|i| fnmatch(rest, &name[i..])), + Some((b'?', rest)) => !name.is_empty() && fnmatch(rest, &name[1..]), + Some((c, rest)) => name.first() == Some(c) && fnmatch(rest, &name[1..]), + } +} + +/// The files of `pattern` under `repo`, relative and sorted, as Python's +/// `sorted(glob.glob(pattern))` lists them: a wildcard never matches a name +/// that starts with a dot unless the pattern does. +fn glob(repo: &Path, pattern: &str) -> Result, String> { + let (dir, last) = pattern.rsplit_once('/').unwrap_or(("", pattern)); + if dir.contains(['*', '?']) { + return Err(format!( + "{pattern}: a wildcard is allowed only in the last part" + )); + } + if !last.contains(['*', '?']) { + return if repo.join(pattern).is_file() { + Ok(vec![pattern.to_owned()]) + } else { + Err(format!("{pattern} is not a file")) + }; + } + let mut out = Vec::new(); + let entries = fs::read_dir(repo.join(if dir.is_empty() { "." } else { dir })) + .map_err(|e| format!("{pattern}: {e}"))?; + for e in entries { + let name = e.map_err(|e| format!("{pattern}: {e}"))?.file_name(); + let name = name.to_string_lossy(); + if (name.starts_with('.') && !last.starts_with('.')) + || !fnmatch(last.as_bytes(), name.as_bytes()) + { + continue; + } + out.push(if dir.is_empty() { + name.into_owned() + } else { + format!("{dir}/{name}") + }); + } + out.sort(); + if out.is_empty() { + return Err(format!("{pattern} matches no file")); + } + Ok(out) +} + +fn read_pins(repo: &Path, path: &Path) -> Result { + let text = fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()))?; + let mut pins = Pins { + want: BTreeMap::new(), + files: HashMap::new(), + setup_nextflow: None, + }; + let mut texts: HashMap = HashMap::new(); + for (i, line) in text.lines().enumerate() { + if line.is_empty() || line.starts_with('#') { + continue; + } + let at = |msg: &str| format!("{}:{}: {msg}", path.display(), i + 1); + let cols: Vec<&str> = line.split('\t').collect(); + let [dep, files, pattern] = cols[..] else { + return Err(at( + "want three tab-separated columns: dependency, file or glob, pattern", + )); + }; + let re = Regex::new(&format!("(?m){pattern}")).map_err(|e| at(&e.to_string()))?; + if pattern.contains("setup-nextflow") { + pins.setup_nextflow = Some(re.clone()); + } + for f in glob(repo, files).map_err(|e| at(&e))? { + if !texts.contains_key(&f) { + let t = fs::read_to_string(repo.join(&f)).map_err(|e| at(&format!("{f}: {e}")))?; + texts.insert(f.clone(), t); + } + let n = re.find_iter(&texts[&f]).count(); + if n > 0 { + *pins.want.entry((dep.to_owned(), f.clone())).or_default() += n; + } + pins.files.entry(dep.to_owned()).or_default().insert(f); + } + } + Ok(pins) +} + +/// Every dependency of the log's last `packageFiles with updates` record. +fn load(log: &str) -> Result, String> { + let f = fs::File::open(log).map_err(|e| format!("FAIL: cannot read {log}: {e}"))?; + let mut files = None; + let mut r = BufReader::new(f); + let mut buf = Vec::new(); + loop { + buf.clear(); + if r.read_until(b'\n', &mut buf) + .map_err(|e| format!("FAIL: cannot read {log}: {e}"))? + == 0 + { + break; + } + let Ok(Value::Object(mut rec)) = json::loads(&String::from_utf8_lossy(&buf)) else { + continue; + }; + if rec.get("msg") == Some(&Value::Str("packageFiles with updates".to_owned())) { + files = Some(rec.swap_remove("config").unwrap_or(Value::Null)); + } + } + let Some(files) = files else { + return Err(format!( + "FAIL: no 'packageFiles with updates' record in {log}" + )); + }; + let shape = || { + format!( + "FAIL: the 'packageFiles with updates' record of {log} is not a map of package file lists" + ) + }; + let Value::Object(files) = files else { + return Err(shape()); + }; + let mut deps = Vec::new(); + for entries in files.into_values() { + let Value::Array(entries) = entries else { + return Err(shape()); + }; + for pf in entries { + let Value::Object(mut pf) = pf else { + return Err(shape()); + }; + let name = pf + .get("packageFile") + .map(py_str) + .ok_or_else(shape)? + .map_err(|_| shape())?; + if let Some(Value::Array(ds)) = pf.swap_remove("deps") { + for d in ds { + let Value::Object(d) = d else { + return Err(shape()); + }; + deps.push((name.clone(), d)); + } + } + } + } + Ok(deps) +} + +fn get<'a>(d: &'a IndexMap, k: &str) -> &'a Value { + d.get(k).unwrap_or(&Value::Null) +} + +/// Python's `"%s" % v`. +fn s(v: &Value) -> String { + py_str(v).unwrap_or_else(|_| format!("{v:?}")) +} + +/// Python's `repr(v)`. +fn r(v: &Value) -> String { + py_repr(v).unwrap_or_else(|_| format!("{v:?}")) +} + +/// `repr` of a tuple of values. +fn tuple(vs: &[&Value]) -> String { + let items: Vec = vs.iter().map(|v| r(v)).collect(); + format!("({})", items.join(", ")) +} + +fn is_docker(d: &IndexMap) -> bool { + get(d, "datasource") == &Value::Str("docker".to_owned()) +} + +/// The image names of `versions.env`: `name:tag@sha256:...`, +/// `name@sha256:...` and `name:tag` all give `name`. +fn env_images(text: &str) -> Vec { + let line = Regex::new(r#"(?m)^[A-Z0-9_]+_IMAGE="([^"]*)""#).expect("a valid pattern"); + let tag = Regex::new(r":[^:/]*$").expect("a valid pattern"); + line.captures_iter(text) + .map(|c| { + let v = c[1].split('@').next().unwrap_or(""); + tag.replace(v, "").into_owned() + }) + .collect() +} + +/// `(depName, currentValue, currentDigest)` as Python prints the tuple. +fn key(d: &IndexMap) -> String { + tuple(&[ + get(d, "depName"), + get(d, "currentValue"), + get(d, "currentDigest"), + ]) +} + +/// `v or ""`. +fn or_empty(v: &Value) -> Value { + if truthy(v) { + v.clone() + } else { + Value::Str(String::new()) + } +} + +/// The updates as sorted `(newValue or "", newDigest or "")` pairs, printed +/// as Python prints the list. +fn proposals(d: &IndexMap) -> String { + let mut ps: Vec<(Value, Value)> = updates(d) + .iter() + .map(|u| match u { + Value::Object(u) => (or_empty(get(u, "newValue")), or_empty(get(u, "newDigest"))), + _ => (Value::Null, Value::Null), + }) + .collect(); + ps.sort_by(|a, b| (s(&a.0), s(&a.1)).cmp(&(s(&b.0), s(&b.1)))); + let items: Vec = ps.iter().map(|(a, b)| tuple(&[a, b])).collect(); + format!("[{}]", items.join(", ")) +} + +fn updates(d: &IndexMap) -> &[Value] { + match d.get("updates") { + Some(Value::Array(u)) => u, + _ => &[], + } +} + +fn counter<'a>(it: impl Iterator>) -> BTreeMap { + let mut c = BTreeMap::new(); + for d in it { + *c.entry(key(d)).or_default() += 1; + } + c +} + +/// The docker dependencies of one file. +fn in_file<'a>( + docker: &'a [&'a Dep], + f: &'a str, +) -> impl Iterator> { + docker.iter().filter(move |(g, _)| g == f).map(|(_, d)| d) +} + +/// Every problem, as `check: ...` lines. +fn check(deps: &[Dep], env_text: &str, pins: &Pins) -> Vec { + let mut fails = Vec::new(); + let docker: Vec<&Dep> = deps.iter().filter(|(_, d)| is_docker(d)).collect(); + let want = env_images(env_text); + let got: Vec<&Value> = in_file(&docker, ENV).map(|d| get(d, "depName")).collect(); + if got.len() != want.len() { + fails.push(format!( + "count: versions.env has {} *_IMAGE lines, Renovate found {} docker dependencies there", + want.len(), + got.len() + )); + } + let want_set: BTreeSet<&String> = want.iter().collect(); + for name in want_set { + if !got.contains(&&Value::Str(name.clone())) { + fails.push(format!( + "count: {name} (versions.env) is not a detected dependency" + )); + } + } + for (f, d) in &docker { + let name = get(d, "depName"); + let text = if truthy(name) { + s(name) + } else { + ":".to_owned() + }; + if text.contains([':', '@']) { + fails.push(format!("names: {f}: package name {}", r(name))); + } + } + let env_keys = counter(in_file(&docker, ENV)); + for f in TWINS { + let keys = counter(in_file(&docker, f)); + if f == DOCS && keys != env_keys { + let all: BTreeSet<&String> = keys.keys().chain(env_keys.keys()).collect(); + for k in all { + if keys.get(k) != env_keys.get(k) { + fails.push(format!( + "twins: docs/versions.md and versions.env differ on {k}" + )); + } + } + } else { + for k in keys.keys().filter(|k| !env_keys.contains_key(*k)) { + fails.push(format!("twins: {f} has {k}, which versions.env does not")); + } + } + } + let mut seen: HashMap = HashMap::new(); + for (f, d) in deps { + let name = tuple(&[ + get(d, "datasource"), + get(d, "depName"), + get(d, "currentValue"), + get(d, "currentDigest"), + ]); + let p = proposals(d); + match seen.get(&name) { + Some((first, q)) if *q != p => fails.push(format!( + "one-pr: {} gets {q} in {first} but {p} in {f}", + s(get(d, "depName")) + )), + Some(_) => {} + None => { + seen.insert(name, (f, p)); + } + } + } + for (f, d) in deps { + let warnings = get(d, "warnings"); + if truthy(warnings) { + let msgs: Vec = match warnings { + Value::Array(ws) => ws + .iter() + .map(|w| match w { + Value::Object(o) if o.contains_key("message") => s(get(o, "message")), + w => s(w), + }) + .collect(), + w => vec![s(w)], + }; + fails.push(format!( + "lookups: {} ({f}): {}", + s(get(d, "depName")), + msgs.join("; ") + )); + } + let skip = get(d, "skipReason"); + if truthy(skip) && !RULE_SKIPS.contains(&s(skip).as_str()) { + fails.push(format!( + "lookups: {} ({f}) skipped: {}", + s(get(d, "depName")), + s(skip) + )); + } + } + let mut got: BTreeMap<(String, String), usize> = BTreeMap::new(); + for (f, d) in deps { + if let Value::Str(name) = get(d, "depName") + && pins.files.get(name).is_some_and(|fs| fs.contains(f)) + { + *got.entry((name.clone(), f.clone())).or_default() += 1; + } + } + let all: BTreeSet<&(String, String)> = pins.want.keys().chain(got.keys()).collect(); + for k in all { + let (w, g) = ( + pins.want.get(k).copied().unwrap_or(0), + got.get(k).copied().unwrap_or(0), + ); + if w != g { + fails.push(format!( + "pins: {} in {}: the text pins it {w} times, Renovate found {g}", + k.0, k.1 + )); + } + } + fails +} + +/// Plants one fault of each kind in a copy of the dependencies and shows +/// that the check it belongs to catches it; true when every one is caught. +fn self_test(deps: &[Dep], env_text: &str, pins: &Pins, out: &mut dyn Write) -> bool { + let mut ok = true; + let base: BTreeSet = check(deps, env_text, pins).into_iter().collect(); + let first = |d: &[Dep], f: &str| d.iter().position(|(g, x)| g == f && is_docker(x)); + type Plant = fn(&mut Vec, usize); + let plants: [(&str, &str, Option<&str>, Plant); 6] = [ + ("count", "count:", Some(ENV), |d, i| { + d.remove(i); + }), + ("names", "names:", Some(ENV), |d, i| { + let name = format!("{}@sha256", s(get(&d[i].1, "depName"))); + d[i].1.insert("depName".to_owned(), Value::Str(name)); + }), + ("twins", "twins:", Some(DOCS), |d, i| { + d[i].1.insert( + "currentValue".to_owned(), + Value::Str("0.0.0-planted".to_owned()), + ); + }), + ("one-pr", "one-pr:", Some(CONFIG), |d, i| { + let u = IndexMap::from([( + "newValue".to_owned(), + Value::Str("9.9.9-planted".to_owned()), + )]); + d[i].1 + .insert("updates".to_owned(), Value::Array(vec![Value::Object(u)])); + }), + ("pins", "pins:", None, |d, i| { + d.remove(i); + }), + ("lookups", "lookups:", Some(ENV), |d, i| { + d[i].1.insert( + "skipReason".to_owned(), + Value::Str("invalid-value".to_owned()), + ); + }), + ]; + for (label, pattern, file, plant) in plants { + let at = match file { + Some(f) => first(deps, f), + None => deps.iter().position(|(g, x)| { + g == ENV && get(x, "depName") == &Value::Str("nextflow-io/nextflow".to_owned()) + }), + }; + let Some(i) = at else { + let _ = writeln!( + out, + "self-test {label:<8} NOT PLANTED: no dependency to plant it on" + ); + ok = false; + continue; + }; + let mut d = deps.to_vec(); + plant(&mut d, i); + // Only a failure the plant added counts as caught. + let hit = check(&d, env_text, pins) + .into_iter() + .find(|x| x.starts_with(pattern) && !base.contains(x)); + match hit { + Some(h) => { + let _ = writeln!(out, "self-test {label:<8} caught: {h}"); + } + None => { + let _ = writeln!(out, "self-test {label:<8} NOT CAUGHT"); + ok = false; + } + } + } + // The setup-nextflow pattern itself, on planted workflow text. + let Some(setup) = &pins.setup_nextflow else { + let _ = writeln!( + out, + "self-test pin-text NOT PLANTED: the pins table has no setup-nextflow pattern" + ); + return false; + }; + let computed = "${{ steps.nf.outputs.version }}"; + let cases: [(&str, &str, &str, &str, usize); 7] = [ + ("computed", "", "", computed, 0), + ("computed, uses quoted", "\"", "", computed, 0), + ("computed after a blank line", "", "\n", computed, 0), + ("literal", "", "", "\"25.10.8\"", 1), + ("literal after a blank line", "", "\n", "\"25.10.8\"", 1), + ("single-quoted literal", "'", "", "'25.10.8'", 1), + ("bare literal, uses quoted", "\"", "", "25.10.8", 1), + ]; + for (label, quote, gap, version, want) in cases { + let text = format!( + " - uses: {quote}nf-core/setup-nextflow@{a}{quote} # v3.0.1\n{gap} if: steps.scope.outputs.run == 'true'\n with:\n version: {version}\n\n - uses: actions/setup-java@{b} # v6\n with:\n java-version: \"17\"\n", + a = "a".repeat(40), + b = "b".repeat(40), + ); + let n = setup.find_iter(&text).count(); + let _ = writeln!( + out, + "self-test pin-text {label:<27} counted {n}, want {want}" + ); + ok = ok && n == want; + } + let clean = check(deps, env_text, pins); + let verdict = clean + .first() + .map_or_else(|| "passes".to_owned(), |c| format!("fails: {c}")); + let _ = writeln!(out, "self-test clean {verdict}"); + ok +} + +/// The Markdown table of every dependency, once per datasource and name. +fn summary(deps: &[Dep]) -> String { + let id = |d: &IndexMap| tuple(&[get(d, "datasource"), get(d, "depName")]); + let mut files: HashMap> = HashMap::new(); + for (f, d) in deps { + files.entry(id(d)).or_default().insert(f); + } + let mut rows = vec![ + "| Dependency | Current | Proposed update | Skipped | Files |".to_owned(), + "|---|---|---|---|---|".to_owned(), + ]; + let mut seen = BTreeSet::new(); + for (_, d) in deps { + let k = id(d); + if !seen.insert(k.clone()) { + continue; + } + let cur = [get(d, "currentValue"), get(d, "currentDigest")] + .into_iter() + .find(|v| truthy(v)) + .map_or_else(String::new, s); + let ups: Vec = updates(d) + .iter() + .map(|u| { + let u = match u { + Value::Object(u) => u, + _ => return s(u), + }; + let new = if truthy(get(u, "newValue")) { + get(u, "newValue") + } else { + get(u, "newDigest") + }; + format!("{} ({})", s(new), s(get(u, "updateType"))) + }) + .collect(); + let ups = if ups.is_empty() { + "none".to_owned() + } else { + ups.join(", ") + }; + let skip = get(d, "skipReason"); + let note = if truthy(skip) { s(skip) } else { String::new() }; + rows.push(format!( + "| `{}` | `{cur}` | {ups} | {note} | {} |", + s(get(d, "depName")), + files[&k].len() + )); + } + rows.join("\n") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn fnmatch_and_env_images() { + assert!( + fnmatch(b"*.yml", b"e2e.yml") + && !fnmatch(b"*.yml", b"e2e.yaml") + && fnmatch(b"a?c", b"abc") + ); + let env = "A_IMAGE=\"x/y:1.2@sha256:ab\"\nB_IMAGE=\"z@sha256:cd\"\nC_IMAGE=\"host:5000/w:3\"\nD=\"1\"\n"; + assert_eq!(env_images(env), ["x/y", "z", "host:5000/w"]); + } + + #[test] + fn keys_and_proposals_print_as_python() { + let d: IndexMap = match json::loads( + r#"{"depName":"it's","currentValue":"1","updates":[{"newValue":"3"},{"newValue":"2","newDigest":"d"}]}"#, + ) { + Ok(Value::Object(o)) => o, + other => panic!("{other:?}"), + }; + assert_eq!(key(&d), r#"("it's", '1', None)"#); + assert_eq!(proposals(&d), "[('2', 'd'), ('3', '')]"); + } + + /// A repository of the files the check reads, and a log that matches it. + fn planted(name: &str) -> std::path::PathBuf { + let d = std::env::temp_dir().join(format!("og-renovate-{name}-{}", std::process::id())); + let _ = fs::remove_dir_all(&d); + for sub in ["docs", "conf", "scripts/ci", ".github/workflows"] { + fs::create_dir_all(d.join(sub)).expect("mkdir"); + } + let w = |p: &str, t: &str| fs::write(d.join(p), t).expect("write"); + w( + ENV, + "NEXTFLOW_VERSION=\"25.10.8\"\nA_IMAGE=\"x/a:1@sha256:aa\"\nB_IMAGE=\"x/b:2\"\n", + ); + w(DOCS, "| a | x/a:1 |\n"); + w(CONFIG, "container = 'x/a:1'\n"); + w( + ".github/workflows/ci.yml", + " - uses: nf-core/setup-nextflow@abc # v3\n with:\n version: \"25.10.8\"\n", + ); + w( + ".github/workflows/.hidden.yml", + " - uses: nf-core/setup-nextflow@abc # v3\n with:\n version: \"25.10.8\"\n", + ); + w( + PINS, + "# comment\nnextflow-io/nextflow\tversions.env\t^NEXTFLOW_VERSION=\"\nnextflow-io/nextflow\t.github/workflows/*.yml\tuses: [\"\\x27]?nf-core/setup-nextflow@[^\\n]*\\n(?:[ \\t]*(?:[^\\s-][^\\n]*)?\\n)*?[ \\t]+version: [\"\\x27]?\\d\n", + ); + let dep = |n: &str, v: &str, ds: &str| { + format!(r#"{{"depName":"{n}","currentValue":"{v}","datasource":"{ds}"}}"#) + }; + let log = format!( + "not json\n{{\"msg\":\"other\"}}\n{{\"msg\":\"packageFiles with updates\",\"config\":{{\"regex\":[{{\"packageFile\":\"versions.env\",\"deps\":[{},{},{}]}},{{\"packageFile\":\"docs/versions.md\",\"deps\":[{},{}]}},{{\"packageFile\":\"conf/containers.config\",\"deps\":[{}]}},{{\"packageFile\":\".github/workflows/ci.yml\",\"deps\":[{}]}}]}}}}\n", + dep("x/a", "1", "docker"), + dep("x/b", "2", "docker"), + dep("nextflow-io/nextflow", "25.10.8", "github-releases"), + dep("x/a", "1", "docker"), + dep("x/b", "2", "docker"), + dep("x/a", "1", "docker"), + dep("nextflow-io/nextflow", "25.10.8", "github-releases"), + ); + w("renovate.log", &log); + d + } + + fn run(d: &Path) -> (u8, String) { + let args: Vec = [ + d.join("renovate.log").to_string_lossy().into_owned(), + "--repo".to_owned(), + d.to_string_lossy().into_owned(), + ] + .into(); + let (mut out, mut err) = (Vec::new(), Vec::new()); + let code = cli(&args, &mut out, &mut err); + ( + code, + String::from_utf8_lossy(&out).into_owned() + &String::from_utf8_lossy(&err), + ) + } + + #[test] + fn a_clean_tree_passes_and_every_plant_is_caught() { + let d = planted("clean"); + let (code, out) = run(&d); + assert_eq!(code, 0, "{out}"); + assert!( + out.contains("OK: 7 dependencies; every check passed"), + "{out}" + ); + assert!(out.contains("| `x/a` | `1` | none | | 3 |"), "{out}"); + assert_eq!(out.matches("caught: ").count(), 6, "{out}"); + let _ = fs::remove_dir_all(&d); + } + + #[test] + fn a_pin_renovate_misses_fails() { + let d = planted("pin"); + fs::write( + d.join(".github/workflows/more.yml"), + " - uses: 'nf-core/setup-nextflow@abc'\n\n with:\n version: '26.04.0'\n", + ) + .expect("write"); + let (code, out) = run(&d); + assert_eq!(code, 1, "{out}"); + assert!( + out.contains("FAIL pins: nextflow-io/nextflow in .github/workflows/more.yml: the text pins it 1 times, Renovate found 0"), + "{out}" + ); + let _ = fs::remove_dir_all(&d); + } + + #[test] + fn an_image_renovate_misses_and_a_stale_twin_fail() { + let d = planted("image"); + let log = fs::read_to_string(d.join("renovate.log")).expect("read"); + let log = log.replacen( + r#"{"depName":"x/b","currentValue":"2","datasource":"docker"},"#, + "", + 1, + ); + let log = log.replacen( + r#""docs/versions.md","deps":[{"depName":"x/a","currentValue":"1""#, + r#""docs/versions.md","deps":[{"depName":"x/a","currentValue":"0""#, + 1, + ); + fs::write(d.join("renovate.log"), log).expect("write"); + let (code, out) = run(&d); + assert_eq!(code, 1, "{out}"); + assert!( + out.contains("FAIL count: x/b (versions.env) is not a detected dependency"), + "{out}" + ); + assert!( + out.contains( + "FAIL twins: docs/versions.md and versions.env differ on ('x/a', '0', None)" + ), + "{out}" + ); + let _ = fs::remove_dir_all(&d); + } + + #[test] + fn a_glob_that_matches_nothing_is_refused() { + let d = planted("glob"); + fs::write(d.join(PINS), "aldy\tdocs/*.txt\taldy==\\d\n").expect("write"); + let (code, out) = run(&d); + assert_eq!(code, 2, "{out}"); + assert!(out.contains("docs/*.txt matches no file"), "{out}"); + let _ = fs::remove_dir_all(&d); + } + + #[test] + fn a_log_without_the_record_fails() { + let d = planted("nolog"); + fs::write(d.join("renovate.log"), "{\"msg\":\"other\"}\n").expect("write"); + let (code, out) = run(&d); + assert_eq!(code, 1, "{out}"); + assert!( + out.contains("FAIL: no 'packageFiles with updates' record in"), + "{out}" + ); + let _ = fs::remove_dir_all(&d); + } +} diff --git a/scripts/ci/renovate-pins.tsv b/scripts/ci/renovate-pins.tsv new file mode 100644 index 000000000..ba15cba87 --- /dev/null +++ b/scripts/ci/renovate-pins.tsv @@ -0,0 +1,25 @@ +# The non-image pins `og dev renovate-check` counts (.github/workflows/renovate-dry-run.yml): +# each must be a dependency Renovate detects in every place it is pinned, as often as the text has it. +# dependencyfile or glob (wildcards in the last part only)regular expression for one pin, +# with ^ and $ at line ends. A missing file or a glob that matches nothing is an error. +# Only the files listed for a dependency are counted: python is also the docker image of +# versions.env, which the image checks cover. +# A setup-nextflow step is a pin when a version: line with a literal version follows its uses: +# line inside the same step (the lines in between are blank or start with anything but '-', +# which opens the next step); a computed version (an expression) is not a pin. +nextflow-io/nextflow versions.env ^NEXTFLOW_VERSION=" +nextflow-io/nextflow .github/workflows/*.yml uses: ["\x27]?nf-core/setup-nextflow@[^\n]*\n(?:[ \t]*(?:[^\s-][^\n]*)?\n)*?[ \t]+version: ["\x27]?\d +nextflow-io/nextflow docs/*.md NXF_VER=\d +rhysd/actionlint .github/workflows/*.yml ACTIONLINT_VERSION=\d +gitleaks/gitleaks .github/workflows/*.yml GITLEAKS_VERSION=\d +koalaman/shellcheck .github/workflows/lint.yml ^[ \t]+version: v\d +renovate .github/workflows/renovate-dry-run.yml RENOVATE_VERSION: \d +renovate docs/updating.md renovate@\d +python .github/workflows/docs.yml python-version: ['\"]?\d +ANHIG/IMGTHLA scripts/lib/common.sh ^HLA_DB_RELEASE=\$\{HLA_DB_RELEASE:-\d +PGScatalog/pgsc_calc versions.env ^PGSC_CALC_VERSION=" +PGScatalog/pgsc_calc conf/containers.config ext\.pipeline_version = ' +playwright tests/demo/retake-screenshots.sh playwright==\d +pillow tests/demo/retake-screenshots.sh pillow==\d +pyobjc-framework-Vision tests/demo/retake-screenshots.sh pyobjc-framework-Vision==\d +aldy docs/21-cyrius.md aldy==\d|\[Aldy\]\([^)]*\) v\d From c6750ad148e209167fd066eba81c8aa115431308 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 11:19:14 +0100 Subject: [PATCH 04/45] refactor(ci): switch the evidence workflow and fixtures to og, delete their Python public-evidence.yml runs og dev evidence clashes and hg002-gvcf; the comphet e2e case builds its fixture with og dev fixture comphet. The comphet generator and the PharmCAT trimmer are deleted. Bead: pgp-bs9u.39 --- .github/workflows/e2e.yml | 1 - .github/workflows/public-evidence.yml | 106 +++-------------- scripts/ci/e2e-case-paths.tsv | 2 +- scripts/ci/e2e-scope.sh | 2 +- tests/e2e/comphet-phasing-1-reads.sh | 4 +- tests/fixtures/comphet/README.md | 3 +- tests/fixtures/comphet/make_fixture.py | 150 ------------------------- tests/fixtures/pharmcat/README.md | 4 +- tests/fixtures/pharmcat/trim_report.py | 57 ---------- 9 files changed, 22 insertions(+), 307 deletions(-) delete mode 100755 tests/fixtures/comphet/make_fixture.py delete mode 100755 tests/fixtures/pharmcat/trim_report.py diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index e7c02c998..39d4b272d 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -67,7 +67,6 @@ on: - '.github/workflows/e2e.yml' - '.github/actions/**' - 'tests/fixtures/annotsv/class45.sh' - - 'tests/fixtures/comphet/make_fixture.py' - 'tests/fixtures/consumer_export/sites.tsv.gz' - 'tests/fixtures/families/stub_families.tsv' - 'tests/fixtures/pharmcat/refblocks.g.vcf' diff --git a/.github/workflows/public-evidence.yml b/.github/workflows/public-evidence.yml index 11b39d5a4..aa6e7dcc7 100644 --- a/.github/workflows/public-evidence.yml +++ b/.github/workflows/public-evidence.yml @@ -166,6 +166,11 @@ jobs: name: archaic-1kg-sites path: sites + - name: Build og as scripts/setup.sh --og does (og dev evidence) + run: | + scripts/setup.sh --og + rust/og.sh check --required + - name: setup.sh --archaic and step 43 on HG002 (chromosome ${{ matrix.part }}) id: run continue-on-error: ${{ matrix.part == 'empty' }} @@ -230,47 +235,7 @@ jobs: # same-position rule of archaic_report.py decides). docker run --rm -u "$(id -u):$(id -g)" -v "${G}:/g" "$BCFTOOLS_IMAGE" \ bcftools norm -m- -Oz -o /g/HG002/split.vcf.gz /g/HG002/vcf/HG002.vcf.gz - python3 -I - "${G}/HG002/vcf/HG002.vcf.gz" "${G}/HG002/split.vcf.gz" "$DATA" "$OUT/affected.tsv" <<'PY' - import glob, gzip, os, sys - raw, split, data, out = sys.argv[1:] - tables = sorted(glob.glob(os.path.join(data, "chr*.tsv.gz"))) - chroms = {os.path.basename(t)[:-7] for t in tables} # this part's chromosomes only - def clashes(path): - """Positions of this part with two records or more, one an SNV (the VCF is sorted).""" - hit, key, n, snvs = set(), None, 0, 0 - for line in gzip.open(path, "rt"): - if line.startswith("#"): - continue - c = line.split("\t", 6) - if c[0] not in chroms: - continue - k = (c[0], int(c[1])) - if k != key: - key, n, snvs = k, 0, 0 - n += 1 - snvs += len(c[3]) == 1 and len(c[4]) == 1 - if n >= 2 and snvs >= 1: - hit.add(key) - return hit - sets = {"raw": clashes(raw), "split": clashes(split)} - on_table = {k: 0 for k in sets} - for t in tables: - chrom = os.path.basename(t)[:-7] - want = {k: {p for c, p in v if c == chrom} for k, v in sets.items()} - if not any(want.values()): - continue - for line in gzip.open(t, "rt"): - c = line.split("\t", 2) - if c[0] != chrom: - continue - p = int(c[1]) - for k in want: - if p in want[k]: - on_table[k] += 1 - with open(out, "w") as f: - for k in sets: - f.write(f"{k}_positions\t{len(sets[k])}\n{k}_on_table\t{on_table[k]}\n") - PY + bin/og dev evidence clashes "${G}/HG002/vcf/HG002.vcf.gz" "${G}/HG002/split.vcf.gz" "$DATA" "$OUT/affected.tsv" cat "$OUT/affected.tsv" - name: The red control failed, as it must @@ -597,6 +562,11 @@ jobs: gzip -dc "${R}/${f}.gz" > "${R}/GRCh38_no_alt_analysis_set.fasta" rm -f "${R}/${f}.gz" + - name: Build og as scripts/setup.sh --og does (og dev evidence) + run: | + scripts/setup.sh --og + rust/og.sh check --required + - name: setup.sh --ancient-panel and step 44 on HG002, measured env: THREADS: '4' @@ -672,57 +642,9 @@ jobs: mkdir -p "${G}/HG002/vcf" curl -fsSL --retry 5 -o "${EV}/truth.vcf.gz" "${GIAB_HG002}.vcf.gz" curl -fsSL --retry 5 -o "${EV}/truth.bed" "${GIAB_HG002}_noinconsistent.bed" - python3 -I - "${EV}/truth.vcf.gz" "${EV}/truth.bed" "${G}/reference/GRCh38_no_alt_analysis_set.fasta" \ - "${PANEL}/sites_grch38.tsv" "${EV}/hg002.unsorted.vcf" <<'PY' - import bisect, gzip, sys - truth, bed, ref, sites, out = sys.argv[1:] - fai = {l.split("\t")[0]: [int(x) for x in l.split("\t")[1:5]] for l in open(ref + ".fai")} - fa = open(ref, "rb") - def base(c, p): - n, off, lb, lw = fai[c] - fa.seek(off + ((p - 1) // lb) * lw + (p - 1) % lb) - return fa.read(1).decode().upper() - conf = {} - for l in open(bed): - c, s, e = l.split()[:3] - conf.setdefault(c, []).append((int(s), int(e))) - starts = {c: [s for s, _ in sorted(v)] for c, v in conf.items()} - ends = {c: [e for _, e in sorted(v)] for c, v in conf.items()} - def confident(c, p): - i = bisect.bisect_right(starts.get(c, []), p - 1) - 1 - return i >= 0 and ends[c][i] >= p - covered, head, body = {}, [], [] - for l in gzip.open(truth, "rt"): - if l.startswith("##"): - head.append(l) - continue - if l.startswith("#"): - head.append('##INFO=\n') - head.append('##ALT=\n') - head.append(l) - continue - f = l.split("\t", 5) - p = int(f[1]) - covered.setdefault(f[0], set()).update(range(p, p + len(f[3]))) - body.append(l) - blocks = 0 - with open(sites) as s: - next(s) - for l in s: - _, c, p = l.split("\t")[:3] - p = int(p) - if c not in fai or p in covered.get(c, ()) or not confident(c, p): - continue - b = base(c, p) - if b not in "ACGT": - continue - body.append(f"{c}\t{p}\t.\t{b}\t<*>\t.\t.\tEND={p}\tGT\t0/0\n") - blocks += 1 - with open(out, "w") as o: - o.writelines(head) - o.writelines(body) - print(f"gVCF: {len(body) - blocks} GIAB records, {blocks} reference blocks at panel sites") - PY + /usr/bin/time -v -o "${M}/gvcf.time" bin/og dev evidence hg002-gvcf "${EV}/truth.vcf.gz" "${EV}/truth.bed" \ + "${G}/reference/GRCh38_no_alt_analysis_set.fasta" "${PANEL}/sites_grch38.tsv" "${EV}/hg002.unsorted.vcf" + grep -E 'Maximum resident|Elapsed' "${M}/gvcf.time" docker run --rm -u "$(id -u):$(id -g)" -v "${EV}:/ev" -v "${G}:/g" "$BCFTOOLS_IMAGE" bash -euo pipefail -c ' bcftools sort -m 2G -T /ev/sort -Oz -o /g/HG002/vcf/HG002.g.vcf.gz /ev/hg002.unsorted.vcf bcftools index -t /g/HG002/vcf/HG002.g.vcf.gz' diff --git a/scripts/ci/e2e-case-paths.tsv b/scripts/ci/e2e-case-paths.tsv index 02c026afa..93069dae9 100644 --- a/scripts/ci/e2e-case-paths.tsv +++ b/scripts/ci/e2e-case-paths.tsv @@ -78,7 +78,7 @@ cn-guard-1-mapq0 bin/cn_loci.tsv bin/copy_number.py bin/og bin/traits_report.py cn-intervals-1-catalog bin/cn_loci.tsv bin/og scripts/ci/build-cn-intervals.py scripts/ci/fake-docker/lib.sh cn-intervals-2-hg002 assets/par_grch38.bed assets/paralog_loci.tsv bin/cn_loci.tsv bin/copy_number.py bin/og bin/traits_report.py modules/local/deepvariant/* modules/local/parascopy/* modules/local/vcf_precheck/* scripts/35-paralogs.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh cn-report-1-tables assets/carrier_genes.tsv assets/par_grch38.bed bin/collect_summary.py bin/couple_report.py bin/family_report.py bin/og bin/paraphase_report.py bin/render_report.py bin/results_index.py bin/traits_report.py scripts/24-html-report.sh scripts/39-couple.sh scripts/ci/fake-docker/lib.sh scripts/generate-report.sh -comphet-phasing-1-reads assets/construct_variants.tsv assets/stub/sample.bam assets/stub/sample.bam.bai bin/clip_check.py bin/og modules/local/slivar/* modules/local/vcf_precheck/* modules/local/vep/* scripts/13-vep-annotation.sh scripts/31-slivar.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh tests/fixtures/comphet/make_fixture.py +comphet-phasing-1-reads assets/construct_variants.tsv assets/stub/sample.bam assets/stub/sample.bam.bai bin/clip_check.py bin/og modules/local/slivar/* modules/local/vcf_precheck/* modules/local/vep/* scripts/13-vep-annotation.sh scripts/31-slivar.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh construct-scan-1-both assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/construct_variants.tsv assets/deep_intronic_sites.tsv assets/mito_confirmed.tsv assets/stub/sample.bam assets/stub/sample.bam.bai assets/stub/sample.vcf.gz bin/clip_check.py bin/collect_summary.py bin/og bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/clinvar_screen/* modules/local/html_report/* modules/local/vcf_precheck/* scripts/06-clinvar-screen.sh scripts/24-html-report.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh consumer-export-1 assets/chip_genes.tsv assets/par_grch38.bed bin/chip_report.py bin/og modules/local/chip/* modules/local/export_consumer/* modules/local/vcf_precheck/* scripts/40-export-consumer.sh scripts/49-chip.sh scripts/chip-to-vcf.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh tests/fixtures/consumer_export/sites.tsv.gz containers-software-versions scripts/ci/fake-docker/lib.sh scripts/ci/gen-containers-config.sh diff --git a/scripts/ci/e2e-scope.sh b/scripts/ci/e2e-scope.sh index 4e254dc56..81ddd26d0 100755 --- a/scripts/ci/e2e-scope.sh +++ b/scripts/ci/e2e-scope.sh @@ -46,7 +46,7 @@ export LC_ALL=C SCOPE_DIRS=(scripts modules workflows bin conf assets tests/e2e .github/actions) SCOPE_FILES=(tests/fixtures/VERSION main.nf nextflow.config nextflow_schema.json versions.env .github/workflows/e2e.yml # The committed fixtures the cases read (their rows name them too). - tests/fixtures/annotsv/class45.sh tests/fixtures/comphet/make_fixture.py tests/fixtures/consumer_export/sites.tsv.gz + tests/fixtures/annotsv/class45.sh tests/fixtures/consumer_export/sites.tsv.gz tests/fixtures/families/stub_families.tsv tests/fixtures/pharmcat/refblocks.g.vcf tests/fixtures/sv/cnvpytor.vcf.in tests/fixtures/sv/delly.vcf.in tests/fixtures/sv/manta.vcf.in tests/fixtures/vcf/combined_allele_clinvar.vcf tests/fixtures/vcf/combined_allele_sample.vcf tests/fixtures/vcf/grch37_no_contigs.vcf) diff --git a/tests/e2e/comphet-phasing-1-reads.sh b/tests/e2e/comphet-phasing-1-reads.sh index 2dc4e2ccf..5107b6ca6 100755 --- a/tests/e2e/comphet-phasing-1-reads.sh +++ b/tests/e2e/comphet-phasing-1-reads.sh @@ -2,7 +2,7 @@ # Step 31 phases each compound-het pair from the reads, and the Nextflow # COMPHET_PHASE process writes the same pairs table. # -# The fixture is tests/fixtures/comphet/make_fixture.py: read pairs on a +# The fixture is og dev fixture comphet (tests/fixtures/comphet): read pairs on a # made-up contig (chrP) and a VEP VCF with three pairs, all rare and HIGH: # GENEC SNV at 1000 and a deletion at 1003 on one copy -> cis, 22 reads # GENET SNVs at 5000 and 5400, one on each copy; only a read and its mate @@ -28,7 +28,7 @@ cell() { make_sample() { local s=$1 f="${CASE_TMP}/fixture-$1" rm -rf "$f" "${GENOME_DIR:?}/${s}" - python3 "${REPO}/tests/fixtures/comphet/make_fixture.py" "$f" "${@:2}" + "${REPO}/bin/og" dev fixture comphet "$f" "${@:2}" mkdir -p "${GENOME_DIR}/${s}/vep" "${GENOME_DIR}/${s}/aligned" sed "s/CHPH/${s}/g" "${f}/vep.vcf" > "${GENOME_DIR}/${s}/vep/${s}_vep.vcf" sed "s/CHPH/${s}/g" "${f}/reads.sam" > "${GENOME_DIR}/${s}/aligned/${s}.sam" diff --git a/tests/fixtures/comphet/README.md b/tests/fixtures/comphet/README.md index b0a5816c6..fc17a306e 100644 --- a/tests/fixtures/comphet/README.md +++ b/tests/fixtures/comphet/README.md @@ -1,6 +1,7 @@ # Made-up reads for the compound-het phase (step 31) -`make_fixture.py OUT_DIR [--swap]` writes the fixture: read pairs of 100 bp +`bin/og dev fixture comphet OUT_DIR [--swap]` writes the fixture +(`rust/crates/og-dev/src/fixture.rs`): read pairs of 100 bp from 500 bp fragments on a made-up 80 kb contig (`chrP`, seeded random sequence), a VEP VCF of six heterozygous variants for step 31, and the three pairs as slivar names them. The reads and variants are made up; they describe diff --git a/tests/fixtures/comphet/make_fixture.py b/tests/fixtures/comphet/make_fixture.py deleted file mode 100755 index 38d842fc6..000000000 --- a/tests/fixtures/comphet/make_fixture.py +++ /dev/null @@ -1,150 +0,0 @@ -#!/usr/bin/env python3 -"""Write the compound-het phasing fixture: made-up reads on a made-up contig. - - make_fixture.py OUT_DIR [--swap] - -writes, for the sample CHPH on contig chrP (80 kb of seeded random sequence): - - reads.sam read pairs of 100 bp, 500 bp fragments (mates 400 bp apart) - vep.vcf the six heterozygous variants with a VEP CSQ, as step 13 writes - them, for step 31 (all rare, HIGH impact, three genes) - comphet.vcf the three pairs as slivar compound-hets names them - (INFO/slivar_comphet), for og comphet phase alone - -The three pairs and what the reads show: - - GENEC SNV at 1000, 2-base deletion at 1003: both on copy 1 (cis); single - reads cover both - GENET SNVs at 5000 and 5400, one on each copy (trans); no single read - covers both, only a read and its mate do - GENEF SNVs at 20000 and 70000, one on each copy; 50 kb apart, so no read - pair covers both and the phase cannot be read (unknown) - ---swap moves GENEC's deletion to copy 2: the same positions, now in trans. -The output is the same for the same arguments; nothing is random at run time. -""" -import os -import random -import sys - -SAMPLE = "CHPH" -CONTIG = "chrP" -LENGTH = 80000 -READ = 100 -FRAG = 500 -FIELDS = ("Allele|Consequence|IMPACT|SYMBOL|Gene|Feature_type|Feature|BIOTYPE|CANONICAL|MANE_SELECT|" - "Existing_variation|MAX_AF|gnomADe_AF|gnomADg_AF|CLIN_SIG") - - -def reference(): - rng = random.Random(31) - return "".join(rng.choice("ACGT") for _ in range(LENGTH)) - - -def other(base): - return "ACGT"["ACGT".index(base) - 1] - - -def variants(seq, swap=False): - """[(gene, pos, ref, alt, consequence, copy)]: pos 1-based, copy 0 or 1.""" - snv = lambda pos: (seq[pos - 1], other(seq[pos - 1])) # noqa: E731 - return [ - ("GENEC", 1000, *snv(1000), "stop_gained", 0), - ("GENEC", 1003, seq[1002:1005], seq[1002], "frameshift_variant", 1 if swap else 0), - ("GENET", 5000, *snv(5000), "stop_gained", 0), - ("GENET", 5400, *snv(5400), "stop_gained", 1), - ("GENEF", 20000, *snv(20000), "stop_gained", 0), - ("GENEF", 70000, *snv(70000), "stop_gained", 1), - ] - - -def read_on(seq, vs, copy, start): - """(sequence, cigar) of a READ-base read from 0-based start on one copy.""" - snvs = {v[1] - 1: v[3] for v in vs if v[5] == copy and len(v[2]) == len(v[3])} - dels = {v[1] + len(v[3]) - 1: len(v[2]) - len(v[3]) for v in vs if v[5] == copy and len(v[2]) > len(v[3])} - out, ops, p = [], [], start - while len(out) < READ: - if p in dels: - ops.append(("D", dels[p])) - p += dels[p] - continue - out.append(snvs.get(p, seq[p])) - ops.append(("M", 1)) - p += 1 - cigar, last, n = "", None, 0 - for op, k in ops + [(None, 0)]: - if op == last: - n += k - continue - if last: - cigar += f"{n}{last}" - last, n = op, k - return "".join(out), cigar - - -def sam(seq, vs): - lines = ["@HD\tVN:1.6\tSO:unsorted", f"@SQ\tSN:{CONTIG}\tLN:{LENGTH}", f"@RG\tID:{SAMPLE}\tSM:{SAMPLE}"] - # Fragment starts (0-based) around each pair: read 1 covers the first - # position, its mate the second one when they are 400 bp apart. - starts = {"c": range(920, 991, 7), "t": range(4905, 4996, 10), "f": range(19905, 19996, 10), - "g": range(69905, 69996, 10)} - for region, rs in starts.items(): - for copy in (0, 1): - for s in rs: - name = f"{region}{copy}_{s}" - m = s + FRAG - READ - s1, c1 = read_on(seq, vs, copy, s) - s2, c2 = read_on(seq, vs, copy, m) - q = "I" * READ - lines.append(f"{name}\t99\t{CONTIG}\t{s + 1}\t60\t{c1}\t=\t{m + 1}\t{FRAG}\t{s1}\t{q}\tRG:Z:{SAMPLE}") - lines.append(f"{name}\t147\t{CONTIG}\t{m + 1}\t60\t{c2}\t=\t{s + 1}\t-{FRAG}\t{s2}\t{q}\tRG:Z:{SAMPLE}") - return "\n".join(lines) + "\n" - - -def vcf_head(info): - return ("##fileformat=VCFv4.2\n" - f"##contig=\n" - '##FILTER=\n' - + info - + '##FORMAT=\n' - '##FORMAT=\n' - '##FORMAT=\n' - '##FORMAT=\n' - f"#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\tFORMAT\t{SAMPLE}\n") - - -def vep_vcf(vs): - out = vcf_head('##INFO=\n') - for gene, pos, ref, alt, cons, _ in vs: - csq = (f"{alt}|{cons}|HIGH|{gene}|ENSG{gene}|Transcript|ENST{pos}|protein_coding|YES|NM_{pos}.1|" - f"rs{pos}|0.0001|0.0001|0.0001|") - out += f"{CONTIG}\t{pos}\t.\t{ref}\t{alt}\t50\tPASS\tCSQ={csq}\tGT:GQ:DP:AD\t0/1:50:30:15,15\n" - return out - - -def comphet_vcf(vs): - out = vcf_head('##INFO=\n') - pid = {"GENEC": 1, "GENET": 2, "GENEF": 3} - for i, (gene, pos, ref, alt, _, _) in enumerate(vs): - mate = vs[i + 1] if i % 2 == 0 else vs[i - 1] - tag = f"{SAMPLE}/{gene}/{pid[gene]}/{CONTIG}/{mate[1]}/{mate[2]}/{mate[3]}" - out += f"{CONTIG}\t{pos}\t.\t{ref}\t{alt}\t50\tPASS\tslivar_comphet={tag}\tGT:GQ:DP:AD\t0/1:50:30:15,15\n" - return out - - -def write(out_dir, swap=False): - seq = reference() - vs = variants(seq, swap) - os.makedirs(out_dir, exist_ok=True) - for name, text in (("reads.sam", sam(seq, vs)), ("vep.vcf", vep_vcf(vs)), ("comphet.vcf", comphet_vcf(vs))): - with open(os.path.join(out_dir, name), "w") as fh: - fh.write(text) - return vs - - -if __name__ == "__main__": - args = [a for a in sys.argv[1:] if a != "--swap"] - if len(args) != 1: - sys.exit(__doc__.split("\n\n")[1]) - write(args[0], swap="--swap" in sys.argv[1:]) diff --git a/tests/fixtures/pharmcat/README.md b/tests/fixtures/pharmcat/README.md index a4451df8d..9e15c7b00 100644 --- a/tests/fixtures/pharmcat/README.md +++ b/tests/fixtures/pharmcat/README.md @@ -1,7 +1,7 @@ # PharmCAT report fixtures Real PharmCAT output for `tests/test_cpic_parser.py` (one `report-.json` per release in its `REAL_REPORTS`), trimmed with -`trim_report.py` to the fields `og pgx` reads (every gene with all +`og dev fixture trim-pharmcat` (formerly `trim_report.py`, which wrote the files below) to the fields `og pgx` reads (every gene with all its listed diplotypes and `relatedDrugs`; up to 4 matched annotations per drug guideline). @@ -16,7 +16,7 @@ with `VERSION` replaced by the PharmCAT release (for example `3.4.0`), and add the release to `REAL_REPORTS` in `tests/test_cpic_parser.py`: ```bash -python3 tests/fixtures/pharmcat/trim_report.py report.json tests/fixtures/pharmcat/report-VERSION.json +bin/og dev fixture trim-pharmcat report.json tests/fixtures/pharmcat/report-VERSION.json ``` ## Reference blocks over PharmCAT's positions longer than one base diff --git a/tests/fixtures/pharmcat/trim_report.py b/tests/fixtures/pharmcat/trim_report.py deleted file mode 100755 index 33adcc5a2..000000000 --- a/tests/fixtures/pharmcat/trim_report.py +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env python3 -"""trim_report.py: cut a PharmCAT report.json down to a test fixture. - - python3 tests/fixtures/pharmcat/trim_report.py REPORT.json OUT.json [MAX_ANNOTATIONS] - -Keeps every gene with all its listed diplotypes (sourceDiplotypes, or -recommendationDiplotypes when that is all there is) and relatedDrugs, and for -each drug of each guidance source up to MAX_ANNOTATIONS (default 4) of the -annotations PharmCAT matched, with only the fields og pgx reads. -Writes compact JSON. See README.md for how each fixture was made. -""" -import json -import sys - -src, dst = sys.argv[1], sys.argv[2] -MAX_ANN = int(sys.argv[3]) if len(sys.argv) > 3 else 4 -d = json.load(open(src)) - - -def dip(x): - if not isinstance(x, dict): return x - out = {k: x[k] for k in ("gene", "label", "phenotypes", "activityScore", "outsidePhenotype") if k in x} - for a in ("allele1", "allele2"): - if a in x: - out[a] = {k: x[a][k] for k in ("gene", "name", "function") if x[a] and k in x[a]} if x[a] else x[a] - return out - - -genes = {} -for g, v in d["genes"].items(): - genes[g] = {k: v[k] for k in ("geneSymbol", "chr", "phased", "callSource") if k in v} - genes[g]["relatedDrugs"] = [{"name": r["name"], "id": r.get("id")} for r in v.get("relatedDrugs") or []] - # sourceDiplotypes is what og pgx reads; recommendationDiplotypes only - # when sourceDiplotypes is absent, so it is kept only then. - k = "sourceDiplotypes" if v.get("sourceDiplotypes") else "recommendationDiplotypes" - if k in v: genes[g][k] = [dip(x) for x in v[k] or []] -drugs = {} -for srcname, by in d["drugs"].items(): - for name, rep in by.items(): - gls = [] - for gl in rep.get("guidelines") or []: - anns = [] - for a in (gl.get("annotations") or [])[:MAX_ANN]: - anns.append({"phenotypes": a.get("phenotypes"), "classification": a.get("classification"), - "drugRecommendation": a.get("drugRecommendation"), - "genotypes": [{"diplotypes": [{"gene": x.get("gene"), "label": x.get("label")} - for x in (gt.get("diplotypes") or []) if isinstance(x, dict)]} - for gt in (a.get("genotypes") or [])]}) - if anns: gls.append({"name": gl.get("name"), "source": gl.get("source"), "annotations": anns}) - if gls: - drugs.setdefault(srcname, {})[name] = {"name": rep.get("name"), "id": rep.get("id"), "guidelines": gls} -out = {k: d[k] for k in ("title", "timestamp", "pharmcatVersion", "dataVersion") if k in d} -out["genes"] = genes -out["drugs"] = drugs -out["_fixture_note"] = (f"Trimmed by tests/fixtures/pharmcat/trim_report.py: every gene, and up to {MAX_ANN} matched " - "annotations per drug guideline, with only the fields og pgx reads.") -json.dump(out, open(dst, "w"), separators=(",", ":")) From 7457f1621717f0472580beff12673862a4b23052 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 11:20:24 +0100 Subject: [PATCH 05/45] docs(changelog): og dev fixture, evidence and renovate-check Bead: pgp-bs9u.39 --- CHANGELOG.md | 5 +++++ rust/og/src/main.rs | 1 + 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd24284ac..506e771c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ Each release's notes. From 1.0.0 on, a major or minor release has hand-written n ## Unreleased +- The Python that CI and the fixtures ran outside the pipeline now runs in the Rust `og` binary. The public evidence workflow runs `og dev evidence clashes` and `og dev evidence hg002-gvcf` instead of two inline Python programs; the Renovate dry run checks what Renovate found with `og dev renovate-check`, whose list of non-image pins is now data in `scripts/ci/renovate-pins.tsv`; `og dev fixture comphet` writes the compound-het fixture of the e2e case `comphet-phasing-1-reads`, byte for byte; and `og dev fixture trim-pharmcat` trims a PharmCAT report for `tests/fixtures/pharmcat`. `tests/fixtures/comphet/make_fixture.py`, `tests/fixtures/pharmcat/trim_report.py` and `tests/smoke/vntyper_testdata.py` are gone: VNtyper's two test BAMs now come from `tests/smoke/vntyper_testdata.sh`, one HTTP range request per BAM with curl, checked by gzip against the archive's CRC-32 and size and then against the pinned sha256. On GIAB's HG002 benchmark VCF (4,048,342 records) with the chr20-22 reference, `og dev evidence hg002-gvcf` writes the same gVCF in 3.5 s and 97 MB instead of 11.8 s and 3.8 GB, because it keeps the spans the records cover instead of every covered position. No pipeline output changes. What changes on purpose: + - `og dev evidence hg002-gvcf` skips a panel site past the end of its contig. The Python read the byte after the contig, and at the end of the file wrote a reference block with an empty REF. + - `og dev renovate-check` stops with exit 2 when a pin's file is missing or its glob matches no file. The Python counted such a pin in no file, so it could never fail. + - `og dev fixture trim-pharmcat` names itself in the trimmed report's `_fixture_note`. + - The database re-check and the monthly freshness issue now run in the Rust `og` binary: `og recheck` replaces `bin/owngenome_recheck.py` and `og dev freshness` replaces `scripts/ci/freshness.py`, which is gone. `bin/owngenome_recheck.py` stays until 1.1.0 as a shim that runs `bin/og recheck` with the same arguments and says on stderr that it is deprecated, so a crontab line keeps working; [docs/recheck.md](docs/recheck.md) has the new lines. Each lookup now returns a typed answer (a date, a release, the release a held pin can move to, or "report only"), and the re-check reads that answer, not the text of the report. The lookups run on up to 8 threads with one request at a time per host, and ask the network through the host's `curl`. The freshness report is the same, byte for byte; it took 20 s and 12 MB instead of 109 s and 27 MB on one live run. On a synthetic sample with 5,000 PGS scores the re-check takes 0.05 s and 16 MB instead of 0.56 s and 33 MB, with the same `state.json` by content. What changes on purpose: - The action lines say `bin/og recheck --sample S --rerun` instead of `bin/owngenome_recheck.py --sample S --rerun`. - `--rerun` prints its own lines before the replayed command's output. The Python held its lines in a buffer, so they could come after the child's. diff --git a/rust/og/src/main.rs b/rust/og/src/main.rs index 7b8f792ab..37a109075 100644 --- a/rust/og/src/main.rs +++ b/rust/og/src/main.rs @@ -69,6 +69,7 @@ const USAGE: &str = "usage: og [args] og exomiser report [args] og lookup query|resolve|report [args] og dev parity-compare|contract|schema|report-only|remnants [args] + og dev fixture|evidence|renovate-check [args] og dev freshness [--out FILE] [--sections a,b] | --update-issue BODY --repo O/N | --self-test og version og --tree-hash From 8a8cb25ad55e62ca0aa3ac3fe0c8fec65a212800 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 11:23:02 +0100 Subject: [PATCH 06/45] test(parity): edge cases for og dev fixture trim-pharmcat Every field the trimmer keeps or drops, a non-object diplotype, a null allele, the recommendationDiplotypes fallback, non-ASCII text, and counts of 2, 0 and -1. Bead: pgp-bs9u.39 --- tests/parity/cases.tsv | 4 +++ .../dev-fixture-trim-pharmcat/edge.json | 25 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 tests/parity/inputs/dev-fixture-trim-pharmcat/edge.json diff --git a/tests/parity/cases.tsv b/tests/parity/cases.tsv index 692b0f540..8d2905251 100644 --- a/tests/parity/cases.tsv +++ b/tests/parity/cases.tsv @@ -884,3 +884,7 @@ dev-fixture-trim-pharmcat all-but-last gen:pgx-fixtures.sh pharmcat-docs-example dev-evidence-clashes synthetic dev-evidence-clashes/raw.vcf.gz,dev-evidence-clashes/split.vcf.gz,dev-evidence-clashes/chrA.tsv.gz,dev-evidence-clashes/chrB.tsv.gz,dev-evidence-clashes/chrZ.tsv.gz,dev-evidence-clashes/notes.tsv.gz raw.vcf.gz split.vcf.gz . affected.tsv affected.tsv bytes PYTHON_IMAGE dev-evidence-hg002-gvcf synthetic dev-evidence-hg002-gvcf/truth.vcf.gz,dev-evidence-hg002-gvcf/truth.bed,dev-evidence-hg002-gvcf/ref.fasta,dev-evidence-hg002-gvcf/ref.fasta.fai,dev-evidence-hg002-gvcf/sites.tsv truth.vcf.gz truth.bed ref.fasta sites.tsv out.vcf out.vcf bytes PYTHON_IMAGE dev-evidence-hg002-gvcf past-the-end dev-evidence-hg002-gvcf/truth.vcf.gz,dev-evidence-hg002-gvcf/truth-past-end.bed,dev-evidence-hg002-gvcf/ref.fasta,dev-evidence-hg002-gvcf/ref.fasta.fai,dev-evidence-hg002-gvcf/sites-past-end.tsv truth.vcf.gz truth-past-end.bed ref.fasta sites-past-end.tsv out.vcf out.vcf bytes PYTHON_IMAGE +dev-fixture-trim-pharmcat edge dev-fixture-trim-pharmcat/edge.json edge.json out.json out.json json:mask=_fixture_note PYTHON_IMAGE +dev-fixture-trim-pharmcat edge2 dev-fixture-trim-pharmcat/edge.json edge.json out.json 2 out.json json:mask=_fixture_note PYTHON_IMAGE +dev-fixture-trim-pharmcat edge0 dev-fixture-trim-pharmcat/edge.json edge.json out.json 0 out.json json:mask=_fixture_note PYTHON_IMAGE +dev-fixture-trim-pharmcat edge-all-but-last dev-fixture-trim-pharmcat/edge.json edge.json out.json -1 out.json json:mask=_fixture_note PYTHON_IMAGE diff --git a/tests/parity/inputs/dev-fixture-trim-pharmcat/edge.json b/tests/parity/inputs/dev-fixture-trim-pharmcat/edge.json new file mode 100644 index 000000000..dd46aeb6b --- /dev/null +++ b/tests/parity/inputs/dev-fixture-trim-pharmcat/edge.json @@ -0,0 +1,25 @@ +{"title":"edge é","timestamp":"t","pharmcatVersion":"3.4.0","dataVersion":"d","other":{"x":1}, + "genes":{ + "CYP2D6":{"geneSymbol":"CYP2D6","chr":"chr22","phased":false,"callSource":"OUTSIDE","extra":1, + "relatedDrugs":[{"name":"codeine","id":"PA449088"},{"name":"tramadol"}], + "sourceDiplotypes":[{"gene":"CYP2D6","label":"*1/*4","phenotypes":["Intermediate Metabolizer"],"activityScore":1.0,"outsidePhenotype":true,"lookupKey":["x"], + "allele1":{"gene":"CYP2D6","name":"*1","function":"Normal function","activityValue":"1.0"},"allele2":{"name":"*4","function":"No function"}}, + "not an object"], + "recommendationDiplotypes":[{"label":"ignored because sourceDiplotypes is set"}]}, + "UGT1A1":{"geneSymbol":"UGT1A1","relatedDrugs":null,"sourceDiplotypes":[], + "recommendationDiplotypes":[{"label":"*1/*80","allele1":null,"allele2":{}}]}, + "DPYD":{"geneSymbol":"DPYD","sourceDiplotypes":[]}, + "NAT2":{"geneSymbol":"NAT2","sourceDiplotypes":null,"recommendationDiplotypes":null}}, + "drugs":{ + "CPIC Guideline Annotation":{ + "codeine":{"name":"codeine","id":"PA449088","source":"x","guidelines":[ + {"name":"g1","source":"CPIC","url":"u","annotations":[ + {"phenotypes":{"CYP2D6":"Intermediate Metabolizer"},"classification":"Moderate","drugRecommendation":"Use label-recommended dosing — caution.","implications":["dropped"], + "genotypes":[{"diplotypes":[{"gene":"CYP2D6","label":"*1/*4","x":1},"skip me",{"label":"no gene"}]},{}]}, + {"classification":"Strong"}, + {"classification":"Optional"}]}, + {"name":"g2","annotations":[]}, + {"name":"g3"}]}, + "tramadol":{"name":"tramadol","guidelines":[]}}, + "DPWG Guideline Annotation":{ + "empty":{"name":"empty","guidelines":null}}}} From 63938de53b7bf542246af55ea8ad3a31200c84fb Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 11:23:02 +0100 Subject: [PATCH 07/45] test: plant one fault in each new og dev verb (red control, reverted next) Comphet seeded with 32, the trimmer drops activityScore, clashes needs three records, and renovate-check no longer flags an @ in a package name. Guard rust and parity must go red on this commit. Bead: pgp-bs9u.39 --- rust/crates/og-dev/src/evidence.rs | 2 +- rust/crates/og-dev/src/fixture.rs | 13 ++----------- rust/crates/og-dev/src/renovate.rs | 2 +- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/rust/crates/og-dev/src/evidence.rs b/rust/crates/og-dev/src/evidence.rs index c9b76d7fc..6971f2f1b 100644 --- a/rust/crates/og-dev/src/evidence.rs +++ b/rust/crates/og-dev/src/evidence.rs @@ -119,7 +119,7 @@ fn clash_positions( n += 1; let one = |a: &[u8]| std::str::from_utf8(a).map_or(a.len(), |s| s.chars().count()) == 1; snvs += u32::from(one(c[3]) && one(c[4])); - if n >= 2 && snvs >= 1 { + if n >= 3 && snvs >= 1 { hit.insert(k); } Ok(()) diff --git a/rust/crates/og-dev/src/fixture.rs b/rust/crates/og-dev/src/fixture.rs index bb163c9f9..0d5d9f3bf 100644 --- a/rust/crates/og-dev/src/fixture.rs +++ b/rust/crates/og-dev/src/fixture.rs @@ -200,7 +200,7 @@ pub mod comphet { /// The contig: `random.Random(31)` choosing among `ACGT` 80,000 times. pub fn reference() -> Vec { - let mut rng = Mt19937::new(31); + let mut rng = Mt19937::new(32); (0..LENGTH) .map(|_| b"ACGT"[rng.below(4) as usize]) .collect() @@ -423,16 +423,7 @@ fn diplotype(x: &Value) -> Result { let Value::Object(x) = x else { return Ok(x.clone()); }; - let mut out = pick( - x, - &[ - "gene", - "label", - "phenotypes", - "activityScore", - "outsidePhenotype", - ], - ); + let mut out = pick(x, &["gene", "label", "phenotypes", "outsidePhenotype"]); for a in ["allele1", "allele2"] { let Some(v) = x.get(a) else { continue }; let kept = match v { diff --git a/rust/crates/og-dev/src/renovate.rs b/rust/crates/og-dev/src/renovate.rs index f3355c766..4544ccb01 100644 --- a/rust/crates/og-dev/src/renovate.rs +++ b/rust/crates/og-dev/src/renovate.rs @@ -421,7 +421,7 @@ fn check(deps: &[Dep], env_text: &str, pins: &Pins) -> Vec { } else { ":".to_owned() }; - if text.contains([':', '@']) { + if text.contains(':') { fails.push(format!("names: {f}: package name {}", r(name))); } } From 103471c7dad096ab0d2801232ab9941b6e32e22d Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 11:34:43 +0100 Subject: [PATCH 08/45] test: take the planted faults out again (red control done) This reverts commit 63938de53b7bf542246af55ea8ad3a31200c84fb. Bead: pgp-bs9u.39 --- rust/crates/og-dev/src/evidence.rs | 2 +- rust/crates/og-dev/src/fixture.rs | 13 +++++++++++-- rust/crates/og-dev/src/renovate.rs | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/rust/crates/og-dev/src/evidence.rs b/rust/crates/og-dev/src/evidence.rs index 6971f2f1b..c9b76d7fc 100644 --- a/rust/crates/og-dev/src/evidence.rs +++ b/rust/crates/og-dev/src/evidence.rs @@ -119,7 +119,7 @@ fn clash_positions( n += 1; let one = |a: &[u8]| std::str::from_utf8(a).map_or(a.len(), |s| s.chars().count()) == 1; snvs += u32::from(one(c[3]) && one(c[4])); - if n >= 3 && snvs >= 1 { + if n >= 2 && snvs >= 1 { hit.insert(k); } Ok(()) diff --git a/rust/crates/og-dev/src/fixture.rs b/rust/crates/og-dev/src/fixture.rs index 0d5d9f3bf..bb163c9f9 100644 --- a/rust/crates/og-dev/src/fixture.rs +++ b/rust/crates/og-dev/src/fixture.rs @@ -200,7 +200,7 @@ pub mod comphet { /// The contig: `random.Random(31)` choosing among `ACGT` 80,000 times. pub fn reference() -> Vec { - let mut rng = Mt19937::new(32); + let mut rng = Mt19937::new(31); (0..LENGTH) .map(|_| b"ACGT"[rng.below(4) as usize]) .collect() @@ -423,7 +423,16 @@ fn diplotype(x: &Value) -> Result { let Value::Object(x) = x else { return Ok(x.clone()); }; - let mut out = pick(x, &["gene", "label", "phenotypes", "outsidePhenotype"]); + let mut out = pick( + x, + &[ + "gene", + "label", + "phenotypes", + "activityScore", + "outsidePhenotype", + ], + ); for a in ["allele1", "allele2"] { let Some(v) = x.get(a) else { continue }; let kept = match v { diff --git a/rust/crates/og-dev/src/renovate.rs b/rust/crates/og-dev/src/renovate.rs index 4544ccb01..f3355c766 100644 --- a/rust/crates/og-dev/src/renovate.rs +++ b/rust/crates/og-dev/src/renovate.rs @@ -421,7 +421,7 @@ fn check(deps: &[Dep], env_text: &str, pins: &Pins) -> Vec { } else { ":".to_owned() }; - if text.contains(':') { + if text.contains([':', '@']) { fails.push(format!("names: {f}: package name {}", r(name))); } } From 36123685c1fb8b1ab224f4df15e174430b074ec2 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 12:03:19 +0100 Subject: [PATCH 09/45] test(parity): commit the hg002-gvcf reference inputs .gitignore hid The dev-evidence-hg002-gvcf parity cases read ref.fasta and its .fai, which the *.fasta ignore rule kept out of the tree, so CI parity stopped with 'input not in tests/parity/inputs'. Bead: pgp-bs9u.39 --- tests/parity/inputs/dev-evidence-hg002-gvcf/ref.fasta | 7 +++++++ tests/parity/inputs/dev-evidence-hg002-gvcf/ref.fasta.fai | 2 ++ 2 files changed, 9 insertions(+) create mode 100644 tests/parity/inputs/dev-evidence-hg002-gvcf/ref.fasta create mode 100644 tests/parity/inputs/dev-evidence-hg002-gvcf/ref.fasta.fai diff --git a/tests/parity/inputs/dev-evidence-hg002-gvcf/ref.fasta b/tests/parity/inputs/dev-evidence-hg002-gvcf/ref.fasta new file mode 100644 index 000000000..b37a59afb --- /dev/null +++ b/tests/parity/inputs/dev-evidence-hg002-gvcf/ref.fasta @@ -0,0 +1,7 @@ +>chrA test +ACGTACGTACGTACGTACGT +NNACGTacgtACGTACGTAC +GTACGTACGT +>chrB test +ACNNNTGCAtgcaACGTACG +TACGTACGTA diff --git a/tests/parity/inputs/dev-evidence-hg002-gvcf/ref.fasta.fai b/tests/parity/inputs/dev-evidence-hg002-gvcf/ref.fasta.fai new file mode 100644 index 000000000..9980f0dd1 --- /dev/null +++ b/tests/parity/inputs/dev-evidence-hg002-gvcf/ref.fasta.fai @@ -0,0 +1,2 @@ +chrA 50 11 20 21 +chrB 30 75 20 21 From 06213bd908cfe31e96fd03fd77b6eafc11e4cc70 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 12:27:54 +0100 Subject: [PATCH 10/45] test(parity): commit the past-the-end golden VCF .gitignore hid The *.vcf ignore rule kept the golden out.vcf of dev-evidence-hg002-gvcf/past-the-end out of the tree, so CI compared og with the oracle and reported the deliberate fix as a difference. Bead: pgp-bs9u.39 --- .../dev-evidence-hg002-gvcf/past-the-end/out.vcf | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 tests/parity/golden/dev-evidence-hg002-gvcf/past-the-end/out.vcf diff --git a/tests/parity/golden/dev-evidence-hg002-gvcf/past-the-end/out.vcf b/tests/parity/golden/dev-evidence-hg002-gvcf/past-the-end/out.vcf new file mode 100644 index 000000000..1e494f38c --- /dev/null +++ b/tests/parity/golden/dev-evidence-hg002-gvcf/past-the-end/out.vcf @@ -0,0 +1,11 @@ +##fileformat=VCFv4.2 +##contig= +##contig= +##INFO= +##ALT= +#CHROM POS ID REF ALT QUAL FILTER INFO FORMAT HG002 +chrA 3 . G T 50 PASS . GT 0/1 +chrA 10 . CGT C 50 PASS . GT 0/1 +chrB 8 . C T 50 PASS . GT 0/1 +chrC 1 . A G 50 PASS . GT 0/1 +chrB 30 . A <*> . . END=30 GT 0/0 From 3ff62369089236db926da5458bb15ef4fad0f78f Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 13:13:27 +0100 Subject: [PATCH 11/45] test(ci): cover HG002 reference blocks and scope the Rust fixture Select comphet-phasing-1-reads when its Rust generator changes. Keep the push paths and scope list aligned. Check HG002 truth records and the last valid reference block directly against the committed golden. Bead: pgp-bs9u.39 --- .github/workflows/e2e.yml | 1 + rust/crates/og-dev/src/evidence.rs | 47 ++++++++++++++++++++++++++++++ scripts/ci/e2e-case-paths.tsv | 2 +- scripts/ci/e2e-scope.sh | 1 + 4 files changed, 50 insertions(+), 1 deletion(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 39d4b272d..b395a6ad0 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -65,6 +65,7 @@ on: - 'nextflow_schema.json' - 'versions.env' - '.github/workflows/e2e.yml' + - 'rust/crates/og-dev/src/fixture.rs' - '.github/actions/**' - 'tests/fixtures/annotsv/class45.sh' - 'tests/fixtures/consumer_export/sites.tsv.gz' diff --git a/rust/crates/og-dev/src/evidence.rs b/rust/crates/og-dev/src/evidence.rs index c9b76d7fc..84529a8ef 100644 --- a/rust/crates/og-dev/src/evidence.rs +++ b/rust/crates/og-dev/src/evidence.rs @@ -343,6 +343,53 @@ fn hg002_gvcf( mod tests { use super::*; + #[test] + fn hg002_gvcf_keeps_truth_and_adds_only_valid_reference_blocks() { + let repo = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../.."); + let inputs = repo.join("tests/parity/inputs/dev-evidence-hg002-gvcf"); + let temp = std::env::temp_dir().join(format!( + "og-evidence-gvcf-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + fs::create_dir(&temp).unwrap(); + let output = temp.join("out.vcf"); + let args = [ + "hg002-gvcf".to_owned(), + inputs.join("truth.vcf.gz").to_str().unwrap().to_owned(), + inputs + .join("truth-past-end.bed") + .to_str() + .unwrap() + .to_owned(), + inputs.join("ref.fasta").to_str().unwrap().to_owned(), + inputs + .join("sites-past-end.tsv") + .to_str() + .unwrap() + .to_owned(), + output.to_str().unwrap().to_owned(), + ]; + let (mut out, mut err) = (Vec::new(), Vec::new()); + assert_eq!(cli(&args, &mut out, &mut err), 0, "{err:?}"); + assert_eq!( + out, + b"gVCF: 4 GIAB records, 1 reference blocks at panel sites\n" + ); + assert!(err.is_empty()); + let actual = fs::read(&output).unwrap(); + fs::remove_dir_all(&temp).unwrap(); + assert_eq!( + actual, + include_bytes!( + "../../../../tests/parity/golden/dev-evidence-hg002-gvcf/past-the-end/out.vcf" + ) + ); + } + #[test] fn spans_merge_and_answer_inside() { let m = merge(vec![(10, 12), (5, 6), (11, 15), (15, 16)]); diff --git a/scripts/ci/e2e-case-paths.tsv b/scripts/ci/e2e-case-paths.tsv index 17e9c8f6d..d01f1b348 100644 --- a/scripts/ci/e2e-case-paths.tsv +++ b/scripts/ci/e2e-case-paths.tsv @@ -78,7 +78,7 @@ cn-guard-1-mapq0 bin/cn_loci.tsv bin/copy_number.py bin/og bin/traits_report.py cn-intervals-1-catalog bin/cn_loci.tsv bin/og scripts/ci/build-cn-intervals.py scripts/ci/fake-docker/lib.sh cn-intervals-2-hg002 assets/par_grch38.bed assets/paralog_loci.tsv bin/cn_loci.tsv bin/copy_number.py bin/og bin/traits_report.py modules/local/deepvariant/* modules/local/parascopy/* modules/local/vcf_precheck/* scripts/35-paralogs.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh cn-report-1-tables assets/carrier_genes.tsv assets/par_grch38.bed bin/collect_summary.py bin/couple_report.py bin/family_report.py bin/og bin/paraphase_report.py bin/render_report.py bin/results_index.py bin/traits_report.py scripts/24-html-report.sh scripts/39-couple.sh scripts/ci/fake-docker/lib.sh scripts/generate-report.sh -comphet-phasing-1-reads assets/construct_variants.tsv assets/stub/sample.bam assets/stub/sample.bam.bai bin/clip_check.py bin/og modules/local/slivar/* modules/local/vcf_precheck/* modules/local/vep/* scripts/13-vep-annotation.sh scripts/31-slivar.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh +comphet-phasing-1-reads assets/construct_variants.tsv assets/stub/sample.bam assets/stub/sample.bam.bai bin/clip_check.py bin/og modules/local/slivar/* modules/local/vcf_precheck/* modules/local/vep/* scripts/13-vep-annotation.sh scripts/31-slivar.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh rust/crates/og-dev/src/fixture.rs construct-scan-1-both assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/construct_variants.tsv assets/deep_intronic_sites.tsv assets/mito_confirmed.tsv assets/stub/sample.bam assets/stub/sample.bam.bai assets/stub/sample.vcf.gz bin/clip_check.py bin/collect_summary.py bin/og bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/clinvar_screen/* modules/local/html_report/* modules/local/vcf_precheck/* scripts/06-clinvar-screen.sh scripts/24-html-report.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh consumer-export-1 assets/chip_genes.tsv assets/par_grch38.bed bin/chip_report.py bin/og modules/local/chip/* modules/local/export_consumer/* modules/local/vcf_precheck/* scripts/40-export-consumer.sh scripts/49-chip.sh scripts/chip-to-vcf.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh tests/fixtures/consumer_export/sites.tsv.gz containers-software-versions scripts/ci/fake-docker/lib.sh scripts/ci/gen-containers-config.sh diff --git a/scripts/ci/e2e-scope.sh b/scripts/ci/e2e-scope.sh index 81ddd26d0..b3305168d 100755 --- a/scripts/ci/e2e-scope.sh +++ b/scripts/ci/e2e-scope.sh @@ -45,6 +45,7 @@ export LC_ALL=C # runs no case. The push paths of e2e.yml are the same list (e2e-plan checks). SCOPE_DIRS=(scripts modules workflows bin conf assets tests/e2e .github/actions) SCOPE_FILES=(tests/fixtures/VERSION main.nf nextflow.config nextflow_schema.json versions.env .github/workflows/e2e.yml + rust/crates/og-dev/src/fixture.rs # The committed fixtures the cases read (their rows name them too). tests/fixtures/annotsv/class45.sh tests/fixtures/consumer_export/sites.tsv.gz tests/fixtures/families/stub_families.tsv tests/fixtures/pharmcat/refblocks.g.vcf tests/fixtures/sv/cnvpytor.vcf.in From 63e44b68022e77d44ad14d6e3fc5392bdc2ada9b Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 13:13:41 +0100 Subject: [PATCH 12/45] test: plant a wrong HG002 reference genotype for the red control Guard must reject 1/1 reference blocks in evidence::tests::hg002_gvcf_keeps_truth_and_adds_only_valid_reference_blocks. Revert this planted fault after recording its red CI job. Bead: pgp-bs9u.39 --- rust/crates/og-dev/src/evidence.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/crates/og-dev/src/evidence.rs b/rust/crates/og-dev/src/evidence.rs index 84529a8ef..44651f17a 100644 --- a/rust/crates/og-dev/src/evidence.rs +++ b/rust/crates/og-dev/src/evidence.rs @@ -332,7 +332,7 @@ fn hg002_gvcf( } blocks += 1; w.write_all(c) - .and_then(|()| writeln!(w, "\t{p}\t.\t{}\t<*>\t.\t.\tEND={p}\tGT\t0/0", b as char)) + .and_then(|()| writeln!(w, "\t{p}\t.\t{}\t<*>\t.\t.\tEND={p}\tGT\t1/1", b as char)) .map_err(werr) })?; w.flush().map_err(werr)?; From d3a7a9da7c02c76788c6098b2dab392cc79f4b07 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 13:13:56 +0100 Subject: [PATCH 13/45] test: restore the HG002 reference genotype after the red control Remove the deliberately wrong 1/1 reference genotype. Keep the direct golden-file regression test. Bead: pgp-bs9u.39 --- rust/crates/og-dev/src/evidence.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/crates/og-dev/src/evidence.rs b/rust/crates/og-dev/src/evidence.rs index 44651f17a..84529a8ef 100644 --- a/rust/crates/og-dev/src/evidence.rs +++ b/rust/crates/og-dev/src/evidence.rs @@ -332,7 +332,7 @@ fn hg002_gvcf( } blocks += 1; w.write_all(c) - .and_then(|()| writeln!(w, "\t{p}\t.\t{}\t<*>\t.\t.\tEND={p}\tGT\t1/1", b as char)) + .and_then(|()| writeln!(w, "\t{p}\t.\t{}\t<*>\t.\t.\tEND={p}\tGT\t0/0", b as char)) .map_err(werr) })?; w.flush().map_err(werr)?; From ea5a440af6ff8b2fce808b337557ebbcdd67fbfb Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 13:31:52 +0100 Subject: [PATCH 14/45] docs(cli): list each new dev verb on its own usage line Bead: pgp-bs9u.39 --- rust/og/src/main.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rust/og/src/main.rs b/rust/og/src/main.rs index 529c1ac2d..841fcc8ef 100644 --- a/rust/og/src/main.rs +++ b/rust/og/src/main.rs @@ -74,7 +74,11 @@ const USAGE: &str = "usage: og [args] og exomiser report [args] og lookup query|resolve|report [args] og dev parity-compare|contract|schema|report-only|remnants [args] - og dev fixture|evidence|renovate-check [args] + og dev fixture comphet OUT [--swap] + og dev fixture trim-pharmcat REPORT OUT [MAX] + og dev evidence clashes RAW.vcf.gz SPLIT.vcf.gz DATA_DIR OUT.tsv + og dev evidence hg002-gvcf TRUTH.vcf.gz TRUTH.bed REF.fasta SITES.tsv OUT.vcf + og dev renovate-check LOG og dev freshness [--out FILE] [--sections a,b] | --update-issue BODY --repo O/N | --self-test og version og --tree-hash From 454720de11d709b3f7d3541a24c37c882e48f35d Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 14:47:04 +0100 Subject: [PATCH 15/45] fix(ci): move trace and smoke readers to Rust Bead: pgp-bs9u.38 --- .github/workflows/nextflow.yml | 133 +------- CHANGELOG.md | 2 + docs/rust.md | 8 + rust/README.md | 2 + rust/crates/og-dev/src/lib.rs | 8 +- rust/crates/og-dev/src/nf_trace.rs | 316 +++++++++++++++++ rust/crates/og-dev/src/pharmcat.rs | 126 +++++++ rust/crates/og-dev/src/smoke.rs | 322 ++++++++++++++++++ rust/crates/og-dev/tests/ci_readers.rs | 294 ++++++++++++++++ rust/og/src/main.rs | 3 + scripts/ci/e2e-case-paths.tsv | 1 + scripts/ci/e2e-case-times.tsv | 1 + scripts/ci/image-smoke.sh | 112 ++---- scripts/ci/nf-trace-requests.tsv | 10 + scripts/ci/parity-diff.sh | 32 +- scripts/ci/settle-doubts.sh | 7 +- tests/e2e/31-pharmcat-07.sh | 4 +- tests/e2e/60-nextflow.sh | 2 +- tests/e2e/lib.sh | 41 +-- tests/e2e/nextflow-from-fastq-2-nextflow.sh | 2 +- tests/e2e/pgx-6-gvcf-column.sh | 2 +- tests/e2e/rust-z2e-readers.sh | 18 + tests/e2e/vendor-vcf-intake-5-docs.sh | 2 +- tests/parity/README.md | 2 + tests/parity/cases.tsv | 16 + tests/parity/gen/ci-smoke.sh | 4 + tests/parity/inputs/ci-readers/attempts.tsv | 3 + tests/parity/inputs/ci-readers/cells.csv | 4 + tests/parity/inputs/ci-readers/empty.json | 1 + tests/parity/inputs/ci-readers/flat.json | 1 + tests/parity/inputs/ci-readers/nested.json | 1 + tests/parity/inputs/ci-readers/oracle.sh | 34 ++ tests/parity/inputs/ci-readers/requests.tsv | 19 ++ tests/parity/inputs/ci-readers/truncated.json | 1 + tests/parity/inputs/ci-readers/want.tsv | 10 + tests/parity/oracle.ref | 9 + tests/smoke/commands.tsv | 20 +- 37 files changed, 1289 insertions(+), 284 deletions(-) create mode 100644 rust/crates/og-dev/src/nf_trace.rs create mode 100644 rust/crates/og-dev/src/pharmcat.rs create mode 100644 rust/crates/og-dev/src/smoke.rs create mode 100644 rust/crates/og-dev/tests/ci_readers.rs create mode 100644 scripts/ci/nf-trace-requests.tsv create mode 100755 tests/e2e/rust-z2e-readers.sh create mode 100755 tests/parity/gen/ci-smoke.sh create mode 100644 tests/parity/inputs/ci-readers/attempts.tsv create mode 100644 tests/parity/inputs/ci-readers/cells.csv create mode 100644 tests/parity/inputs/ci-readers/empty.json create mode 100644 tests/parity/inputs/ci-readers/flat.json create mode 100644 tests/parity/inputs/ci-readers/nested.json create mode 100644 tests/parity/inputs/ci-readers/oracle.sh create mode 100644 tests/parity/inputs/ci-readers/requests.tsv create mode 100644 tests/parity/inputs/ci-readers/truncated.json create mode 100644 tests/parity/inputs/ci-readers/want.tsv diff --git a/.github/workflows/nextflow.yml b/.github/workflows/nextflow.yml index 8f5426c6c..d4aaf9022 100644 --- a/.github/workflows/nextflow.yml +++ b/.github/workflows/nextflow.yml @@ -119,8 +119,8 @@ jobs: - name: Validate nextflow_schema.json and the samplesheet schema if: steps.scope.outputs.run == 'true' run: | - python3 -m json.tool nextflow_schema.json > /dev/null - python3 -m json.tool assets/schema_input.json > /dev/null + jq empty nextflow_schema.json > /dev/null + jq empty assets/schema_input.json > /dev/null # The stub run below pulls every image of the default tools: the pinned # leg ran out of disk in it twice on 2026-11-05 (#300, job 127077180393; @@ -430,83 +430,10 @@ jobs: nextflow run main.nf -profile test_all -stub -ansi-log false -c "${RUNNER_TEMP}/resources.config" \ --tools vep,manta,cnvpytor,prs --max_cpus 16 --max_memory 64.GB --max_time 48.h \ --outdir "${RUNNER_TEMP}/resources" -work-dir "${RUNNER_TEMP}/resources-work" - python3 - "${RUNNER_TEMP}"/resources/pipeline_info/trace_*.txt "$exec_cpus" "$exec_mem_gb" <<'PY' - import csv, sys - GB, H = 1024 ** 3, 3600 * 1000 - exec_cpus, exec_mem = int(sys.argv[2]), int(sys.argv[3]) * GB - # process: (cpus, memory, time) of the first attempt - want = { - 'DEEPVARIANT': (8, 32 * GB, 48 * H), - 'FASTP': (8, 8 * GB, 8 * H), - 'ALIGN_MINIMAP2': (8, 32 * GB, 24 * H), - 'MANTA': (8, 16 * GB, 24 * H), - 'CNVPYTOR': (4, 8 * GB, 24 * H), - 'VEP': (8, 16 * GB, 24 * H), - 'PRS_PREPARE': (1, 2 * GB, 2 * H), - 'PRS_SCORE_SITES': (2, 4 * GB, 4 * H), - 'PRS': (4, 24 * GB, 8 * H), - } - seen, bad = {p: {} for p in want}, 0 - attempts = all_cpus = all_mem = 0 - rows = {} - with open(sys.argv[1]) as fh: - for row in csv.DictReader(fh, delimiter='\t'): - first_row = rows.setdefault(row['task_id'], row) - if first_row is not row: - # The late copy can be built before the exit status was - # set, so it may read '-' there; nothing else may differ. - exits = {first_row['exit'], row['exit']} - {'-'} - if len(exits) > 1 or any(first_row[k] != row[k] for k in row if k != 'exit'): - print(f"FAIL: task {row['task_id']} has two different rows in the trace: {first_row} and {row}") - bad += 1 - else: - print(f"{row['name']}: attempt {row['attempt']} is written twice in the trace; counted once") - continue - attempts += 1 - all_cpus += int(row['cpus']) - all_mem += int(row['memory']) - proc = row['name'].split(' (')[0].split(':')[-1] - if proc not in want: - continue - got = (int(row['cpus']), int(row['memory']), int(row['time'])) - print(f"{row['name']}: attempt {row['attempt']}, exit {row['exit']}, " - f"cpus {got[0]}, memory {got[1] / GB:g} GB, time {got[2] / H:g} h") - seen[proc].setdefault(int(row['attempt']), {}).setdefault(row['name'], []).append(got) - for proc, (cpus, mem, time) in want.items(): - first, second = seen[proc].get(1, {}), seen[proc].get(2, {}) - unexpected = sorted(set(seen[proc]) - {1, 2}) - if unexpected: - print(f"FAIL {proc}: attempt(s) {unexpected} in the trace, want only a first attempt and one retry") - bad += 1 - if not first: - print(f"FAIL {proc}: no first attempt in the trace") - bad += 1 - continue - for name in sorted(set(first) | set(second)): - n1, n2 = len(first.get(name, [])), len(second.get(name, [])) - if n1 != 1 or n2 != 1: - print(f"FAIL {name}: {n1} first and {n2} second attempts in the trace, want one of each") - bad += 1 - for got in (g for gs in first.values() for g in gs): - if got != (cpus, mem, time): - print(f"FAIL {proc}: first attempt asks for cpus {got[0]}, memory {got[1] / GB:g} GB, " - f"time {got[2] / H:g} h; want {cpus}, {mem / GB:g} GB, {time / H:g} h") - bad += 1 - for got in (g for gs in second.values() for g in gs): - if got[1] != 2 * mem: - print(f"FAIL {proc}: the retry asks for {got[1] / GB:g} GB, want {2 * mem / GB:g} GB") - bad += 1 - # Every attempt takes its request once and gives it back once. If - # the executor has at least the sum, losing every release still - # leaves room for the next task. - print(f"{attempts} task attempts ask for {all_cpus} CPUs and {all_mem / GB:g} GB added together; " - f"the executor is told it has {exec_cpus} CPUs and {exec_mem / GB:g} GB") - if all_cpus > exec_cpus or all_mem > exec_mem: - print("FAIL: executor.cpus or executor.memory of this step is below that sum; raise exec_cpus / exec_mem_gb") - bad += 1 - print(f"{bad} failure(s)") - sys.exit(1 if bad else 0) - PY + traces=("${RUNNER_TEMP}"/resources/pipeline_info/trace_*.txt) + [ "${#traces[@]}" -eq 1 ] && [ -f "${traces[0]}" ] || { echo "FAIL: expected exactly one resource trace"; exit 2; } + rust/target/release/og dev nf-trace requests "${traces[0]}" --want scripts/ci/nf-trace-requests.tsv \ + --executor-cpus "$exec_cpus" --executor-mem-gb "$exec_mem_gb" # The step above fakes the kill with beforeScript, so it cannot see how # ALIGN_MINIMAP2's own pipe reports one. This runs the module's real @@ -574,28 +501,8 @@ jobs: --max_cpus 16 --max_memory 64.GB --outdir "$t/out" -work-dir "$t/work" || rc=$? echo "nextflow exited ${rc}" cat "$t/trace.txt" - python3 - "$t/trace.txt" "$rc" <<'PY' - import csv, sys - GB = 1024 ** 3 - # One row per task_id: the trace can list a finished task twice - # (the retry step above says why), and the first row is the one - # written when the task completed. - # A repeated row must match the first; only its exit may read '-'. - rows = {} - for r in csv.DictReader(open(sys.argv[1]), delimiter='\t'): - first = rows.setdefault(r['task_id'], r) - exits = {first['exit'], r['exit']} - {'-'} - if len(exits) > 1 or any(first[k] != r[k] for k in r if k != 'exit'): - print(f"FAIL: task {r['task_id']} has two different rows in the trace: {first} and {r}") - sys.exit(1) - rows = [r for r in rows.values() if 'ALIGN_MINIMAP2' in r['name']] - got = [(int(r['attempt']), r['exit'], int(r['memory']) // GB) for r in rows] - want = [(1, '137', 32), (2, '0', 64)] - print(f"ALIGN_MINIMAP2 attempts (attempt, exit, GB): {got}; want {want}") - ok = sorted(got) == want and sys.argv[2] == '0' - print('OK' if ok else 'FAIL: a kill inside the pipe did not reach Nextflow as exit 137 and a retry') - sys.exit(0 if ok else 1) - PY + rust/target/release/og dev nf-trace attempts "$t/trace.txt" --name-glob '*ALIGN_MINIMAP2*' \ + --want '1:137:32,2:0:64' --nextflow-exit "$rc" # PRS runs pgsc_calc, a Nextflow pipeline of its own, and gives it the # task's memory as --max_memory, the most one pgsc_calc task may have. A @@ -696,26 +603,10 @@ jobs: --max_cpus 16 --max_memory 64.GB --outdir "$t/out" -work-dir "$t/work" || rc=$? echo "nextflow exited ${rc}" cat "$t/trace.txt" - python3 - "$t/trace.txt" "$rc" <<'PY' - import csv, sys - GB = 1024 ** 3 - # One row per task_id, as in the ALIGN_MINIMAP2 step above. - rows = {} - for r in csv.DictReader(open(sys.argv[1]), delimiter='\t'): - first = rows.setdefault(r['task_id'], r) - exits = {first['exit'], r['exit']} - {'-'} - if len(exits) > 1 or any(first[k] != r[k] for k in r if k != 'exit'): - print(f"FAIL: task {r['task_id']} has two different rows in the trace: {first} and {r}") - sys.exit(1) - rows = list(rows.values()) - want, ok = [(1, '137', 24), (2, '0', 48)], sys.argv[2] == '0' - for sample in ('sigkill', 'pool'): - got = sorted((int(r['attempt']), r['exit'], int(r['memory']) // GB) for r in rows if r['name'] == f'PRS ({sample})') - print(f"PRS ({sample}) attempts (attempt, exit, GB): {got}; want {want}") - ok = ok and got == want - print('OK' if ok else 'FAIL: a memory kill inside pgsc_calc did not reach Nextflow as exit 137 and a retry that passes') - sys.exit(0 if ok else 1) - PY + for sample in sigkill pool; do + rust/target/release/og dev nf-trace attempts "$t/trace.txt" --name-glob "PRS ($sample)" \ + --want '1:137:24,2:0:48' --nextflow-exit "$rc" + done # A Nextflow run that fails, or that its step's timeout cuts, leaves its # log in the checkout as .nextflow.log (older runs of the job as diff --git a/CHANGELOG.md b/CHANGELOG.md index cd74b71a6..5ba164c75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Each release's notes. From 1.0.0 on, a major or minor release has hand-written n ## Unreleased +- CI readers now use `og dev pharmcat diplotypes`, `nf-trace` and `smoke`. Unreadable PharmCAT reports fail with exit 2 and are marked not assessed. Resource checks refuse trace globs and malformed numeric cells. The smoke mini-reference uses a fixed Rust seed for the planted bases. Gzip smoke reads have deterministic headers. Smoke table checks report concise property results. + - The container helpers check (`scripts/ci/check-container-helpers.sh`, Guard job `helper-binaries`) now derives its table with the Rust `og` binary, `og dev helpers`. It replaces the Python inside the script, which is gone; the script keeps the docker probe, the orad download and its self-test. On this tree the table and the rule 1 output are the same, byte for byte. The static run takes 0.01 s and 6 MB where the Python took 0.2 s and 15 MB. What changes: - A helper called in a process's `shell:` block is checked too. The Python read only `script:` blocks, so such a call went unchecked. No module has one today. - A helper called in a process's `exec:` block always fails the check: Nextflow runs that block on the host, where no image applies. diff --git a/docs/rust.md b/docs/rust.md index c2ed48f65..bea0896ec 100644 --- a/docs/rust.md +++ b/docs/rust.md @@ -111,3 +111,11 @@ To add a primitive: write it in `pycompat` with unit tests, add an operation to - **Release** (`release.yml`): a tag builds the four binaries, writes `SHA256SUMS`, attests them and attaches all five files to the release. A pull request that changes `rust/` or the workflow builds them as a dry run. [Testing](testing.md) has the rest of CI. + +## CI readers + +`og dev pharmcat diplotypes REPORT` prints sorted, unique `GENE\tA1/A2` rows from flat or source-nested reports. `--called` prints the called count and names on stderr. `--called-lines` prints one called gene per line for e2e checks. An unreadable report exits 2 without a count. A valid empty report succeeds with a zero count. + +`og dev nf-trace requests TRACE --want FILE --executor-cpus N --executor-mem-gb N` checks initial requests, doubled retry memory and the executor budget. The expectation table is [nf-trace-requests.tsv](../scripts/ci/nf-trace-requests.tsv). `og dev nf-trace attempts TRACE --name-glob PATTERN --want '1:137:32,2:0:64' --nextflow-exit RC` checks a retry sequence. Both accept one explicit path, refuse globs, parse quoted TSV records and reject conflicting duplicates. Identical duplicate task IDs count once. Exit 1 means the check found a mismatch. Exit 2 means input or arguments could not be assessed. + +`og dev smoke` supplies `mini-ref SRC ORIG MINI POS LEN`, `long-reads HAP1 HAP2 OUT.gz`, `eh-catalog REF OUT END`, `csv-cell FILE KEY=VALUE[,KEY=VALUE] COLUMN` and `check KIND FILE`. Check kinds are `hap-ibd`, `ibdmix`, `rfmix`, `eigen` and `parascopy`. Mini-reference bases use a fixed Rust seed. Long-read gzip headers are deterministic. These synthetic fixtures do not change pipeline output files. Smoke references are limited to 1,003,000 bases. CSV lookups and table checks read one record at a time. diff --git a/rust/README.md b/rust/README.md index 394e83e10..40089ce54 100644 --- a/rust/README.md +++ b/rust/README.md @@ -34,3 +34,5 @@ cargo build --release && ../tests/rust/pycompat-diff.sh target/release/og # py ``` A binary from a plain `cargo build` is stamped `unknown`, and `rust/og.sh check` refuses it: only `scripts/setup.sh --og` stamps the tree hash the pipeline checks. + +CI readers also run through `og dev pharmcat diplotypes`, `og dev nf-trace requests|attempts` and `og dev smoke`. See [the Rust command notes](../docs/rust.md#ci-readers). diff --git a/rust/crates/og-dev/src/lib.rs b/rust/crates/og-dev/src/lib.rs index 49938910c..1222b0f2e 100644 --- a/rust/crates/og-dev/src/lib.rs +++ b/rust/crates/og-dev/src/lib.rs @@ -18,16 +18,19 @@ pub mod contract; pub mod helpers; +pub mod nf_trace; pub mod parity; +pub mod pharmcat; pub mod remnants; pub mod report_only; pub mod schema; pub mod shlex; +pub mod smoke; mod zip; use std::io::Write; -const USAGE: &str = "usage: og dev parity-compare KIND ORACLE NEW\n og dev parity-compare --self-test\n og dev contract --static [--repo DIR] [--contract FILE]\n og dev contract --outputs DIR --leg bash|nextflow --sample S [--observe]\n og dev schema SCHEMA.json DOC.json\n og dev report-only [--repo DIR]\n og dev remnants [--list FILE] [--root DIR]\n og dev helpers --list|--static|--images|--orad-calls [--root DIR]\n og dev helpers --orad --help-file FILE [--orad-version V] [--root DIR]\n"; +const USAGE: &str = "usage: og dev parity-compare KIND ORACLE NEW\n og dev parity-compare --self-test\n og dev contract --static [--repo DIR] [--contract FILE]\n og dev contract --outputs DIR --leg bash|nextflow --sample S [--observe]\n og dev schema SCHEMA.json DOC.json\n og dev report-only [--repo DIR]\n og dev remnants [--list FILE] [--root DIR]\n og dev pharmcat diplotypes REPORT [--called|--called-lines]\n og dev nf-trace requests|attempts TRACE [options]\n og dev smoke mini-ref|long-reads|eh-catalog|csv-cell|check [args]\n og dev helpers --list|--static|--images|--orad-calls [--root DIR]\n og dev helpers --orad --help-file FILE [--orad-version V] [--root DIR]\n"; /// Python's `str(OSError)`: `[Errno 2] No such file or directory: 'path'`. pub(crate) fn py_os_error(e: &std::io::Error, path: &str) -> String { @@ -62,6 +65,9 @@ pub fn main(args: &[String]) -> u8 { Some("contract") => run(contract::cli, &args[1..]), Some("schema") => run(schema::cli, &args[1..]), Some("report-only") => run(report_only::cli, &args[1..]), + Some("pharmcat") => run(pharmcat::cli, &args[1..]), + Some("nf-trace") => run(nf_trace::cli, &args[1..]), + Some("smoke") => run(smoke::cli, &args[1..]), Some("helpers") => run(helpers::cli, &args[1..]), Some("-h" | "--help") => { print!("{USAGE}"); diff --git a/rust/crates/og-dev/src/nf_trace.rs b/rust/crates/og-dev/src/nf_trace.rs new file mode 100644 index 000000000..ac9441b72 --- /dev/null +++ b/rust/crates/og-dev/src/nf_trace.rs @@ -0,0 +1,316 @@ +//! Strict checks over raw Nextflow traces. Only one explicit trace path is accepted. +use pycompat::csv::{Quoting, Reader}; +use std::collections::{BTreeMap, BTreeSet}; +use std::io::Write; + +const GB: u64 = 1 << 30; +const H: u64 = 3_600_000; +type Record = BTreeMap; + +fn records(path: &str, mut f: impl FnMut(Record) -> Result<(), String>) -> Result<(), String> { + if path.contains(['*', '?', '[', ']']) { + return Err("trace glob refused: pass exactly one explicit trace path".into()); + } + let lines = super::smoke::csv_lines(path)?; + let mut csv = Reader::new(lines, '\t', Quoting::Minimal); + let header = csv + .next() + .ok_or("no trace header")? + .map_err(|e| e.to_string())?; + for row in csv { + let row = row.map_err(|e| e.to_string())?; + if row.is_empty() { + continue; + } + if row.len() != header.len() { + return Err(format!("{path}: row width does not match header")); + } + f(header.iter().cloned().zip(row).collect())?; + } + Ok(()) +} +fn cell<'a>(r: &'a Record, k: &str) -> Result<&'a str, String> { + r.get(k) + .map(String::as_str) + .ok_or_else(|| format!("missing column {k}")) +} +fn num(r: &Record, k: &str) -> Result { + cell(r, k)? + .parse() + .map_err(|_| format!("column {k} is not an unsigned integer")) +} +fn traces(path: &str, out: &mut dyn Write) -> Result, String> { + let mut rows: Vec = Vec::new(); + let mut ids = BTreeMap::new(); + records(path, |r| { + let id = cell(&r, "task_id")?.to_owned(); + if let Some(&i) = ids.get(&id) { + let first: &Record = &rows[i]; + let a = cell(first, "exit")?; + let b = cell(&r, "exit")?; + if (a != "-" && b != "-" && a != b) + || r.iter() + .any(|(k, v)| k != "exit" && first.get(k) != Some(v)) + { + return Err(format!("task {id} has two different rows in the trace")); + } + writeln!( + out, + "{}: attempt {} is written twice in the trace; counted once", + cell(&r, "name")?, + cell(&r, "attempt")? + ) + .map_err(|e| e.to_string())?; + } else { + ids.insert(id, rows.len()); + rows.push(r); + } + Ok(()) + })?; + if rows.is_empty() { + return Err("no trace rows".into()); + } + Ok(rows) +} +fn options(args: &[String]) -> Result, String> { + if !args.len().is_multiple_of(2) { + return Err("expected named option and value; pass exactly one trace path".into()); + } + let mut opts = BTreeMap::new(); + for pair in args.as_chunks::<2>().0 { + if !pair[0].starts_with("--") || opts.insert(pair[0].as_str(), pair[1].as_str()).is_some() { + return Err("unexpected or repeated argument".into()); + } + } + Ok(opts) +} +fn opt<'a>(opts: &BTreeMap<&str, &'a str>, key: &str) -> Result<&'a str, String> { + opts.get(key) + .copied() + .ok_or_else(|| format!("{key} is required")) +} +fn glob(pattern: &str, text: &str) -> bool { + // Escaping makes punctuation literal. Only '*' and '?' have glob meaning. + let re = regex::escape(pattern) + .replace("\\*", ".*") + .replace("\\?", "."); + regex::Regex::new(&format!("(?s)^{re}$")).is_ok_and(|r| r.is_match(text)) +} +fn attempts(path: &str, opts: &BTreeMap<&str, &str>, out: &mut dyn Write) -> Result { + for k in opts.keys() { + if !["--name-glob", "--want", "--nextflow-exit"].contains(k) { + return Err(format!("unknown option {k}")); + } + } + let pattern = opt(opts, "--name-glob")?; + let want: Vec<(u64, String, u64)> = opt(opts, "--want")? + .split(',') + .map(|t| { + let parts: Vec<_> = t.split(':').collect(); + if parts.len() != 3 { + return Err("--want must be attempt:exit:GB,...".into()); + } + Ok(( + parts[0].parse().map_err(|_| "bad attempt")?, + parts[1].to_owned(), + parts[2].parse().map_err(|_| "bad memory")?, + )) + }) + .collect::>()?; + let mut got = Vec::new(); + for r in traces(path, out)? { + if glob(pattern, cell(&r, "name")?) { + got.push(( + num(&r, "attempt")?, + cell(&r, "exit")?.to_owned(), + num(&r, "memory")? / GB, + )); + } + } + got.sort(); + let tuples = |xs: &[(u64, String, u64)]| { + format!( + "[{}]", + xs.iter() + .map(|(a, e, m)| format!("({a}, '{e}', {m})")) + .collect::>() + .join(", ") + ) + }; + let label = if pattern.contains("ALIGN_MINIMAP2") { + "ALIGN_MINIMAP2" + } else { + pattern + }; + writeln!( + out, + "{label} attempts (attempt, exit, GB): {}; want {}", + tuples(&got), + tuples(&want) + ) + .map_err(|e| e.to_string())?; + let ok = got == want && opt(opts, "--nextflow-exit")? == "0"; + writeln!( + out, + "{}", + if ok { + "OK" + } else { + "FAIL: a memory kill did not reach Nextflow as exit 137 and a retry that passes" + } + ) + .map_err(|e| e.to_string())?; + Ok(ok) +} +fn requests(path: &str, opts: &BTreeMap<&str, &str>, out: &mut dyn Write) -> Result { + for k in opts.keys() { + if !["--want", "--executor-cpus", "--executor-mem-gb"].contains(k) { + return Err(format!("unknown option {k}")); + } + } + let mut want = BTreeMap::new(); + records(opt(opts, "--want")?, |r| { + let name = cell(&r, "process")?.to_owned(); + let v = ( + num(&r, "cpus")?, + num(&r, "memory_gb")? + .checked_mul(GB) + .ok_or("memory overflow")?, + num(&r, "time_h")?.checked_mul(H).ok_or("time overflow")?, + ); + if want.insert(name, v).is_some() { + return Err("duplicate process in --want".into()); + } + Ok(()) + })?; + if want.is_empty() { + return Err("empty request expectations".into()); + } + let rows = traces(path, out)?; + type Seen<'a> = BTreeMap<&'a str, BTreeMap>>>; + let mut seen: Seen<'_> = BTreeMap::new(); + let mut all_cpus = 0u64; + let mut all_mem = 0u64; + let mut bad = 0; + for r in &rows { + let cpus = num(r, "cpus")?; + let mem = num(r, "memory")?; + all_cpus = all_cpus.checked_add(cpus).ok_or("CPU sum overflow")?; + all_mem = all_mem.checked_add(mem).ok_or("memory sum overflow")?; + let name = cell(r, "name")?; + let proc = name + .split(" (") + .next() + .unwrap_or(name) + .rsplit(':') + .next() + .unwrap_or(name); + if !want.contains_key(proc) { + continue; + } + let attempt = num(r, "attempt")?; + let time = num(r, "time")?; + writeln!( + out, + "{name}: attempt {attempt}, exit {}, cpus {cpus}, memory {} GB, time {} h", + cell(r, "exit")?, + mem / GB, + time / H + ) + .map_err(|e| e.to_string())?; + seen.entry(proc) + .or_default() + .entry(attempt) + .or_default() + .entry(name) + .or_default() + .push((cpus, mem, time)); + } + for (proc, &expected) in &want { + let attempts = seen.get(proc.as_str()); + if attempts.is_some_and(|a| a.keys().any(|k| ![1, 2].contains(k))) { + writeln!(out, "FAIL {proc}: want only a first attempt and one retry") + .map_err(|e| e.to_string())?; + bad += 1; + } + let first = attempts.and_then(|a| a.get(&1)); + let second = attempts.and_then(|a| a.get(&2)); + if first.is_none_or(|a| a.is_empty()) { + writeln!(out, "FAIL {proc}: no first attempt in the trace") + .map_err(|e| e.to_string())?; + bad += 1; + continue; + } + let names: BTreeSet<_> = first + .into_iter() + .chain(second) + .flat_map(|a| a.keys()) + .collect(); + for name in names { + let a = first.and_then(|a| a.get(name)).map_or(0, Vec::len); + let b = second.and_then(|a| a.get(name)).map_or(0, Vec::len); + if a != 1 || b != 1 { + writeln!( + out, + "FAIL {name}: {a} first and {b} second attempts in the trace, want one of each" + ) + .map_err(|e| e.to_string())?; + bad += 1; + } + } + for got in first.into_iter().flat_map(|a| a.values()).flatten() { + if *got != expected { + writeln!(out,"FAIL {proc}: first attempt asks for cpus {}, memory {} GB, time {} h; want {}, {} GB, {} h",got.0,got.1/GB,got.2/H,expected.0,expected.1/GB,expected.2/H).map_err(|e| e.to_string())?; + bad += 1; + } + } + for got in second.into_iter().flat_map(|a| a.values()).flatten() { + if Some(got.1) != expected.1.checked_mul(2) { + writeln!( + out, + "FAIL {proc}: the retry asks for {} GB, want {} GB", + got.1 / GB, + expected.1 / GB * 2 + ) + .map_err(|e| e.to_string())?; + bad += 1; + } + } + } + let cpus: u64 = opt(opts, "--executor-cpus")? + .parse() + .map_err(|_| "invalid executor CPUs")?; + let mem: u64 = opt(opts, "--executor-mem-gb")? + .parse() + .map_err(|_| "invalid executor memory")?; + writeln!(out,"{} task attempts ask for {all_cpus} CPUs and {} GB added together; the executor is told it has {cpus} CPUs and {mem} GB",rows.len(),all_mem/GB).map_err(|e| e.to_string())?; + if all_cpus > cpus || Some(all_mem) > mem.checked_mul(GB) { + writeln!(out,"FAIL: executor.cpus or executor.memory of this step is below that sum; raise exec_cpus / exec_mem_gb").map_err(|e| e.to_string())?; + bad += 1; + } + writeln!(out, "{bad} failure(s)").map_err(|e| e.to_string())?; + Ok(bad == 0) +} + +/// Checks `requests TRACE ...` or `attempts TRACE ...`. +pub fn cli(args: &[String], out: &mut dyn Write, err: &mut dyn Write) -> u8 { + let result = (|| { + if args.len() < 2 { + return Err("usage: og dev nf-trace requests|attempts TRACE [options]".into()); + } + let opts = options(&args[2..])?; + match args[0].as_str() { + "requests" => requests(&args[1], &opts, out), + "attempts" => attempts(&args[1], &opts, out), + _ => Err("unknown nf-trace verb".into()), + } + })(); + match result { + Ok(true) => 0, + Ok(false) => 1, + Err(e) => { + let _ = writeln!(err, "FAIL: {e}"); + 2 + } + } +} diff --git a/rust/crates/og-dev/src/pharmcat.rs b/rust/crates/og-dev/src/pharmcat.rs new file mode 100644 index 000000000..10301dc3d --- /dev/null +++ b/rust/crates/og-dev/src/pharmcat.rs @@ -0,0 +1,126 @@ +//! The single reader of PharmCAT's flat and source-nested gene maps. +use pycompat::json::{self, Value}; +use std::collections::BTreeSet; +use std::io::Write; + +trait Get { + fn get(&self, key: &str) -> Option<&Value>; +} +impl Get for Value { + fn get(&self, key: &str) -> Option<&Value> { + if let Value::Object(o) = self { + o.get(key) + } else { + None + } + } +} + +fn diplotypes(g: &Value) -> Result<&[Value], String> { + let empty = &[][..]; + let pick = ["sourceDiplotypes", "recommendationDiplotypes"] + .into_iter() + .filter_map(|k| g.get(k)) + .find(|v| pycompat::value::truthy(v)); + match pick { + None => Ok(empty), + Some(Value::Array(a)) => Ok(a), + _ => Err("diplotypes must be an array".into()), + } +} + +/// All diplotypes and the genes with a named allele. Invalid structure is +/// unassessed, just like invalid JSON, and cannot produce an empty success. +pub fn read(data: &Value) -> Result<(BTreeSet, BTreeSet), String> { + let Value::Object(root) = data else { + return Err("report must be an object".into()); + }; + let mut rows = BTreeSet::new(); + let mut called = BTreeSet::new(); + let mut gene = |name: &str, g: &Value| -> Result<(), String> { + for d in diplotypes(g)? { + if !matches!(d, Value::Object(_)) { + return Err("diplotype must be an object".into()); + } + let mut names = Vec::with_capacity(2); + for a in ["allele1", "allele2"] { + let n = match d.get(a) { + None | Some(Value::Null) => "", + Some(Value::Object(o)) => match o.get("name") { + None | Some(Value::Null) => "", + Some(Value::Str(s)) => s, + _ => return Err("allele name must be a string".into()), + }, + _ => return Err("allele must be an object".into()), + }; + names.push(n); + } + rows.insert(format!("{name}\t{}", names.join("/"))); + if names.iter().any(|n| { + !n.is_empty() && !["unknown", "none", "?"].contains(&n.to_lowercase().as_str()) + }) { + called.insert(name.to_owned()); + } + } + Ok(()) + }; + match root.get("genes") { + None | Some(Value::Null) => {} + Some(Value::Object(genes)) => { + for (key, val) in genes { + let Value::Object(map) = val else { continue }; + if map.contains_key("sourceDiplotypes") + || map.contains_key("recommendationDiplotypes") + { + gene(key, val)?; + } else { + for (name, g) in map { + if matches!(g, Value::Object(_)) { + gene(name, g)?; + } + } + } + } + } + _ => return Err("genes must be an object".into()), + } + Ok((rows, called)) +} + +/// `og dev pharmcat diplotypes REPORT [--called|--called-lines]`. +pub fn cli(args: &[String], out: &mut dyn Write, err: &mut dyn Write) -> u8 { + let valid = args.first().is_some_and(|s| s == "diplotypes") + && (args.len() == 2 + || (args.len() == 3 && ["--called", "--called-lines"].contains(&args[2].as_str()))); + if !valid { + let _ = writeln!( + err, + "usage: og dev pharmcat diplotypes REPORT [--called|--called-lines]" + ); + return 2; + } + let result = std::fs::read_to_string(&args[1]) + .map_err(|e| e.to_string()) + .and_then(|s| json::loads(&s).map_err(|e| e.to_string())) + .and_then(|v| read(&v)); + let (rows, called) = match result { + Ok(r) => r, + Err(e) => { + let _ = writeln!(err, "unreadable report: {e}"); + return 2; + } + }; + let result = match args.get(2).map(String::as_str) { + Some("--called") => writeln!( + err, + "{}", + called.iter().cloned().collect::>().join(" ") + ) + .and_then(|()| writeln!(out, "{}", called.len())), + Some("--called-lines") => { + writeln!(out, "{}", called.into_iter().collect::>().join("\n")) + } + _ => writeln!(out, "{}", rows.into_iter().collect::>().join("\n")), + }; + if result.is_ok() { 0 } else { 2 } +} diff --git a/rust/crates/og-dev/src/smoke.rs b/rust/crates/og-dev/src/smoke.rs new file mode 100644 index 000000000..41d2c80b8 --- /dev/null +++ b/rust/crates/og-dev/src/smoke.rs @@ -0,0 +1,322 @@ +//! Small, local smoke fixtures and table checks. No subprocess or network. +use og_io::{AtomicFile, Lines, Utf8}; +use pycompat::csv::{DictReader, Quoting}; +use std::io::Write; + +fn lines(path: &str) -> Result>, String> { + Ok(Lines::new( + og_io::open(path).map_err(|e| format!("{path}: {e}"))?, + Utf8::Strict, + )) +} +pub(crate) fn csv_lines( + path: &str, +) -> Result>, String> { + let mut input = lines(path)?; + Ok(std::iter::from_fn(move || { + let mut line = String::new(); + match input.read_line(&mut line) { + Ok(true) => Some(Ok(line)), + Ok(false) => None, + Err(e) => Some(Err(e)), + } + })) +} +fn fasta(path: &str) -> Result, String> { + let mut seq = Vec::new(); + for line in lines(path)? { + let line = line.map_err(|e| e.to_string())?; + if !line.starts_with('>') { + seq.extend(line.trim().bytes().map(|b| b.to_ascii_uppercase())); + } + } + if seq.len() > 1_003_000 { + return Err("smoke reference exceeds 1003000 bases".into()); + } + if !seq.is_ascii() { + return Err("FASTA sequence is not ASCII".into()); + } + Ok(seq) +} +fn write_fasta(path: &str, seq: &[u8]) -> Result<(), String> { + let mut out = AtomicFile::create(path).map_err(|e| e.to_string())?; + out.write_all(b">chr20\n").map_err(|e| e.to_string())?; + for chunk in seq.chunks(60) { + out.write_all(chunk) + .and_then(|()| out.write_all(b"\n")) + .map_err(|e| e.to_string())?; + } + out.commit().map_err(|e| e.to_string()) +} +fn number(s: &str) -> Result { + s.parse().map_err(|_| format!("invalid integer: {s}")) +} +fn mini(args: &[String]) -> Result<(), String> { + if args.len() != 5 { + return Err("mini-ref SRC ORIG MINI POS LEN".into()); + } + let mut seq = fasta(&args[0])?; + if seq.len() != 500_000 { + return Err(format!("mini-ref needs 500000 bases, got {}", seq.len())); + } + let pos = number(&args[3])?; + let n = number(&args[4])?; + if pos > seq.len() || n > 1_000_000 { + return Err("plant outside bounded mini reference".into()); + } + write_fasta(&args[1], &seq)?; + // Stable, portable xorshift64 seed. These bases are deliberately independent + // of CPython's RNG. They only create a deletion in reads of the original. + let mut state = 20u64; + let insert = (0..n).map(|_| { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + b"ACGT"[(state & 3) as usize] + }); + seq.splice(pos..pos, insert); + write_fasta(&args[2], &seq) +} +fn long_reads(args: &[String], stdout: &mut dyn Write) -> Result<(), String> { + if args.len() != 3 { + return Err("long-reads HAP1 HAP2 OUT.gz".into()); + } + let mut out = AtomicFile::create(&args[2]).map_err(|e| e.to_string())?; + let mut gz = flate2::GzBuilder::new() + .mtime(0) + .write(&mut out, flate2::Compression::default()); + let mut k = 0; + for (h, path) in args[..2].iter().enumerate() { + let seq = fasta(path)?; + if seq.len() < 10000 { + continue; + } + for start in (0..=seq.len() - 10000).step_by(1000) { + let mut r = seq[start..start + 10000].to_vec(); + if k % 2 == 1 { + r.reverse(); + for b in &mut r { + *b = match b { + b'A' => b'T', + b'C' => b'G', + b'G' => b'C', + b'T' => b'A', + _ => *b, + }; + } + } + writeln!(gz, "@hap{}_{start}", h + 1) + .and_then(|()| gz.write_all(&r)) + .and_then(|()| gz.write_all(b"\n+\n")) + .and_then(|()| gz.write_all(&[b'?'; 10000])) + .and_then(|()| gz.write_all(b"\n")) + .map_err(|e| e.to_string())?; + k += 1; + } + } + gz.finish().map_err(|e| e.to_string())?; + out.commit().map_err(|e| e.to_string())?; + writeln!(stdout, "{k} reads").map_err(|e| e.to_string()) +} +fn catalog(args: &[String], stdout: &mut dyn Write) -> Result<(), String> { + if args.len() != 3 { + return Err("eh-catalog REF OUT END".into()); + } + let seq = fasta(&args[0])?; + let end = number(&args[2])?; + let re = regex::bytes::Regex::new("(?:CA){12,}").expect("constant regex"); + let m = re + .find_iter(&seq) + .find(|m| m.start() > 20000 && m.end().checked_add(20000).is_some_and(|n| n < end)) + .ok_or("no suitable CA repeat")?; + let data = format!( + "[\n {{\n \"LocusId\": \"SMOKE1\",\n \"LocusStructure\": \"(CA)*\",\n \"ReferenceRegion\": \"chr20:{}-{}\",\n \"VariantType\": \"Repeat\"\n }}\n]", + m.start(), + m.end() + ); + let mut out = AtomicFile::create(&args[1]).map_err(|e| e.to_string())?; + out.write_all(data.as_bytes()).map_err(|e| e.to_string())?; + out.commit().map_err(|e| e.to_string())?; + writeln!( + stdout, + "repeat {} {} {} units", + m.start(), + m.end(), + m.len() / 2 + ) + .map_err(|e| e.to_string()) +} +fn csv_cell(args: &[String], out: &mut dyn Write) -> Result<(), String> { + if args.len() != 3 { + return Err("csv-cell FILE KEY=VALUE[,KEY=VALUE] COLUMN".into()); + } + let want = args[1] + .split(',') + .map(|kv| kv.split_once('=').ok_or("expected KEY=VALUE")) + .collect::, _>>()?; + for row in DictReader::new(csv_lines(&args[0])?, ',', Quoting::Minimal) { + let row = row.map_err(|e| e.to_string())?; + if want.iter().all(|(k, v)| row.get(k) == Some(Some(v))) { + let val = row.get(&args[2]).flatten().ok_or("missing CSV column")?; + writeln!(out, "{val}").map_err(|e| e.to_string())?; + break; + } + } + Ok(()) +} +fn fields(line: &str) -> Vec<&str> { + line.split_whitespace().collect() +} +fn float(s: Option<&&str>) -> Result { + pycompat::num::parse_float(s.ok_or("short table row")?).map_err(|e| e.to_string()) +} +fn check(kind: &str, path: &str, out: &mut dyn Write) -> Result { + // The windows keep only their first covering row, matching Python's next(). + let mut n = 0; + let mut ab = 0; + let mut ab_len = 0.; + let mut coverage = 0.; + let mut outside = 0; + let mut groups = [Vec::new(), Vec::new(), Vec::new()]; + let mut codes = std::collections::BTreeMap::new(); + let mut at = [None, None]; + let mut aggregate = false; + for (i, line) in lines(path)?.enumerate() { + let line = line.map_err(|e| e.to_string())?; + let r = fields(&line); + match kind { + "hap-ibd" => { + if r.len() < 8 { + return Err("short hap-ibd row".into()); + } + n += 1; + if (r[0] == "A" && r[2] == "B") || (r[0] == "B" && r[2] == "A") { + ab += 1; + ab_len = float(r.get(7))?; + } + } + "ibdmix" => { + if i == 0 { + continue; + } + if r.first() == Some(&"S") + && float(r.get(4))? >= 4. + && float(r.get(3))? - float(r.get(2))? >= 50000. + { + let a = float(r.get(2))?; + let b = float(r.get(3))?; + n += 1; + coverage += (b.min(2600000.) - a.max(2000000.)).max(0.); + if b <= 2000000. || a >= 2600000. { + outside += 1; + } + } + } + "eigen" => { + if line.trim_start().starts_with('#') || r.is_empty() { + continue; + } + let g = match r.last() { + Some(&"PopA") => 0, + Some(&"PopB") => 1, + Some(&"Proj") => 2, + _ => continue, + }; + groups[g].push(float(r.get(1))?); + } + "rfmix" => { + if i == 0 { + for pair in line + .split_once(':') + .ok_or("missing population codes")? + .1 + .split_whitespace() + { + let (k, v) = pair.split_once('=').ok_or("bad population code")?; + codes.insert(k.to_owned(), v.to_owned()); + } + } + if line.starts_with('#') { + continue; + } + let a = float(r.get(1))?; + let b = float(r.get(2))?; + n += 1; + for (j, p) in [5000000., 15000000.].iter().enumerate() { + if at[j].is_none() && a <= *p && *p < b { + at[j] = Some([ + r.get(6).ok_or("short RFMix row")?.to_string(), + r.get(7).ok_or("short RFMix row")?.to_string(), + ]); + } + } + } + "parascopy" => { + if r.is_empty() || r[0].starts_with('#') { + continue; + } + if r.len() < 8 { + return Err("short Parascopy row".into()); + } + aggregate |= + r[0] == "chr5" && !["", "*"].contains(&r[6]) && !["", "*"].contains(&r[7]); + n += 1; + } + _ => return Err(format!("unknown smoke check {kind}")), + } + } + let ok = match kind { + "hap-ibd" => n == 1 && ab == 1 && (8. ..=11.).contains(&ab_len), + "ibdmix" => coverage >= 540000. && outside == 0, + "eigen" => { + if groups.iter().any(Vec::is_empty) { + return Err("missing PCA group".into()); + } + let a = &groups[0]; + let b = &groups[1]; + let p = groups[2][0]; + let ma = pycompat::num::sum(a.iter().copied()) / a.len() as f64; + let mb = pycompat::num::sum(b.iter().copied()) / b.len() as f64; + (a.iter().copied().fold(f64::NEG_INFINITY, f64::max) + < b.iter().copied().fold(f64::INFINITY, f64::min) + || a.iter().copied().fold(f64::INFINITY, f64::min) + > b.iter().copied().fold(f64::NEG_INFINITY, f64::max)) + && (p - ma).abs() < (p - mb).abs() + } + "rfmix" => ["POPA", "POPB"].iter().enumerate().all(|(i, p)| { + codes + .get(*p) + .is_some_and(|c| at[i].as_ref() == Some(&[c.clone(), c.clone()])) + }), + "parascopy" => aggregate, + _ => false, + }; + writeln!(out, "{kind}: {n} rows; {}", if ok { "OK" } else { "FAIL" }) + .map_err(|e| e.to_string())?; + Ok(ok) +} + +/// Runs a smoke generator, CSV lookup or table assertion. +pub fn cli(args: &[String], out: &mut dyn Write, err: &mut dyn Write) -> u8 { + let result = (|| { + let (verb, args) = args.split_first().ok_or("smoke verb required")?; + match verb.as_str() { + "mini-ref" => mini(args).map(|()| true), + "long-reads" => long_reads(args, out).map(|()| true), + "eh-catalog" => catalog(args, out).map(|()| true), + "csv-cell" => csv_cell(args, out).map(|()| true), + "check" if args.len() == 2 => check(&args[0], &args[1], out), + _ => { + Err("usage: og dev smoke mini-ref|long-reads|eh-catalog|csv-cell|check ...".into()) + } + } + })(); + match result { + Ok(true) => 0, + Ok(false) => 1, + Err(e) => { + let _ = writeln!(err, "ERROR: {e}"); + 2 + } + } +} diff --git a/rust/crates/og-dev/tests/ci_readers.rs b/rust/crates/og-dev/tests/ci_readers.rs new file mode 100644 index 000000000..72b8f8db8 --- /dev/null +++ b/rust/crates/og-dev/tests/ci_readers.rs @@ -0,0 +1,294 @@ +//! Black-box controls for the CI readers. Every clean fixture is paired with +//! a changed or unreadable input that must fail. +use og_dev::{nf_trace, pharmcat, smoke}; +use std::path::PathBuf; +use std::sync::atomic::{AtomicUsize, Ordering}; +static NEXT: AtomicUsize = AtomicUsize::new(0); +struct Temp(PathBuf); +impl Temp { + fn new() -> Self { + let p = std::env::temp_dir().join(format!( + "og-ci-readers-{}-{}", + std::process::id(), + NEXT.fetch_add(1, Ordering::Relaxed) + )); + std::fs::create_dir_all(&p).unwrap(); + Self(p) + } + fn file(&self, name: &str, text: &str) -> String { + let p = self.0.join(name); + std::fs::write(&p, text).unwrap(); + p.to_str().unwrap().into() + } + fn path(&self, name: &str) -> String { + self.0.join(name).to_str().unwrap().into() + } +} +impl Drop for Temp { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } +} +type Cli = fn(&[String], &mut dyn std::io::Write, &mut dyn std::io::Write) -> u8; +fn run(cli: Cli, args: &[&str]) -> (u8, String, String) { + let mut out = Vec::new(); + let mut err = Vec::new(); + let code = cli( + &args.iter().map(|s| s.to_string()).collect::>(), + &mut out, + &mut err, + ); + ( + code, + String::from_utf8(out).unwrap(), + String::from_utf8(err).unwrap(), + ) +} +#[test] +fn unreadable_pharmcat_is_unassessed() { + let t = Temp::new(); + for p in [ + t.path("missing"), + t.file("broken", "{\"genes\":"), + t.file("wrong", "[]"), + t.file("bad-shape", "{\"genes\": []}"), + ] { + for flag in ["--called", "--called-lines"] { + let (rc, out, err) = run(pharmcat::cli, &["diplotypes", &p, flag]); + assert_eq!(rc, 2, "{err}"); + assert!(out.is_empty()); + assert!(err.starts_with("unreadable report:")); + } + } + let p = t.file("empty", "{\"genes\": {}}"); + assert_eq!( + run(pharmcat::cli, &["diplotypes", &p, "--called"]), + (0, "0\n".into(), "\n".into()) + ); +} +#[test] +fn pharmcat_shapes_fallback_and_deduplication() { + let t = Temp::new(); + let p=t.file("r",r#"{"genes":{"CPIC":{"B":{"sourceDiplotypes":[],"recommendationDiplotypes":[{"allele1":{"name":"*1"}}]},"A":{"sourceDiplotypes":[{"allele1":{"name":"Unknown"},"allele2":{"name":"?"}}]}},"DPWG":{"B":{"sourceDiplotypes":[{"allele1":{"name":"*1"}}]}},"C":{"sourceDiplotypes":[{"allele1":{"name":"NONE"}}]}}}"#); + assert_eq!( + run(pharmcat::cli, &["diplotypes", &p]), + (0, "A\tUnknown/?\nB\t*1/\nC\tNONE/\n".into(), "".into()) + ); + assert_eq!( + run(pharmcat::cli, &["diplotypes", &p, "--called"]), + (0, "1\n".into(), "B\n".into()) + ); + let flat = t.file( + "flat", + r#"{"genes":{"B":{"sourceDiplotypes":[{"allele1":{"name":"*1"}}]}}}"#, + ); + assert_eq!( + run(pharmcat::cli, &["diplotypes", &flat, "--called-lines"]), + (0, "B\n".into(), "".into()) + ); +} +const TRACE: &str = "\"task_id\"\tname\tattempt\texit\tmemory\tcpus\ttime\n1\tX:ALIGN_MINIMAP2 (s)\t1\t137\t34359738368\t8\t86400000\n2\tX:ALIGN_MINIMAP2 (s)\t2\t0\t68719476736\t8\t86400000\n"; +#[test] +fn trace_attempts_and_duplicate_controls() { + let t = Temp::new(); + let p = t.file("trace", TRACE); + let args = |p: &str| { + run( + nf_trace::cli, + &[ + "attempts", + p, + "--name-glob", + "*ALIGN_MINIMAP2*", + "--want", + "1:137:32,2:0:64", + "--nextflow-exit", + "0", + ], + ) + }; + assert_eq!(args(&p).0, 0); + let duplicate = t.file( + "dup", + &format!("{TRACE}1\tX:ALIGN_MINIMAP2 (s)\t1\t-\t34359738368\t8\t86400000\n"), + ); + assert_eq!(args(&duplicate).0, 0); + for (name, text) in [ + ("memory", TRACE.replace("68719476736", "34359738368")), + ("exit", TRACE.replace("\t137\t", "\t1\t")), + ( + "missing", + TRACE.lines().take(2).collect::>().join("\n"), + ), + ("empty", String::new()), + ("dash", TRACE.replace("68719476736", "-")), + ( + "conflict", + format!("{TRACE}1\tX:ALIGN_MINIMAP2 (s)\t1\t0\t34359738368\t8\t86400000\n"), + ), + ] { + assert_ne!(args(&t.file(name, &text)).0, 0, "{name}"); + } + assert_eq!(args("trace_*.txt").0, 2); + assert_eq!(run(nf_trace::cli, &["attempts", &p, &p]).0, 2); +} +#[test] +fn trace_requests_and_budget_controls() { + let t = Temp::new(); + let p = t.file("trace", TRACE); + let want = t.file( + "want", + "process\tcpus\tmemory_gb\ttime_h\nALIGN_MINIMAP2\t8\t32\t24\n", + ); + let args = |p: &str, cpus: &str, mem: &str| { + run( + nf_trace::cli, + &[ + "requests", + p, + "--want", + &want, + "--executor-cpus", + cpus, + "--executor-mem-gb", + mem, + ], + ) + }; + assert_eq!(args(&p, "16", "96").0, 0); + assert_eq!(args(&p, "15", "96").0, 1); + assert_eq!(args(&p, "16", "95").0, 1); + for (name, text) in [ + ("cpus", TRACE.replace("\t8\t", "\t4\t")), + ("time", TRACE.replace("86400000", "3600000")), + ("third", TRACE.replace("\t2\t0\t", "\t3\t0\t")), + ( + "duplicate-name", + format!("{TRACE}3\tX:ALIGN_MINIMAP2 (s)\t1\t137\t34359738368\t8\t86400000\n"), + ), + ] { + assert_eq!(args(&t.file(name, &text), "512", "2048").0, 1, "{name}"); + } +} +#[test] +fn quoted_csv_cell_and_missing_column() { + let t = Temp::new(); + let p = t.file( + "csv", + "Type,Filter,Value\r\n\"S,N\",PASS,\"a,b\nnext\"\r\nSNP,PASS,0.9\r\n", + ); + assert_eq!( + run( + smoke::cli, + &["csv-cell", &p, "Type=SNP,Filter=PASS", "Value"] + ), + (0, "0.9\n".into(), "".into()) + ); + assert_eq!( + run(smoke::cli, &["csv-cell", &p, "Filter=PASS", "Value"]).1, + "a,b\nnext\n" + ); + assert_eq!( + run(smoke::cli, &["csv-cell", &p, "Filter=PASS", "missing"]).0, + 2 + ); +} +#[test] +fn smoke_tables_reject_removed_properties() { + let t = Temp::new(); + for (kind, good, bad) in [ + ( + "hap-ibd", + "A 1 B 1 20 1 10000000 10\n", + "A 1 C 1 20 1 10000000 10\n", + ), + ( + "ibdmix", + "h\nS 22 2000000 2600000 4\n", + "h\nS 22 2000000 2300000 4\n", + ), + ( + "rfmix", + "#codes: POPA=0 POPB=1\n#h\n20 0 10000000 0 0 0 0 0\n20 10000000 20000000 0 0 0 1 1\n", + "#codes: POPA=0 POPB=1\n20 0 20000000 0 0 0 0 0\n", + ), + ( + "eigen", + "#h\na -2 0 PopA\nb 2 0 PopB\np -1 0 Proj\n", + "#h\na -2 0 PopA\nb 2 0 PopB\np 1 0 Proj\n", + ), + ( + "parascopy", + "chr5 1 2 a b c 4 99\n", + "chr5 1 2 a b c * 99\n", + ), + ] { + let p = t.file("table", good); + assert_eq!(run(smoke::cli, &["check", kind, &p]).0, 0, "{kind}"); + let p = t.file("table", bad); + assert_eq!(run(smoke::cli, &["check", kind, &p]).0, 1, "{kind}"); + } +} +#[test] +fn smoke_generators_keep_coordinates_and_reverse_complements() { + let t = Temp::new(); + let seq = format!( + "{}{}{}", + "A".repeat(21000), + "CA".repeat(12), + "G".repeat(478976) + ); + let src = t.file("src", &format!(">slice\n{seq}\n")); + let orig = t.path("orig"); + let mini = t.path("mini"); + assert_eq!( + run( + smoke::cli, + &["mini-ref", &src, &orig, &mini, "450000", "3000"] + ) + .0, + 0 + ); + let fa = std::fs::read_to_string(&mini).unwrap(); + let s = fa.lines().skip(1).collect::(); + assert_eq!(s.len(), 503000); + assert_eq!(&s[..450000], &seq[..450000]); + assert_eq!(&s[453000..], &seq[450000..]); + let cat = t.path("cat"); + assert_eq!( + run(smoke::cli, &["eh-catalog", &mini, &cat, "449000"]).1, + "repeat 21000 21024 12 units\n" + ); + assert!( + std::fs::read_to_string(&cat) + .unwrap() + .contains("chr20:21000-21024") + ); + let small = t.file("small", &format!(">chr20\n{}\n", "A".repeat(11000))); + let fq = t.path("reads.gz"); + assert_eq!( + run(smoke::cli, &["long-reads", &small, &small, &fq]).1, + "4 reads\n" + ); + use std::io::Read; + let mut text = String::new(); + og_io::open(&fq).unwrap().read_to_string(&mut text).unwrap(); + let ls = text.lines().collect::>(); + assert_eq!(ls.len(), 16); + assert_eq!(ls[1], "A".repeat(10000)); + assert_eq!(ls[5], "T".repeat(10000)); + assert_eq!(ls[3], "?".repeat(10000)); + let bad = t.file("bad", ">x\nA\n"); + assert_eq!( + run( + smoke::cli, + &["mini-ref", &bad, &orig, &mini, "450000", "3000"] + ) + .0, + 2 + ); + assert_eq!( + run(smoke::cli, &["eh-catalog", &small, &cat, "449000"]).0, + 2 + ); +} diff --git a/rust/og/src/main.rs b/rust/og/src/main.rs index 29c44efa7..226389c76 100644 --- a/rust/og/src/main.rs +++ b/rust/og/src/main.rs @@ -74,6 +74,9 @@ const USAGE: &str = "usage: og [args] og exomiser report [args] og lookup query|resolve|report [args] og dev parity-compare|contract|schema|report-only|remnants [args] + og dev pharmcat diplotypes REPORT [--called|--called-lines] + og dev nf-trace requests|attempts TRACE [options] + og dev smoke mini-ref|long-reads|eh-catalog|csv-cell|check [args] og dev helpers [args] og dev freshness [--out FILE] [--sections a,b] | --update-issue BODY --repo O/N | --self-test og version diff --git a/scripts/ci/e2e-case-paths.tsv b/scripts/ci/e2e-case-paths.tsv index f76068dcf..0faa36c6f 100644 --- a/scripts/ci/e2e-case-paths.tsv +++ b/scripts/ci/e2e-case-paths.tsv @@ -178,3 +178,4 @@ vntyper-1 assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.t y-private-report-1-private-and-tree assets/live/index.html assets/viewer/index.html bin/collect_summary.py bin/og bin/render_report.py bin/results_index.py bin/run_delta.py bin/traits_report.py bin/yleaf_run.py modules/local/html_report/* modules/local/y_haplogroup/* scripts/16-indexcov.sh scripts/24-html-report.sh scripts/37-y-haplogroup.sh scripts/generate-report.sh scripts/ci/fake-docker/lib.sh scripts/setup.sh y-str-1 assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/live/index.html assets/mito_confirmed.tsv assets/stub/sample.vcf.gz assets/viewer/index.html assets/ystr_panel.tsv bin/collect_summary.py bin/og bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/html_report/* modules/local/indexcov/* modules/local/vcf_precheck/* modules/local/y_str/* scripts/16-indexcov.sh scripts/24-html-report.sh scripts/45-y-str.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh y-tree-current-1-placement assets/live/index.html assets/viewer/index.html bin/og bin/yleaf_run.py scripts/16-indexcov.sh scripts/37-y-haplogroup.sh scripts/ci/fake-docker/lib.sh scripts/setup.sh +rust-z2e-readers rust/crates/og-dev/src/pharmcat.rs rust/crates/og-dev/src/nf_trace.rs rust/crates/og-dev/src/smoke.rs tests/e2e/lib.sh scripts/ci/image-smoke.sh scripts/ci/nf-trace-requests.tsv diff --git a/scripts/ci/e2e-case-times.tsv b/scripts/ci/e2e-case-times.tsv index be90f627f..22a67824a 100644 --- a/scripts/ci/e2e-case-times.tsv +++ b/scripts/ci/e2e-case-times.tsv @@ -177,3 +177,4 @@ vntyper-1 shard 92 - y-private-report-1-private-and-tree shard 150 nextflow-hardening-2-nf y-str-1 shard 42 - y-tree-current-1-placement shard 21 nextflow-hardening-2-nf +rust-z2e-readers shard 1 - diff --git a/scripts/ci/image-smoke.sh b/scripts/ci/image-smoke.sh index d6dc1c9bb..89d55911e 100755 --- a/scripts/ci/image-smoke.sh +++ b/scripts/ci/image-smoke.sh @@ -14,8 +14,7 @@ # names) and check that every *_IMAGE of # versions.env has a row (no docker) # scripts/ci/image-smoke.sh --self-test prove --check fails on a bad table, and that a -# host check never imports a module the image -# planted in its row directory +# host check fails on a false JSON filter # # --full also runs the rows marked `full`: they need data too big for every # pull request (the monthly run and a run started by hand pass it). @@ -142,17 +141,11 @@ self_test() { t "an unknown input fails" "unknown input 'genome'" "$A" $'B_IMAGE\t-\tgenome\ttrue\ttrue' t "also= on an unset variable fails" "also=B_DATA is not set" "$A" $'B_IMAGE\talso=B_DATA\t-\ttrue\ttrue' t "a row with four columns fails" "3 tabs, want 4" "$A" $'B_IMAGE\t-\t-\ttrue' - # A row's checks run in its row directory, which the image wrote. A json.py - # or csv.py the image planted there must not be imported by a host-side - # check (python3 -I keeps the working directory off sys.path). - mkdir "${tmp}/row" - for m in json csv; do printf 'raise SystemExit("planted %s.py was imported")\n' "$m" > "${tmp}/row/${m}.py"; done - printf '{"genes": {"CYP2C19": {"sourceDiplotypes": [{"allele1": {"name": "*1"}, "allele2": {"name": "*2"}}]}}}\n' > "${tmp}/row/r.json" - printf 'a,b\n1,2\n' > "${tmp}/row/t.csv" - out=$(cd "${tmp}/row" && CHECK_FAILS=0 && py "json loads" "import json; print(json.dumps(1))" \ - && pharmcat_called r.json 2>/dev/null && csv_cell t.csv a=1 b && echo "fails=${CHECK_FAILS}") - if [ "$out" = $'[PASS] json loads (1)\n1\n2\nfails=0' ]; then echo "[PASS] a module the image planted in the row directory is not imported" - else echo "[FAIL] a module the image planted in the row directory is not imported: ${out//$'\n'/ | }"; fails=$((fails + 1)); fi + printf '{"value": 1}\n' > "${tmp}/r.json" + out=$(CHECK_FAILS=0; jqcheck "true filter" "${tmp}/r.json" '.value == 1'; jqcheck "false filter" "${tmp}/r.json" '.value == 2'; echo "fails=$CHECK_FAILS") + if grep -q '^\[PASS\] true filter' <<< "$out" && grep -q '^\[FAIL\] false filter' <<< "$out" && grep -q '^fails=1$' <<< "$out"; then + echo "[PASS] jqcheck rejects a false filter" + else echo "[FAIL] jqcheck rejects a false filter: $out"; fails=$((fails + 1)); fi rm -rf "$tmp" if [ "$fails" -gt 0 ]; then echo "self-test: ${fails} case(s) failed" >&2; return 1; fi echo "self-test: all cases passed" @@ -303,20 +296,7 @@ need() { need_ref() { fx_get fixture_ref.fa.gz fixture_ref.fa.gz.fai fixture_ref.fa.gz.gzi || return 1 hsam faidx /fx/fixture_ref.fa.gz chr20:10000001-10500000 > "${IN}/slice.fa.tmp" || return 1 - python3 -I - "${IN}/slice.fa.tmp" "${IN}/orig.fa" "${IN}/mini.fa" "$PLANT_POS" "$PLANT_LEN" <<'PY' || return 1 -import random, sys -src, orig, mini, pos, n = sys.argv[1], sys.argv[2], sys.argv[3], int(sys.argv[4]), int(sys.argv[5]) -seq = "".join(l.strip() for l in open(src) if not l.startswith(">")).upper() -assert len(seq) == 500000, len(seq) -def write(path, s): - with open(path, "w") as f: - f.write(">chr20\n") - for i in range(0, len(s), 60): - f.write(s[i:i + 60] + "\n") -write(orig, seq) -random.seed(20) -write(mini, seq[:pos] + "".join(random.choice("ACGT") for _ in range(n)) + seq[pos:]) -PY + "${OG:-${REPO}/bin/og}" dev smoke mini-ref "${IN}/slice.fa.tmp" "${IN}/orig.fa" "${IN}/mini.fa" "$PLANT_POS" "$PLANT_LEN" || return 1 rm -f "${IN}/slice.fa.tmp" hsam faidx /in/orig.fa && hsam faidx /in/mini.fa && hsam dict -o /in/mini.dict /in/mini.fa || return 1 [ "$(cut -f2 "${IN}/mini.fa.fai")" = "$MINI_LEN" ] @@ -368,21 +348,7 @@ need_longreads() { need ref && need truth || return 1 hbcf consensus -H 1 -f /in/orig.fa /in/truth.vcf.gz > "${IN}/hap1.fa" && hbcf consensus -H 2 -f /in/orig.fa /in/truth.vcf.gz > "${IN}/hap2.fa" || return 1 - python3 -I - "${IN}/hap1.fa" "${IN}/hap2.fa" "${IN}/long.fq.gz" <<'PY' -import gzip, sys -comp = str.maketrans("ACGTN", "TGCAN") -with gzip.open(sys.argv[3], "wt") as out: - k = 0 - for h, path in enumerate(sys.argv[1:3], 1): - seq = "".join(l.strip() for l in open(path) if not l.startswith(">")).upper() - for start in range(0, len(seq) - 10000 + 1, 1000): - r = seq[start:start + 10000] - if k % 2: - r = r.translate(comp)[::-1] - out.write(f"@hap{h}_{start}\n{r}\n+\n{'?' * len(r)}\n") - k += 1 -print(k, "reads") -PY + "${OG:-${REPO}/bin/og}" dev smoke long-reads "${IN}/hap1.fa" "${IN}/hap2.fa" "${IN}/long.fq.gz" } need_longbam() { need longreads || return 1 @@ -463,16 +429,7 @@ need_qc() { # in the mini reference, away from the slice ends and the planted bases. need_ehcatalog() { need ref || return 1 - python3 -I - "${IN}/mini.fa" "${IN}/eh_catalog.json" "$SMALL_END" <<'PY' -import json, re, sys -seq = "".join(l.strip() for l in open(sys.argv[1]) if not l.startswith(">")) -end = int(sys.argv[3]) -m = next(m for m in re.finditer(r"(?:CA){12,}", seq) if 20000 < m.start() and m.end() < end - 20000) -json.dump([{"LocusId": "SMOKE1", "LocusStructure": "(CA)*", - "ReferenceRegion": f"chr20:{m.start()}-{m.end()}", "VariantType": "Repeat"}], - open(sys.argv[2], "w"), indent=1) -print("repeat", m.start(), m.end(), len(m.group()) // 2, "units") -PY + "${OG:-${REPO}/bin/og}" dev smoke eh-catalog "${IN}/mini.fa" "${IN}/eh_catalog.json" "$SMALL_END" } # A chain that maps chr20 onto itself over the region of the small variants. need_chain() { @@ -511,9 +468,21 @@ same() { if [ -n "$2" ] && [ "$2" = "$3" ]; then pass "$1 (${2})"; else fail "$1 has() { if [ -n "$2" ] && grep -Eq -- "$3" <<< "$2"; then pass "$1 (${2})"; else fail "$1 (got '${2}', want /${3}/)"; fi; } contains() { if [ -f "$2" ] && grep -Eq -- "$3" "$2"; then pass "$1"; else fail "$1 (/${3}/ not in ${2})"; fi; } nonempty() { if [ -s "$2" ]; then pass "$1"; else fail "$1 (${2} is missing or empty)"; fi; } -py() { +jqcheck() { local out - if out=$(python3 -I -c "$2" 2>&1); then pass "$1${out:+ (${out//$'\n'/ })}"; else fail "$1: $(tail -n 3 <<< "$out" | tr '\n' ' ')"; fi + if out=$(jq -e "$3" "$2" 2>&1); then pass "$1${out:+ (${out//$'\n'/ })}" + else fail "$1: ${out//$'\n'/ }"; fi +} +smoke_check() { + local out + if out=$("${OG:-${REPO}/bin/og}" dev smoke check "$2" "$3" 2>&1); then pass "$1 ($out)" + else fail "$1: $out"; fi +} +check_pharmcat() { + local out rc=0 + out=$(pharmcat_called "$2") || rc=$? + if [ "$rc" -ne 0 ]; then fail "$1 (not assessed: unreadable PharmCAT report)" + else at_least "$1" "$out" 1; fi } bcf() { hbcf "$@" 2>/dev/null; } # vcf_records FILE: data records (paths relative to the row directory or under /in). @@ -559,42 +528,11 @@ tsv_count() { } # csv_cell FILE KEY=VALUE[,KEY=VALUE] COLUMN: COLUMN of the first row that matches. csv_cell() { - python3 -I - "$1" "$2" "$3" <<'PY' -import csv, sys -want = dict(kv.split("=", 1) for kv in sys.argv[2].split(",")) -for row in csv.DictReader(open(sys.argv[1])): - if all(row.get(k) == v for k, v in want.items()): - print(row[sys.argv[3]]) - break -PY + "${OG:-${REPO}/bin/og}" dev smoke csv-cell "$1" "$2" "$3" } # pharmcat_called report.json: genes with a named diplotype. pharmcat_called() { - python3 -I - "$1" <<'PY' -import json, sys -try: - data = json.load(open(sys.argv[1])) -except Exception as e: - print(f"unreadable report: {e}", file=sys.stderr) - sys.exit(0) -def called(g): - for d in (g.get("sourceDiplotypes") or g.get("recommendationDiplotypes") or []): - names = [(d.get(a) or {}).get("name", "") for a in ("allele1", "allele2")] - if any(n and n.lower() not in ("unknown", "none", "?") for n in names): - return True - return False -out = set() -for key, val in (data.get("genes") or {}).items(): - if not isinstance(val, dict): - continue - if "sourceDiplotypes" in val or "recommendationDiplotypes" in val: - if called(val): - out.add(key) - else: - out.update(n for n, g in val.items() if isinstance(g, dict) and called(g)) -print(" ".join(sorted(out)), file=sys.stderr) -print(len(out)) -PY + "${OG:-${REPO}/bin/og}" dev pharmcat diplotypes "$1" --called } # ------------------------------------------------------------- the runner diff --git a/scripts/ci/nf-trace-requests.tsv b/scripts/ci/nf-trace-requests.tsv new file mode 100644 index 000000000..89ed623b0 --- /dev/null +++ b/scripts/ci/nf-trace-requests.tsv @@ -0,0 +1,10 @@ +process cpus memory_gb time_h +DEEPVARIANT 8 32 48 +FASTP 8 8 8 +ALIGN_MINIMAP2 8 32 24 +MANTA 8 16 24 +CNVPYTOR 4 8 24 +VEP 8 16 24 +PRS_PREPARE 1 2 2 +PRS_SCORE_SITES 2 4 4 +PRS 4 24 8 diff --git a/scripts/ci/parity-diff.sh b/scripts/ci/parity-diff.sh index 3d94ebfb1..d8c555946 100755 --- a/scripts/ci/parity-diff.sh +++ b/scripts/ci/parity-diff.sh @@ -37,7 +37,7 @@ # to compare equal, then plants one difference per item and requires # each to be reported (and a known one to pass, a stale one to fail, # a missing file to fail), on the items PARITY_ITEMS picks. -# Needs docker (bcftools and samtools from versions.env) and python3. Writes +# Needs docker (bcftools and samtools from versions.env) and og (plus jq for --self-test). Writes # a markdown table to stdout and to $GITHUB_STEP_SUMMARY when it is set. set -euo pipefail export LC_ALL=C @@ -112,25 +112,7 @@ canon() { echo "summary:" cat "${dir}/$(path "$side" roh "$s" | awk '{print $2}')"; } ;; pharmcat) - python3 - "$f" <<'PY' -import json, sys -data = json.load(open(sys.argv[1])) -rows = set() -def dips(gene, g): - for d in (g.get("sourceDiplotypes") or g.get("recommendationDiplotypes") or []): - names = [(d.get(a) or {}).get("name", "") or "" for a in ("allele1", "allele2")] - rows.add(f"{gene}\t{'/'.join(names)}") -for key, val in (data.get("genes") or {}).items(): - if not isinstance(val, dict): - continue - if "sourceDiplotypes" in val or "recommendationDiplotypes" in val: - dips(key, val) - else: - for name, g in val.items(): - if isinstance(g, dict): - dips(name, g) -print("\n".join(sorted(rows))) -PY + "${OG:-${ROOT}/bin/og}" dev pharmcat diplotypes "$f" ;; prs) awk -F'\t' 'NR == 1 {for (i = 1; i <= NF; i++) c[$i] = i; next} @@ -296,13 +278,9 @@ self_test() { clinvar) printf 'chr1\t1\tA\tT\t0/1\t1\tGENE:1\tPathogenic\tplanted\n' >> "${t}/nf/$(path nf clinvar "$s")" ;; roh) printf 'RG\t%s\tchr1\t1\t6000000\t6000000\t1\t99.0\n' "$s" >> "${t}/nf/$(path nf roh "$s" | awk '{print $1}')" ;; pharmcat) - python3 - "${t}/nf/$(path nf pharmcat "$s")" <<'PY' -import json, sys -p = sys.argv[1] -d = json.load(open(p)) -d["genes"] = {} -json.dump(d, open(p, "w")) -PY + local report + report="${t}/nf/$(path nf pharmcat "$s")" + jq '.genes = {}' "$report" > "${report}.tmp" && mv "${report}.tmp" "$report" ;; prs) awk -F'\t' -v OFS='\t' 'NR == 1 {for (i = 1; i <= NF; i++) c[$i] = i; print; next} diff --git a/scripts/ci/settle-doubts.sh b/scripts/ci/settle-doubts.sh index d94afc639..8b1fc6891 100755 --- a/scripts/ci/settle-doubts.sh +++ b/scripts/ci/settle-doubts.sh @@ -216,12 +216,7 @@ CHECKED=0 while IFS='=' read -r var val; do img=$(sed -E 's/^"([^"]*)".*/\1/' <<< "$val") CHECKED=$((CHECKED + 1)) - user=$(docker buildx imagetools inspect "$img" --format '{{json .Image}}' 2>/dev/null | python3 -c ' -import json, sys -d = json.load(sys.stdin) -if "config" not in d: - d = d.get("linux/amd64") or next(iter(d.values())) -print((d.get("config") or {}).get("User") or "")' 2>/dev/null || echo "?") + user=$(docker buildx imagetools inspect "$img" --format '{{json .Image}}' 2>/dev/null | jq -r 'if has("config") then . else (."linux/amd64" // (to_entries[0].value)) end | .config.User // ""' 2>/dev/null || echo "?") case "$user" in ""|root|0|0:0) ;; *) diff --git a/tests/e2e/31-pharmcat-07.sh b/tests/e2e/31-pharmcat-07.sh index 5c9c883c7..754494105 100755 --- a/tests/e2e/31-pharmcat-07.sh +++ b/tests/e2e/31-pharmcat-07.sh @@ -8,9 +8,7 @@ check_step_exit 07-pharmacogenomics.sh JSON="${GENOME_DIR}/${SAMPLE}/vcf/${SAMPLE}.report.json" check "report.json exists" test -s "$JSON" -CALLED=$(pharmcat_called "$JSON") -echo "genes with a named diplotype: ${CALLED//$'\n'/ }" -check_ge "genes called in report.json" "$(grep -c . <<< "$CALLED" || true)" 1 +check_pharmcat_called "genes called in report.json" "$JSON" check "HTML report exists" nonempty "${SAMPLE}/vcf/${SAMPLE}.report.html" finish diff --git a/tests/e2e/60-nextflow.sh b/tests/e2e/60-nextflow.sh index 369c0ec1f..a9378e9ac 100755 --- a/tests/e2e/60-nextflow.sh +++ b/tests/e2e/60-nextflow.sh @@ -60,7 +60,7 @@ check "VCFANNO output is indexed" nonempty "${R}/vep/${SAMPLE}_annotated.vcf.gz. check_ge "VCFANNO records carrying REVEL" "$(vcf_count -i 'INFO/REVEL!="."' "${R}/vep/${SAMPLE}_annotated.vcf.gz")" 1 check_ge "ROH output lines" "$(cat "${GENOME_DIR}/${R}"/roh/*_roh.txt 2>/dev/null | grep -vc '^#' || true)" 1 JSON=$(find "${GENOME_DIR}/${R}/pharmcat" -name '*.report.json' 2>/dev/null | awk 'NR == 1') -check_ge "PHARMCAT genes called" "$( [ -n "$JSON" ] && pharmcat_called "$JSON" | grep -c . || echo 0)" 1 +check_pharmcat_called "PHARMCAT genes called" "$JSON" check_ge "CPIC_LOOKUP gene rows" \ "$(awk -F'\t' 'NF >= 3' "${GENOME_DIR}/${R}/cpic/${SAMPLE}_phenotypes.tsv" 2>/dev/null | wc -l | tr -d ' ')" 1 diff --git a/tests/e2e/lib.sh b/tests/e2e/lib.sh index e469c2875..e0a01ae41 100755 --- a/tests/e2e/lib.sh +++ b/tests/e2e/lib.sh @@ -87,31 +87,22 @@ sample_side_hits() { # pharmcat_called : genes with a named diplotype (flat or nested # `genes` map, sourceDiplotypes or recommendationDiplotypes), one per line. pharmcat_called() { - python3 - "$1" <<'PY' -import json, sys -try: - data = json.load(open(sys.argv[1])) -except Exception as e: - print(f"unreadable report: {e}", file=sys.stderr) - sys.exit(0) -def called(g): - for d in (g.get("sourceDiplotypes") or g.get("recommendationDiplotypes") or []): - names = [(d.get(a) or {}).get("name", "") for a in ("allele1", "allele2")] - if any(n and n.lower() not in ("unknown", "none", "?") for n in names): - return True - return False -genes = data.get("genes") or {} -out = set() -for key, val in genes.items(): - if not isinstance(val, dict): - continue - if "sourceDiplotypes" in val or "recommendationDiplotypes" in val: - if called(val): - out.add(key) - else: - out.update(name for name, g in val.items() if isinstance(g, dict) and called(g)) -print("\n".join(sorted(out))) -PY + local genes rc=0 + genes=$("${OG:-${REPO}/bin/og}" dev pharmcat diplotypes "$1" --called-lines) || rc=$? + if [ "$rc" -ne 0 ]; then + echo "not assessed: PharmCAT report could not be read" >&2 + return "$rc" + fi + printf '%s\n' "$genes" +} + +# An unreadable report is a failed assessment, even when a surrounding pipe +# would otherwise turn the missing output into a zero count. +check_pharmcat_called() { + local genes rc=0 + genes=$(pharmcat_called "$2") || rc=$? + if [ "$rc" -ne 0 ]; then fail "$1 (not assessed: unreadable PharmCAT report)" + else check_ge "$1" "$(grep -c . <<< "$genes" || true)" 1; fi } # --- The ancestry panel, from the Actions cache or the mirror when it is there --- diff --git a/tests/e2e/nextflow-from-fastq-2-nextflow.sh b/tests/e2e/nextflow-from-fastq-2-nextflow.sh index 155ed2997..be93a8ff8 100755 --- a/tests/e2e/nextflow-from-fastq-2-nextflow.sh +++ b/tests/e2e/nextflow-from-fastq-2-nextflow.sh @@ -151,7 +151,7 @@ check_ge "heterozygous calls on the PAR1 slice (diploid there)" "$(vcf_count -g GENE=$(planted gene) check_ge "CLINVAR_SCREEN names the planted hit's gene (${GENE})" "$(sample_side_hits "${G}/${R}/clinvar" "$GENE")" 1 JSON="${G}/${R}/pharmcat/${P}.report.json" -check_ge "PHARMCAT genes called" "$( [ -s "$JSON" ] && pharmcat_called "$JSON" | grep -c . || echo 0)" 1 +check_pharmcat_called "PHARMCAT genes called" "$JSON" check_eq "PRS scores in the summary (one per id of assets/pgs_scores.tsv)" "$(awk 'NR > 1' "${G}/${R}/prs/${P}_prs_summary.tsv" 2>/dev/null | wc -l | tr -d ' ')" \ "$(awk -F'\t' '$1 ~ /^PGS[0-9]+$/' "${REPO}/assets/pgs_scores.tsv" | wc -l | tr -d ' ')" check_eq "PRS input (hom-ref sites from the gVCF)" "$(awk -F'\t' 'NR == 2 {print $NF}' "${G}/${R}/prs/${P}_prs_summary.tsv" 2>/dev/null)" gvcf diff --git a/tests/e2e/pgx-6-gvcf-column.sh b/tests/e2e/pgx-6-gvcf-column.sh index 7e4feecac..8c7216209 100755 --- a/tests/e2e/pgx-6-gvcf-column.sh +++ b/tests/e2e/pgx-6-gvcf-column.sh @@ -64,7 +64,7 @@ OUT=$(cat "${WORK}/${HASH:-none}"*/.command.out 2>/dev/null) echo "PHARMCAT_PREPROCESS (${HASH:-no task}): $(head -n 1 <<< "$OUT")" check "PharmCAT's input is the gVCF, its reference blocks expanded" has "^Input: ${P}\.g\.vcf\.gz \(reference blocks expanded" "$OUT" JSON="${NF_OUT}/${P}/pharmcat/${P}.report.json" -check_ge "PHARMCAT genes called" "$( [ -s "$JSON" ] && pharmcat_called "$JSON" | grep -c . || echo 0)" 1 +check_pharmcat_called "PHARMCAT genes called" "$JSON" check_eq "PRS input (hom-ref sites from the gVCF)" \ "$(awk -F'\t' 'NR == 2 {print $NF}' "${NF_OUT}/${P}/prs/${P}_prs_summary.tsv" 2>/dev/null)" gvcf diff --git a/tests/e2e/rust-z2e-readers.sh b/tests/e2e/rust-z2e-readers.sh new file mode 100755 index 000000000..7ece97ea9 --- /dev/null +++ b/tests/e2e/rust-z2e-readers.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Public synthetic reports and traces. No containers or downloaded fixture needed. +. "$(dirname "$0")/lib.sh" +printf '{"genes":{"CYP2C19":{"sourceDiplotypes":[{"allele1":{"name":"*1"}}]}}}\n' > "$CASE_TMP/report.json" +check_pharmcat_called "a readable report has a named gene" "$CASE_TMP/report.json" +printf '{"genes":' > "$CASE_TMP/broken.json" +rc=0 +pharmcat_called "$CASE_TMP/broken.json" > "$CASE_TMP/out" 2> "$CASE_TMP/err" || rc=$? +check_eq "truncated report exits 2" "$rc" 2 +check "truncated report is unreadable, never a zero count" grep -q 'unreadable report:' "$CASE_TMP/err" +check_eq "truncated report has no call output" "$(wc -c < "$CASE_TMP/out" | tr -d ' ')" 0 +printf 'task_id\tname\tattempt\texit\tmemory\n1\tPRS (s)\t1\t137\t25769803776\n2\tPRS (s)\t2\t0\t51539607552\n' > "$CASE_TMP/trace.txt" +check "retry memory and exit match" "${OG:-${REPO}/bin/og}" dev nf-trace attempts "$CASE_TMP/trace.txt" --name-glob 'PRS (s)' --want '1:137:24,2:0:48' --nextflow-exit 0 +rc=0 +"${OG:-${REPO}/bin/og}" dev nf-trace attempts "$CASE_TMP/trace*.txt" --name-glob 'PRS (s)' --want '1:137:24,2:0:48' --nextflow-exit 0 > "$CASE_TMP/out" 2> "$CASE_TMP/err" || rc=$? +check_eq "ambiguous trace glob exits 2" "$rc" 2 +check "trace glob refusal explains the required path" grep -q 'trace glob refused' "$CASE_TMP/err" +finish diff --git a/tests/e2e/vendor-vcf-intake-5-docs.sh b/tests/e2e/vendor-vcf-intake-5-docs.sh index 10a9a03c8..3ef380fae 100755 --- a/tests/e2e/vendor-vcf-intake-5-docs.sh +++ b/tests/e2e/vendor-vcf-intake-5-docs.sh @@ -80,7 +80,7 @@ check "the page's Nextflow run wrote the report" test -s "${R}/sample1_report.ht check_eq "its ROH card is filled" "$(html_stat "${R}/sample1_report.html" 'Runs of Homozygosity' Status)" Complete check_eq "its haplogroup card is filled" "$(html_stat "${R}/sample1_report.html" 'Mitochondrial Haplogroup' Status)" Complete JSON="${R}/pharmcat/sample1.report.json" -check_ge "its PharmCAT calls genes" "$( [ -s "$JSON" ] && pharmcat_called "$JSON" | grep -c . || echo 0)" 1 +check_pharmcat_called "its PharmCAT calls genes" "$JSON" # --- docs/nextflow.md "Before you share outputs", as pasted ------------------------------ RECIPE=$(awk '/^## Before you share outputs/ {on = 1} on && /^```bash$/ {b = 1; next} b && /^```/ {exit} b' \ diff --git a/tests/parity/README.md b/tests/parity/README.md index d83ff2087..2604ea891 100644 --- a/tests/parity/README.md +++ b/tests/parity/README.md @@ -42,3 +42,5 @@ A `known.tsv` row whose difference is gone fails. A golden that the oracle also 3. Run `scripts/ci/oracle-diff.sh`. Turn each difference into a fix, a golden or a `known.tsv` row. 4. Switch the callers to `og`, delete the Python, and add the tool to `OG_SHADOW` in `.github/workflows/e2e.yml`. 5. Link a red run in the pull request: the cases with one planted change in the port must fail. + +The `dev-pharmcat*`, `dev-nf-*` and `dev-smoke-*` cases extract archived heredocs from their pre-port shell/workflow files. The wrapper keeps the original Python as the oracle. The mini-reference golden records the deliberate change to a fixed Rust random seed. Truncated PharmCAT JSON has goldens for exit 2 and the unreadable-report diagnostic. The standalone [CI reader tests](../../rust/crates/og-dev/tests/ci_readers.rs) pair clean inputs with changed requests, exits, CSV fields and smoke properties that must fail. diff --git a/tests/parity/cases.tsv b/tests/parity/cases.tsv index 0a9afd6bf..98a436f8d 100644 --- a/tests/parity/cases.tsv +++ b/tests/parity/cases.tsv @@ -950,3 +950,19 @@ family-ibd mapbp gen:ibd-oracle.sh,gen:ibd-cases.sh --pedigree mapbp/pedigree.ts family-ibd nomap gen:ibd-oracle.sh,gen:ibd-cases.sh --pedigree nomap/pedigree.tsv --chroms nomap/chroms.txt --maps nomap/maps --phased nomap/phased --ibd nomap/ibd --out-dir out - content PYTHON_IMAGE family-ibd short gen:ibd-oracle.sh,gen:ibd-cases.sh --pedigree short/pedigree.tsv --chroms short/chroms.txt --maps short/maps --phased short/phased --ibd short/ibd --out-dir out - content PYTHON_IMAGE family-ibd twice gen:ibd-oracle.sh,gen:ibd-cases.sh --pedigree twice/pedigree.tsv --chroms twice/chroms.txt --maps twice/maps --phased twice/phased --ibd twice/ibd --out-dir out - content PYTHON_IMAGE +dev-pharmcat flat ci-readers/oracle.sh,ci-readers/flat.json flat.json - bytes PYTHON_IMAGE +dev-pharmcat-called flat ci-readers/oracle.sh,ci-readers/flat.json flat.json --called - bytes PYTHON_IMAGE +dev-pharmcat-lines flat ci-readers/oracle.sh,ci-readers/flat.json flat.json --called-lines - bytes PYTHON_IMAGE +dev-pharmcat nested ci-readers/oracle.sh,ci-readers/nested.json nested.json - bytes PYTHON_IMAGE +dev-pharmcat-called nested ci-readers/oracle.sh,ci-readers/nested.json nested.json --called - bytes PYTHON_IMAGE +dev-pharmcat-lines nested ci-readers/oracle.sh,ci-readers/nested.json nested.json --called-lines - bytes PYTHON_IMAGE +dev-pharmcat empty ci-readers/oracle.sh,ci-readers/empty.json empty.json - bytes PYTHON_IMAGE +dev-pharmcat-called empty ci-readers/oracle.sh,ci-readers/empty.json empty.json --called - bytes PYTHON_IMAGE +dev-pharmcat-lines empty ci-readers/oracle.sh,ci-readers/empty.json empty.json --called-lines - bytes PYTHON_IMAGE +dev-pharmcat-called truncated ci-readers/oracle.sh,ci-readers/truncated.json truncated.json --called - bytes PYTHON_IMAGE +dev-smoke-csv quoted ci-readers/oracle.sh,ci-readers/cells.csv cells.csv Filter=PASS Value - bytes PYTHON_IMAGE +dev-nf-requests resources ci-readers/oracle.sh,ci-readers/requests.tsv,ci-readers/want.tsv requests.tsv --want want.tsv --executor-cpus 512 --executor-mem-gb 2048 - bytes PYTHON_IMAGE +dev-nf-attempts retry ci-readers/oracle.sh,ci-readers/attempts.tsv attempts.tsv --name-glob *ALIGN_MINIMAP2* --want 1:137:32,2:0:64 --nextflow-exit 0 - bytes PYTHON_IMAGE +dev-smoke-mini fixture ci-readers/oracle.sh,gen:ci-smoke.sh slice.fa orig.fa mini.fa 450000 3000 orig.fa,mini.fa bytes PYTHON_IMAGE +dev-smoke-long fixture ci-readers/oracle.sh,gen:ci-smoke.sh hap1.fa hap2.fa long.fq.gz long.fq.gz gz-content PYTHON_IMAGE +dev-smoke-catalog fixture ci-readers/oracle.sh,gen:ci-smoke.sh slice.fa eh.json 449000 eh.json json PYTHON_IMAGE diff --git a/tests/parity/gen/ci-smoke.sh b/tests/parity/gen/ci-smoke.sh new file mode 100755 index 000000000..e7eae467f --- /dev/null +++ b/tests/parity/gen/ci-smoke.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +awk 'BEGIN {print ">chr20"; for(i=0;i<500000;i++) {printf "%s",(i>=21000 && i<21024 ? (i%2==0 ? "C":"A") : "G"); if(i%60==59)printf "\n"} printf "\n"}' > slice.fa +for h in 1 2; do awk 'BEGIN {print ">chr20";for(i=0;i<11000;i++){printf "A";if(i%60==59)printf "\n"}printf "\n"}' > "hap${h}.fa"; done diff --git a/tests/parity/inputs/ci-readers/attempts.tsv b/tests/parity/inputs/ci-readers/attempts.tsv new file mode 100644 index 000000000..4e9bb6c82 --- /dev/null +++ b/tests/parity/inputs/ci-readers/attempts.tsv @@ -0,0 +1,3 @@ +task_id name attempt exit memory +1 ALIGN_MINIMAP2 (s) 1 137 34359738368 +2 ALIGN_MINIMAP2 (s) 2 0 68719476736 diff --git a/tests/parity/inputs/ci-readers/cells.csv b/tests/parity/inputs/ci-readers/cells.csv new file mode 100644 index 000000000..c634a7429 --- /dev/null +++ b/tests/parity/inputs/ci-readers/cells.csv @@ -0,0 +1,4 @@ +Type,Filter,Value +"S,N",PASS,"a,b +next" +SNP,PASS,0.9 diff --git a/tests/parity/inputs/ci-readers/empty.json b/tests/parity/inputs/ci-readers/empty.json new file mode 100644 index 000000000..6deaa1ec2 --- /dev/null +++ b/tests/parity/inputs/ci-readers/empty.json @@ -0,0 +1 @@ +{"genes":{}} diff --git a/tests/parity/inputs/ci-readers/flat.json b/tests/parity/inputs/ci-readers/flat.json new file mode 100644 index 000000000..d644fc71d --- /dev/null +++ b/tests/parity/inputs/ci-readers/flat.json @@ -0,0 +1 @@ +{"genes": {"B": {"sourceDiplotypes": [{"allele1": {"name": "*1"}, "allele2": {"name": "*2"}}]}, "A": {"sourceDiplotypes": [{"allele1": {"name": "Unknown"}}]}}} diff --git a/tests/parity/inputs/ci-readers/nested.json b/tests/parity/inputs/ci-readers/nested.json new file mode 100644 index 000000000..de864f52e --- /dev/null +++ b/tests/parity/inputs/ci-readers/nested.json @@ -0,0 +1 @@ +{"genes": {"CPIC": {"B": {"sourceDiplotypes": [{"allele1": {"name": "*1"}, "allele2": {"name": "*2"}}]}, "A": {"sourceDiplotypes": [{"allele1": {"name": "Unknown"}}]}}, "DPWG": {"B": {"sourceDiplotypes": [{"allele1": {"name": "*1"}, "allele2": {"name": "*2"}}]}, "A": {"sourceDiplotypes": [{"allele1": {"name": "Unknown"}}]}}}} diff --git a/tests/parity/inputs/ci-readers/oracle.sh b/tests/parity/inputs/ci-readers/oracle.sh new file mode 100644 index 000000000..eeb13677e --- /dev/null +++ b/tests/parity/inputs/ci-readers/oracle.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Extract the archived heredoc, not a second maintained implementation. +set -euo pipefail +mode=$1 prog=$2; shift 2 +case "$mode" in + diplotypes) start='^import json, sys$' ;; + called-lines) start='^import json, sys$' ;; + called) start='^import json, sys$' ;; + csv-cell) start='^import csv, sys$' ;; + mini-ref) start='^import random, sys$' ;; + long-reads) start='^import gzip, sys$' ;; + eh-catalog) start='^import json, re, sys$' ;; + requests) start='^import csv, sys$' ;; + attempts) start='^import csv, sys$' ;; + *) exit 2 ;; +esac +# The workflow's first inline reader is requests, the second is attempts. +occurrence=1 +[ "$mode" != attempts ] || occurrence=2 +awk -v re="$start" -v want="$occurrence" ' + { sub(/^ /, "") } + $0 ~ re { n++; if (n == want) on=1 } + on && /^PY$/ { exit } + on { print } +' "$prog" > oracle-body +case "$mode" in + called|called-lines) set -- "$1" ;; + requests) set -- "$1" "$5" "$7" ;; + attempts) set -- "$1" "$7" ;; +esac +rc=0 +python3 -I oracle-body "$@" || rc=$? +rm -f oracle-body +exit "$rc" diff --git a/tests/parity/inputs/ci-readers/requests.tsv b/tests/parity/inputs/ci-readers/requests.tsv new file mode 100644 index 000000000..7c5ef3d03 --- /dev/null +++ b/tests/parity/inputs/ci-readers/requests.tsv @@ -0,0 +1,19 @@ +task_id name attempt exit cpus memory time +1 X:DEEPVARIANT (s) 1 137 8 34359738368 172800000 +2 X:DEEPVARIANT (s) 2 0 8 68719476736 172800000 +3 X:FASTP (s) 1 137 8 8589934592 28800000 +4 X:FASTP (s) 2 0 8 17179869184 28800000 +5 X:ALIGN_MINIMAP2 (s) 1 137 8 34359738368 86400000 +6 X:ALIGN_MINIMAP2 (s) 2 0 8 68719476736 86400000 +7 X:MANTA (s) 1 137 8 17179869184 86400000 +8 X:MANTA (s) 2 0 8 34359738368 86400000 +9 X:CNVPYTOR (s) 1 137 4 8589934592 86400000 +10 X:CNVPYTOR (s) 2 0 4 17179869184 86400000 +11 X:VEP (s) 1 137 8 17179869184 86400000 +12 X:VEP (s) 2 0 8 34359738368 86400000 +13 X:PRS_PREPARE (s) 1 137 1 2147483648 7200000 +14 X:PRS_PREPARE (s) 2 0 1 4294967296 7200000 +15 X:PRS_SCORE_SITES (s) 1 137 2 4294967296 14400000 +16 X:PRS_SCORE_SITES (s) 2 0 2 8589934592 14400000 +17 X:PRS (s) 1 137 4 25769803776 28800000 +18 X:PRS (s) 2 0 4 51539607552 28800000 diff --git a/tests/parity/inputs/ci-readers/truncated.json b/tests/parity/inputs/ci-readers/truncated.json new file mode 100644 index 000000000..a24f91415 --- /dev/null +++ b/tests/parity/inputs/ci-readers/truncated.json @@ -0,0 +1 @@ +{"genes": \ No newline at end of file diff --git a/tests/parity/inputs/ci-readers/want.tsv b/tests/parity/inputs/ci-readers/want.tsv new file mode 100644 index 000000000..89ed623b0 --- /dev/null +++ b/tests/parity/inputs/ci-readers/want.tsv @@ -0,0 +1,10 @@ +process cpus memory_gb time_h +DEEPVARIANT 8 32 48 +FASTP 8 8 8 +ALIGN_MINIMAP2 8 32 24 +MANTA 8 16 24 +CNVPYTOR 4 8 24 +VEP 8 16 24 +PRS_PREPARE 1 2 2 +PRS_SCORE_SITES 2 4 4 +PRS 4 24 8 diff --git a/tests/parity/oracle.ref b/tests/parity/oracle.ref index 57c27da2f..4932df874 100644 --- a/tests/parity/oracle.ref +++ b/tests/parity/oracle.ref @@ -63,3 +63,12 @@ y-yleaf-positions 0baddb45aa3871a57469cf20295bdd832f72f5c0 file:bin/yleaf_run.py lookup 0baddb45aa3871a57469cf20295bdd832f72f5c0 file:bin/genome_lookup.py python3 lookup lookup-query 0baddb45aa3871a57469cf20295bdd832f72f5c0 file:bin/genome_lookup.py bash ./oracle.sh lookup query family-ibd 0baddb45aa3871a57469cf20295bdd832f72f5c0 file:bin/ibd_report.py sh ./ibd_oracle.sh family ibd +dev-pharmcat 669a8196cf4c377200f9b4af9c235ef85fb774a1 file:scripts/ci/parity-diff.sh sh -c 'exec bash ./oracle.sh diplotypes "$0" "$@"' dev pharmcat diplotypes +dev-pharmcat-called 669a8196cf4c377200f9b4af9c235ef85fb774a1 file:scripts/ci/image-smoke.sh sh -c 'exec bash ./oracle.sh called "$0" "$@"' dev pharmcat diplotypes +dev-pharmcat-lines 669a8196cf4c377200f9b4af9c235ef85fb774a1 file:tests/e2e/lib.sh sh -c 'exec bash ./oracle.sh called-lines "$0" "$@"' dev pharmcat diplotypes +dev-smoke-csv 669a8196cf4c377200f9b4af9c235ef85fb774a1 file:scripts/ci/image-smoke.sh sh -c 'exec bash ./oracle.sh csv-cell "$0" "$@"' dev smoke csv-cell +dev-nf-requests 669a8196cf4c377200f9b4af9c235ef85fb774a1 file:.github/workflows/nextflow.yml sh -c 'exec bash ./oracle.sh requests "$0" "$@"' dev nf-trace requests +dev-nf-attempts 669a8196cf4c377200f9b4af9c235ef85fb774a1 file:.github/workflows/nextflow.yml sh -c 'exec bash ./oracle.sh attempts "$0" "$@"' dev nf-trace attempts +dev-smoke-mini 669a8196cf4c377200f9b4af9c235ef85fb774a1 file:scripts/ci/image-smoke.sh sh -c 'exec bash ./oracle.sh mini-ref "$0" "$@"' dev smoke mini-ref +dev-smoke-long 669a8196cf4c377200f9b4af9c235ef85fb774a1 file:scripts/ci/image-smoke.sh sh -c 'exec bash ./oracle.sh long-reads "$0" "$@"' dev smoke long-reads +dev-smoke-catalog 669a8196cf4c377200f9b4af9c235ef85fb774a1 file:scripts/ci/image-smoke.sh sh -c 'exec bash ./oracle.sh eh-catalog "$0" "$@"' dev smoke eh-catalog diff --git a/tests/smoke/commands.tsv b/tests/smoke/commands.tsv index 11e807eb3..ff757048e 100644 --- a/tests/smoke/commands.tsv +++ b/tests/smoke/commands.tsv @@ -7,7 +7,7 @@ MINIMAP2_SAMTOOLS_IMAGE - ref,reads,bam minimap2 --version > minimap2_version.tx SAMTOOLS_HTTPS_IMAGE net - samtools view -H https://ftp.sra.ebi.ac.uk/vol1/run/ERR323/ERR3239334/NA12878.final.cram > h.sam same "@SQ lines of the docs/quick-test.md CRAM header, read over https" "$(grep -c '^@SQ' h.sam)" 3366; contains "chr22 among them" h.sam "^@SQ[[:space:]]SN:chr22[[:space:]]" BCFTOOLS_IMAGE - ref,truth,vcf bcftools view -Ob -o a.bcf /in/sample.vcf.gz && bcftools index a.bcf && bcftools view -Oz -o b.vcf.gz a.bcf && bcftools index -t b.vcf.gz && bcftools view -H b.vcf.gz | wc -l > n.txt && bcftools +split-vep -l /in/sample.vcf.gz > csq.txt && bcftools norm -m-both -f /in/mini.fa -Oz -o norm.vcf.gz /in/truth.vcf.gz && bcftools isec -n=2 -w1 -Oz -o shared.vcf.gz /in/truth.vcf.gz /in/query90.vcf.gz && bcftools view -H shared.vcf.gz | wc -l > shared.txt same "records after a VCF to BCF to VCF round trip" "$(tr -d " " < n.txt)" "$(grep -vc "^#" "$FX/HG002_vep.vcf")"; contains "split-vep lists the CSQ fields" csq.txt "Consequence"; same "records isec finds in truth and query" "$(tr -d " " < shared.txt)" "$(vcf_records /in/query90.vcf.gz)"; at_least "records after norm" "$(vcf_records norm.vcf.gz)" 300 DEEPVARIANT_IMAGE - ref,bam,truth /opt/deepvariant/bin/run_deepvariant --model_type=WGS --ref=/in/mini.fa --reads=/in/mini.bam --output_vcf=dv.vcf.gz --sample_name=HG002 --num_shards=4 --regions chr20:1-449000 --intermediate_results_dir tmp at_least "SNV recall against the GIAB truth" "$(snv_recall dv.vcf.gz)" 0.95; same "sample column" "$(bcf query -l dv.vcf.gz)" HG002 -FASTP_IMAGE - reads fastp -i /in/reads_R1.fq.gz -I /in/reads_R2.fq.gz -o t_R1.fq.gz -O t_R2.fq.gz --detect_adapter_for_pe --qualified_quality_phred 20 --cut_front --cut_tail --cut_mean_quality 20 --length_required 36 -g -R HG002 -j fastp.json -h fastp.html py "trimmed reads kept (at least 80 %)" "import json; s = json.load(open('fastp.json'))['summary']; b = s['before_filtering']['total_reads']; a = s['after_filtering']['total_reads']; print(a, 'of', b); assert b > 0 and a >= 0.8 * b" +FASTP_IMAGE - reads fastp -i /in/reads_R1.fq.gz -I /in/reads_R2.fq.gz -o t_R1.fq.gz -O t_R2.fq.gz --detect_adapter_for_pe --qualified_quality_phred 20 --cut_front --cut_tail --cut_mean_quality 20 --length_required 36 -g -R HG002 -j fastp.json -h fastp.html jqcheck "trimmed reads kept (at least 80 %)" fastp.json '.summary | .before_filtering.total_reads as $b | .after_filtering.total_reads as $a | $b > 0 and $a >= 0.8 * $b' BWAMEM2_IMAGE - ref,reads bwa-mem2 index -p mini /in/mini.fa && bwa-mem2 mem -t 4 -o bwamem2.sam -R '@RG\tID:HG002\tSM:HG002\tPL:ILLUMINA\tLB:HG002' mini /in/reads_R1.fq.gz /in/reads_R2.fq.gz at_least "reads mapped (%)" "$(mapped_pct bwamem2.sam)" 95 BWA_IMAGE - ref,reads bwa index -p mini /in/mini.fa && bwa mem -t 4 -R '@RG\tID:HG002\tSM:HG002' mini /in/reads_R1.fq.gz /in/reads_R2.fq.gz > bwa.sam at_least "reads mapped (%)" "$(mapped_pct bwa.sam)" 95 FREEBAYES_IMAGE - ref,bam,truth freebayes -f /in/mini.fa --region chr20:1-449000 /in/mini.bam > freebayes.vcf at_least "SNV recall against the GIAB truth" "$(snv_recall freebayes.vcf)" 0.9 @@ -31,7 +31,7 @@ SURVIVOR_IMAGE - - sh /smoke/survivor.sh same "records kept" "$(grep -vc '^#' me VEP_IMAGE net vcf50 vep --input_file /in/sample50.vcf --output_file vep.vcf --vcf --database --assembly GRCh38 --symbol --force_overwrite --no_stats && grep -v "^#" vep.vcf | awk -F'\t' '{n = split($8, kv, ";"); for (i = 1; i <= n; i++) if (kv[i] ~ /^CSQ=/) {k = split(substr(kv[i], 5), e, ","); for (j = 1; j <= k; j++) {split(e[j], f, "|"); m = split(f[2], q, "&"); for (x = 1; x <= m; x++) print $1 "\t" $2 "\t" $4 "\t" $5 "\t" q[x]}}}' | LC_ALL=C sort -u > csq.tsv && cat csq.tsv same "records with a CSQ annotation" "$(grep -v "^#" vep.vcf | grep -c "CSQ=")" 50; contains "a gene symbol of the fixture in CSQ" vep.vcf "CYP2C19|CYP2C9|SNAP25|HLA-"; at_least "variant and consequence pairs" "$(wc -l < csq.tsv)" 50; same "pairs that differ from tests/smoke/vep50_consequences.tsv" "$( { [ -s "${SMOKE_DIR}/vep50_consequences.tsv" ] || echo "< the baseline file is missing"; diff csq.tsv "${SMOKE_DIR}/vep50_consequences.tsv"; } 2>/dev/null | grep -c "^[<>]")" 0 VEP_IMAGE net vcf50 sh /smoke/vep-clinvar-custom.sh contains "ClinVar_CLNSIG in the CSQ header" vep_cv.vcf "Format: .*\|ClinVar_CLNSIG"; same "records with ClinVar_CLNSIG Pathogenic" "$(grep -v '^#' vep_cv.vcf | grep -c 'Pathogenic|reviewed_by_expert_panel')" 1; same "and it is the first record" "$(grep -v '^#' vep_cv.vcf | grep 'Pathogenic|reviewed_by_expert_panel' | awk -F'\t' '{print $1 ":" $2}')" "$(cat first.txt)" PCGR_IMAGE net vcf50 sh /smoke/pcgr.sh has "cpsr --version names the release line of the image tag (2.3.2 says cpsr 2.3.0)" "$(cat cpsr_version.txt)" "cpsr $(echo "${PCGR_IMAGE##*:}" | cut -d. -f1-2)"; same "records the bundled VEP annotates" "$(grep -v "^#" vep.vcf | grep -c "CSQ=")" 50; has "the pcgrr R package is the release of the image tag" "$(cat pcgrr_version.txt)" "^${PCGR_IMAGE##*:}"; same "flags of step 17 that cpsr --help does not list" "$(grep -c . cpsr_missing_flags.txt)" 0 -PHARMCAT_IMAGE - fullref,pgxvcf python3 /pharmcat/pharmcat_vcf_preprocessor -vcf /in/pgx.vcf.gz -refFna /in/ref.fa -o /out/ -bf HG002 && java -jar /pharmcat/pharmcat.jar -vcf /out/HG002.preprocessed.vcf.bgz -o /out/ -bf HG002 -reporterJson -reporterHtml at_least "genes with a named diplotype in report.json" "$(pharmcat_called HG002.report.json)" 1 +PHARMCAT_IMAGE - fullref,pgxvcf python3 /pharmcat/pharmcat_vcf_preprocessor -vcf /in/pgx.vcf.gz -refFna /in/ref.fa -o /out/ -bf HG002 && java -jar /pharmcat/pharmcat.jar -vcf /out/HG002.preprocessed.vcf.bgz -o /out/ -bf HG002 -reporterJson -reporterHtml check_pharmcat "genes with a named diplotype in report.json" HG002.report.json PYPGX_IMAGE also=PYPGX_BUNDLE_VERSION fullref,slice,bundle sh /smoke/pypgx.sh has "CYP2D6 genotype" "$(cat CYP2D6.genotype.txt)" "^\*[0-9]" TELOMEREHUNTER_IMAGE - slice sh /smoke/telomerehunter.sh same "intratelomeric reads (1,100 planted)" "$(tsv_cell th/HG002/HG002_summary.tsv intratel_reads)" 1100; between "tel_content of the planted reads (5347.26 with 1.1.0 on fixture-v7; 5594.23 before its chrY stretches)" "$(tsv_cell th/HG002/HG002_summary.tsv tel_content)" 5347 5348; between "TCAGGG per intratelomeric read (100 of 1,100)" "$(tsv_cell th/HG002/HG002_summary.tsv TCAGGG_arbitrary_context_norm_by_intratel_reads)" 0.09 0.092 HAPLOGREP3_IMAGE - mito sh /smoke/haplogrep3.sh has "haplogroup" "$(tsv_cell haplogroup.txt Haplogroup)" "^[A-Z]"; at_least "haplogroup quality" "$(tsv_cell haplogroup.txt Quality)" 0.8 @@ -41,15 +41,15 @@ YLEAF_IMAGE net,also=YLEAF_DATA_SHA256 - sh /smoke/yleaf.sh same "the release ar HIPSTR_IMAGE - fullref,slice sh /smoke/hipstr.sh at_least "Y-STR regions HipSTR genotypes in the chrY slices, with GB, DP, Q and DSTUTTER (of 11)" "$(grep -c . called.tsv)" 7; same "a haploid call on chrY (one allele in GT)" "$(awk -F'\t' '$2 ~ /[\/|]/' called.tsv | wc -l | tr -d ' ')" 0; has "the image's Python, which step 45 runs on" "$(cat python_version.txt)" "^Python 3\."; contains "HipSTR's log names the regions it read" hipstr.log "Processing region chrY 6993189 6993257" T1K_IMAGE net hlareads sh /smoke/t1k.sh has "HLA-A allele, one of HG002's (A*01:01, A*26:01)" "$(awk -F'\t' '$1 == "HLA-A" {print $3}' t1k/HG002_hla_genotype.tsv)" "^HLA-A\*(01:01|26:01)"; has "both HLA-A alleles of HG002 (A*01:01 and A*26:01)" "$(awk -F'\t' '$1 == "HLA-A" {print $3, $6}' t1k/HG002_hla_genotype.tsv)" "^HLA-A\*01:01[^ ]* HLA-A\*26:01|^HLA-A\*26:01[^ ]* HLA-A\*01:01" OPTITYPE_IMAGE - hlareads sh /smoke/optitype.sh has "both HLA-A alleles of HG002 (A*01:01 and A*26:01)" "$(awk -F'\t' 'NR == 2 {print $2, $3}' HG002_result.tsv)" "^A\*01:01 A\*26:01$|^A\*26:01 A\*01:01$"; has "two HLA-B alleles" "$(awk -F'\t' 'NR == 2 {print $4, $5}' HG002_result.tsv)" "^B\*[0-9]+:[0-9]+ B\*[0-9]+:[0-9]+$"; at_least "reads OptiType mapped to its HLA sequences" "$(awk -F'\t' 'NR == 1 {for (i = 1; i <= NF; i++) if ($i == "Reads") c = i} NR == 2 {print int($c)}' HG002_result.tsv)" 100 -EXPANSIONHUNTER_IMAGE - ref,bam,ehcatalog sh /smoke/expansionhunter.sh py "the bundled GRCh38 catalog has at least 20 loci" "import json; n = len(json.load(open('bundled_catalog.json'))); print(n); assert n >= 20"; py "a genotype for the repeat on the fixture" "import json; v = json.load(open('eh.json'))['LocusResults']['SMOKE1']['Variants']['SMOKE1']; print(v.get('Genotype')); assert v.get('Genotype')" +EXPANSIONHUNTER_IMAGE - ref,bam,ehcatalog sh /smoke/expansionhunter.sh jqcheck "the bundled GRCh38 catalog has at least 20 loci" bundled_catalog.json 'length >= 20'; jqcheck "a genotype for the repeat on the fixture" eh.json '.LocusResults.SMOKE1.Variants.SMOKE1.Genotype | select(. != null and . != false and . != 0 and . != "" and . != [] and . != {})' GOLEFT_IMAGE - slice goleft indexcov --directory indexcov /in/HG002_slice.bam at_least "sample rows in the .ped" "$(awk '!/^#/ && NF >= 6' indexcov/indexcov-indexcov.ped | wc -l)" 1 MOSDEPTH_IMAGE - ref,bam mosdepth --by 500 --fast-mode --no-per-base --threads 4 --thresholds 1,5,10,15,20,30,50 md /in/mini.bam at_least "mean depth on chr20" "$(awk '$1 == "chr20" {print $4}' md.mosdepth.summary.txt)" 15 # Zenodo data (homology table and models, ~50 MB) is downloaded in the row, as setup.sh --parascopy-data does. -PARASCOPY_IMAGE net,also=PARASCOPY_DATA_VERSION fullref,slice sh /smoke/parascopy.sh at_least "rows of the SMN1/SMN2 profile" "$(grep -vc '^#' smn.tsv)" 1; py "an aggregate copy number of the SMN1/SMN2 region, with its quality" "import csv; rows = [r for r in csv.reader(open('smn.tsv'), delimiter='\t') if r and not r[0].startswith('#')]; print(rows); assert any(r[0] == 'chr5' and r[6] not in ('', '*') and r[7] not in ('', '*') for r in rows)" +PARASCOPY_IMAGE net,also=PARASCOPY_DATA_VERSION fullref,slice sh /smoke/parascopy.sh at_least "rows of the SMN1/SMN2 profile" "$(grep -vc '^#' smn.tsv)" 1; smoke_check "an aggregate copy number of the SMN1/SMN2 region, with its quality" parascopy smn.tsv # Paraphase reads PacBio HiFi only: the row runs its own demo, the HG01175 SMN1/SMN2 HiFi reads in the release's tests/data, # whose result docs/demo.md of that release publishes: two SMN1, one SMN2, one SMN with exons 7-8 deleted. # busybox wget does not validate TLS, so the two files are checked against their sha256 in Paraphase v4.1.0. -PARAPHASE_IMAGE net fullref v=${PARAPHASE_IMAGE##*:} && u=https://raw.githubusercontent.com/PacificBiosciences/paraphase/v${v%%--*}/tests/data/bams/HG01175_smn1_extracted.bam && wget -q -O hg.bam "$u" && wget -q -O hg.bam.bai "${u}.bai" && { printf '%s hg.bam\n%s hg.bam.bai\n' c9659364ad9e60e41ee4771cf92dc017313b6b75a1e08df3e8c0e95ef38eadca 8b5dd238fdf1cc508e85105ff2a157a2c3d73aa3f441d08afdba9458beecca51 | sha256sum -c || { echo 'the demo BAM or its index is not the file Paraphase v4.1.0 ships'; exit 1; }; } && paraphase -b hg.bam -r /in/ref.fa -o pp -p HG01175 -g smn1 -t 4 && cp pp/HG01175.paraphase.json smn1.json && rm -rf pp hg.bam hg.bam.bai py "SMN1, SMN2 and SMN exon 7-8 deletion copy numbers of HG01175 as Paraphase's demo publishes them (2, 1, 1)" "import json; s = json.load(open('smn1.json'))['smn1']; print(json.dumps(s)[:3000]); r = s['region_specific_info']; c = (r['smn1_cn'], r['smn2_cn'], r['smn_del78_cn']); print(c); assert c == (2, 1, 1)" +PARAPHASE_IMAGE net fullref v=${PARAPHASE_IMAGE##*:} && u=https://raw.githubusercontent.com/PacificBiosciences/paraphase/v${v%%--*}/tests/data/bams/HG01175_smn1_extracted.bam && wget -q -O hg.bam "$u" && wget -q -O hg.bam.bai "${u}.bai" && { printf '%s hg.bam\n%s hg.bam.bai\n' c9659364ad9e60e41ee4771cf92dc017313b6b75a1e08df3e8c0e95ef38eadca 8b5dd238fdf1cc508e85105ff2a157a2c3d73aa3f441d08afdba9458beecca51 | sha256sum -c || { echo 'the demo BAM or its index is not the file Paraphase v4.1.0 ships'; exit 1; }; } && paraphase -b hg.bam -r /in/ref.fa -o pp -p HG01175 -g smn1 -t 4 && cp pp/HG01175.paraphase.json smn1.json && rm -rf pp hg.bam hg.bam.bai jqcheck "SMN1, SMN2 and SMN exon 7-8 deletion copy numbers of HG01175 (2, 1, 1)" smn1.json '.smn1.region_specific_info | [.smn1_cn, .smn2_cn, .smn_del78_cn] == [2, 1, 1]' # MEGAnE's call needs a whole chromosome: on mini.fa's 500 kb it stops in its own code (IndexError). The e2e case mei-1 runs it on chr20. MEGANE_IMAGE - ref build_kmerset -fa /in/mini.fa -prefix mini -outdir /out/mk && { call_genotype_38 -h > help.txt 2>&1 || true; } at_least "repeated 32-mers in the k-mer set of mini.fa" "$(cat mk/mini.mi)" 1000; same "the .mk holds the 8-byte k-mers the .mi counts" "$(( $(wc -c < mk/mini.mk) / 8 ))" "$(cat mk/mini.mi)"; contains "call_genotype_38 takes the -mainchr step 50 passes" help.txt "-mainchr"; contains "and -no_sex_chr" help.txt "-no_sex_chr"; contains "and -only_ins" help.txt "-only_ins" # Step 52 on VNtyper's own test data, two BAMs read out of its Zenodo archive by range requests (tests/smoke/vntyper_testdata.py, @@ -63,19 +63,19 @@ VERIFYBAMID2_IMAGE - fullref,slice sh /smoke/verifybamid2.sh contains "with the GLNEXUS_IMAGE - - H='##fileformat=VCFv4.2\n##FILTER=\n##FILTER=\n##INFO=\n##FORMAT=\n##FORMAT=\n##FORMAT=\n##FORMAT=\n##FORMAT=\n##FORMAT=\n##FORMAT=\n##contig=\n#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\tFORMAT\t'; R='0/0:50:30:0,90,990'; printf "${H}A\nchr20\t1\t.\tA\t<*>\t0\t.\tEND=999\tGT:GQ:MIN_DP:PL\t$R\nchr20\t1000\t.\tA\tG,<*>\t40\tPASS\t.\tGT:GQ:DP:AD:VAF:PL\t0/1:40:30:15,15,0:0.5,0:40,0,40,990,990,990\nchr20\t1001\t.\tC\t<*>\t0\t.\tEND=2000\tGT:GQ:MIN_DP:PL\t$R\n" > A.g.vcf && printf "${H}B\nchr20\t1\t.\tA\t<*>\t0\t.\tEND=1499\tGT:GQ:MIN_DP:PL\t$R\nchr20\t1500\t.\tT\tC,<*>\t45\tPASS\t.\tGT:GQ:DP:AD:VAF:PL\t1/1:45:30:0,30,0:1,0:990,90,0,990,990,990\nchr20\t1501\t.\tG\t<*>\t0\t.\tEND=2000\tGT:GQ:MIN_DP:PL\t$R\n" > B.g.vcf && glnexus_cli --config DeepVariantWGS --dir db --threads 2 A.g.vcf B.g.vcf > joint.bcf same "records of the joint call of two DeepVariant-style gVCFs" "$(vcf_records joint.bcf)" 2; same "genotypes: A heterozygous at 1000, B homozygous ALT at 1500, each reference from the other's block" "$(bcf query -f '%POS[ %GT]\n' joint.bcf | tr '\n' ';')" "1000 0/1 0/0;1500 0/0 1/1;" # Step 38: a three-site trio phased by inheritance, no reads, as the module runs it. WHATSHAP_IMAGE - - H='##fileformat=VCFv4.2\n##contig=\n##FORMAT=\n#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\tFORMAT\tkid\tdad\tmum\n'; printf "${H}chr20\t100\t.\tA\tG\t50\tPASS\t.\tGT\t0/1\t0/0\t1/1\nchr20\t200\t.\tC\tT\t50\tPASS\t.\tGT\t0/1\t1/1\t0/0\nchr20\t300\t.\tG\tA\t50\tPASS\t.\tGT\t0/1\t0/1\t0/1\n" > trio.vcf && printf 'f\tkid\tdad\tmum\t1\t0\n' > trio.ped && whatshap phase --ped trio.ped --use-ped-samples -o phased.vcf.gz trio.vcf && python -c "import pysam; pysam.tabix_index('phased.vcf.gz', preset='vcf', force=True)" has "the kid by inheritance alone: 100 and 200 phased on opposite haplotypes, 300 (all three heterozygous) left unphased" "$(gzip -dc phased.vcf.gz | awk -F'\t' '!/^#/ {split($10, g, ":"); printf "%s ", g[1]}')" '^(0\|1 1\|0|1\|0 0\|1) 0/1 $'; nonempty "pysam indexed the phased VCF (the module and step 38 do the same)" phased.vcf.gz.tbi -HAP_IBD_IMAGE - - awk 'BEGIN { srand(7); printf "##fileformat=VCFv4.2\n##contig=\n##FORMAT=\n#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\tFORMAT\tA\tB\tC\tD\n" > "t.vcf"; for (j = 1; j <= 2000; j++) { pos = j * 10000; s = (rand() < 0.5); b1 = (j >= 500 && j <= 1500) ? s : (rand() < 0.5); printf "chr20\t%d\t.\tA\tG\t.\tPASS\t.\tGT\t%d|%d\t%d|%d\t%d|%d\t%d|%d\n", pos, s, (rand() < 0.5), b1, (rand() < 0.5), (rand() < 0.5), (rand() < 0.5), (rand() < 0.5), (rand() < 0.5) > "t.vcf"; printf "chr20\t.\t%.4f\t%d\n", pos / 1000000, pos > "g.map" } }' && hap-ibd gt=t.vcf map=g.map out=o > hapibd.log 2>&1 py "hap-ibd finds the planted 10 cM segment A and B share, and no other" "import gzip; rows = [l.split() for l in gzip.open('o.ibd.gz', 'rt')]; print(rows); ab = [r for r in rows if {r[0], r[2]} == {'A', 'B'}]; assert len(rows) == 1 and len(ab) == 1 and 8 <= float(ab[0][7]) <= 11, rows" +HAP_IBD_IMAGE - - awk 'BEGIN { srand(7); printf "##fileformat=VCFv4.2\n##contig=\n##FORMAT=\n#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\tFORMAT\tA\tB\tC\tD\n" > "t.vcf"; for (j = 1; j <= 2000; j++) { pos = j * 10000; s = (rand() < 0.5); b1 = (j >= 500 && j <= 1500) ? s : (rand() < 0.5); printf "chr20\t%d\t.\tA\tG\t.\tPASS\t.\tGT\t%d|%d\t%d|%d\t%d|%d\t%d|%d\n", pos, s, (rand() < 0.5), b1, (rand() < 0.5), (rand() < 0.5), (rand() < 0.5), (rand() < 0.5), (rand() < 0.5) > "t.vcf"; printf "chr20\t.\t%.4f\t%d\n", pos / 1000000, pos > "g.map" } }' && hap-ibd gt=t.vcf map=g.map out=o > hapibd.log 2>&1 smoke_check "hap-ibd finds the planted 10 cM segment A and B share, and no other" hap-ibd o.ibd.gz GATK_IMAGE - ref,bam,truth gatk --java-options -Xmx4g HaplotypeCaller -R /in/mini.fa -I /in/mini.bam -O gatk.vcf.gz -L chr20:1-449000 --native-pair-hmm-threads 4 -ERC NONE at_least "SNV recall against the GIAB truth" "$(snv_recall gatk.vcf.gz)" 0.9 PICARD_IMAGE - ref,truth,chain java -Xmx2g -jar /usr/picard/picard.jar LiftoverVcf I=/in/truth.vcf.gz O=lift.vcf.gz CHAIN=/in/identity.chain R=/in/mini.fa REJECT=rejected.vcf.gz WARN_ON_MISSING_CONTIG=true same "records lifted over an identity chain" "$(vcf_records lift.vcf.gz)" "$(vcf_records /in/truth.vcf.gz)" -PYTHON_IMAGE net,also=CYRIUS_VERSION cyrius sh /smoke/cyrius.sh same "Cyrius installed from the hash-locked file" "$(cat installed.txt)" "cyrius ${CYRIUS_VERSION}"; same "the TSV has a row for the sample" "$(tsv_cell HG002_cyp2d6.tsv Sample)" HG002_cyrius; py "Cyrius read depth from the BAM (median depth above 0)" "import json; d = json.load(open('HG002_cyp2d6.json'))['HG002_cyrius']; print('median depth', d['Median_depth'], 'MAD', d['Coverage_MAD'], 'genotype', d['Genotype']); assert float(d['Median_depth']) > 0" +PYTHON_IMAGE net,also=CYRIUS_VERSION cyrius sh /smoke/cyrius.sh same "Cyrius installed from the hash-locked file" "$(cat installed.txt)" "cyrius ${CYRIUS_VERSION}"; same "the TSV has a row for the sample" "$(tsv_cell HG002_cyp2d6.tsv Sample)" HG002_cyrius; jqcheck "Cyrius read depth from the BAM (median depth above 0)" HG002_cyp2d6.json '.["HG002_cyrius"].Median_depth | tonumber > 0' PLINK2_IMAGE - vcf sh /smoke/plink2.sh between "score sum over five ALT alleles of weight 1" "$(tsv_cell score.sscore SCORE1_SUM)" 5 10 # Step 26b: two planted groups for ADMIXTURE's supervised fit and the projection of a third person drawn from one of them. ADMIXTURE_IMAGE - - awk 'BEGIN { srand(7); for (j = 1; j <= 500; j++) { pa[j] = 0.1 + 0.8 * rand(); pb[j] = 1 - pa[j]; print 1, "s" j, 0, j * 1000 > "d.map"; print 1, "s" j, 0, j * 1000 > "t.map" }; for (i = 1; i <= 40; i++) { g = (i <= 20) ? "A" : "B"; print g > "d.pop"; printf "f%d i%d 0 0 0 -9", i, i > "d.ped"; for (j = 1; j <= 500; j++) { q = (g == "A") ? pa[j] : pb[j]; printf " %d %d", (rand() < q) ? 1 : 2, (rand() < q) ? 1 : 2 > "d.ped" }; printf "\n" > "d.ped" }; printf "t t 0 0 0 -9" > "t.ped"; for (j = 1; j <= 500; j++) printf " %d %d", (rand() < pa[j]) ? 1 : 2, (rand() < pa[j]) ? 1 : 2 > "t.ped"; printf "\n" > "t.ped" }' && admixture --supervised d.ped 2 > sup.log 2>&1 && cp d.2.P t.2.P.in && admixture -P t.ped 2 > proj.log 2>&1 contains "the supervised run on two planted groups converged" sup.log "Converged"; between "the projected person's share of the group it was drawn from" "$(awk '{print ($1 > $2 ? $1 : $2)}' t.2.Q)" 0.9 1 BEAGLE_IMAGE - - awk 'BEGIN { srand(3); h = "##fileformat=VCFv4.2\n##contig=\n##FORMAT=\n#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\tFORMAT"; printf "%s", h > "r.vcf"; for (i = 1; i <= 50; i++) printf "\tR%d", i > "r.vcf"; printf "\n" > "r.vcf"; printf "%s\tT\n", h > "t.vcf"; for (j = 1; j <= 300; j++) { printf "chr20\t%d\t.\tA\tG\t.\tPASS\t.\tGT", j * 1000 > "r.vcf"; for (i = 1; i <= 50; i++) printf "\t%d|%d", (rand() < 0.5), (rand() < 0.5) > "r.vcf"; printf "\n" > "r.vcf"; a = rand() < 0.5; b = rand() < 0.5; printf "chr20\t%d\t.\tA\tG\t.\tPASS\t.\tGT\t%d/%d\n", j * 1000, (a < b ? a : b), (a < b ? b : a) > "t.vcf" } }' && beagle gt=t.vcf ref=r.vcf out=o impute=false seed=1 nthreads=2 > beagle.log 2>&1 at_least "target genotypes Beagle returns phased" "$(gzip -dc o.vcf.gz | grep -v '^#' | grep -c '[01]|[01]')" 300; contains "Beagle finished" beagle.log "finished" MOCHA_IMAGE - ref { bcftools +mocha > usage.txt 2>&1 || true; } && awk 'BEGIN { print "##fileformat=VCFv4.2"; print "##contig="; print "##FORMAT="; print "##FORMAT="; printf "#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\tFORMAT\tHG002\n"; for (i = 1; i <= 100; i++) printf "chr20\t%d\t.\tA\tG\t.\tPASS\t.\tGT:AD\t0|1\t15,15\n", i * 4000 }' > v.vcf && bcftools +mochatools --no-version -Ov -o gc.vcf v.vcf -- -t GC -f /in/mini.fa contains "the plugin prints the version MOCHA_VERSION pins" usage.txt "version ${MOCHA_VERSION}"; same "records mochatools gave a GC content from the reference" "$(grep -v '^#' gc.vcf | grep -c 'GC=0[.][0-9]')" 100 # A planted query whose first half is drawn from POPA's frequencies and second half from POPB's. -RFMIX_IMAGE - - awk 'BEGIN { srand(5); h = "##fileformat=VCFv4.2\n##contig=\n##FORMAT=\n#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\tFORMAT"; printf "%s", h > "r.vcf"; for (i = 1; i <= 60; i++) { printf "\tR%d", i > "r.vcf"; print "R" i "\t" (i <= 30 ? "POPA" : "POPB") > "map.tsv" }; printf "\n" > "r.vcf"; printf "%s\tQ\n", h > "q.vcf"; for (j = 1; j <= 2000; j++) { pos = j * 10000; pa = 0.05 + 0.9 * rand(); pb = 1 - pa; printf "chr20\t%d\t.\tA\tG\t.\tPASS\t.\tGT", pos > "r.vcf"; for (i = 1; i <= 60; i++) { p = (i <= 30) ? pa : pb; printf "\t%d|%d", (rand() < p), (rand() < p) > "r.vcf" }; printf "\n" > "r.vcf"; p = (j <= 1000) ? pa : pb; printf "chr20\t%d\t.\tA\tG\t.\tPASS\t.\tGT\t%d|%d\n", pos, (rand() < p), (rand() < p) > "q.vcf"; printf "chr20\t%d\t%.4f\n", pos, pos / 1000000 > "g.map" } }' && rfmix -f q.vcf -r r.vcf -m map.tsv -g g.map -o o --chromosome=chr20 --n-threads=2 --random-seed=1 > rfmix.log 2>&1 py "RFMix paints the planted halves POPA then POPB" "import sys; rows = [l.split() for l in open('o.msp.tsv') if not l.startswith('#')]; codes = dict(x.split('=') for x in open('o.msp.tsv').readline().split(':', 1)[1].split()); at = lambda p: next((r[6:8] for r in rows if int(r[1]) <= p < int(r[2])), None); print(len(rows), 'windows', at(5000000), 'at 5 Mb and', at(15000000), 'at 15 Mb'); assert at(5000000) == [codes['POPA']] * 2 and at(15000000) == [codes['POPB']] * 2" +RFMIX_IMAGE - - awk 'BEGIN { srand(5); h = "##fileformat=VCFv4.2\n##contig=\n##FORMAT=\n#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\tFORMAT"; printf "%s", h > "r.vcf"; for (i = 1; i <= 60; i++) { printf "\tR%d", i > "r.vcf"; print "R" i "\t" (i <= 30 ? "POPA" : "POPB") > "map.tsv" }; printf "\n" > "r.vcf"; printf "%s\tQ\n", h > "q.vcf"; for (j = 1; j <= 2000; j++) { pos = j * 10000; pa = 0.05 + 0.9 * rand(); pb = 1 - pa; printf "chr20\t%d\t.\tA\tG\t.\tPASS\t.\tGT", pos > "r.vcf"; for (i = 1; i <= 60; i++) { p = (i <= 30) ? pa : pb; printf "\t%d|%d", (rand() < p), (rand() < p) > "r.vcf" }; printf "\n" > "r.vcf"; p = (j <= 1000) ? pa : pb; printf "chr20\t%d\t.\tA\tG\t.\tPASS\t.\tGT\t%d|%d\n", pos, (rand() < p), (rand() < p) > "q.vcf"; printf "chr20\t%d\t%.4f\n", pos, pos / 1000000 > "g.map" } }' && rfmix -f q.vcf -r r.vcf -m map.tsv -g g.map -o o --chromosome=chr20 --n-threads=2 --random-seed=1 > rfmix.log 2>&1 smoke_check "RFMix paints the planted halves POPA then POPB" rfmix o.msp.tsv # Step 43: one person with a planted 600 kb stretch carrying the Altai allele, against 100 stand-ins at the 1000 Genomes count. -IBDMIX_IMAGE - - awk 'BEGIN { srand(9); h = "chrom\tpos\tref\talt\tAltai\tS"; for (i = 1; i <= 100; i++) h = h sprintf("\tP%03d", i); print h > "g.txt"; print "S" > "s.txt"; for (i = 1; i <= 100; i++) printf "P%03d\n", i > "s.txt"; for (j = 0; j < 4000; j++) { pos = 1000000 + j * 1000; if (j % 2 == 0) { f = 0.02; a = 2; ac = 4 } else { f = 0.1 + 0.8 * rand(); a = (rand() < f) + (rand() < f); ac = int(f * 200 + 0.5) }; g = (rand() < f) + (rand() < f); if (j % 2 == 0 && pos >= 2000000 && pos < 2600000) g = 1; line = "22\t" pos "\tA\tG\t" a "\t" g; for (i = 1; i <= 100; i++) line = line "\t" (i <= int(ac / 2) ? 2 : (i == int(ac / 2) + 1 && ac % 2 ? 1 : 0)); print line > "g.txt" } }' && ibdmix -g g.txt -s s.txt -n Altai -o o.txt -d 4 -m 1 -a 0.01 -e 0.002 -c 2 > ibdmix.log 2>&1 py "IBDmix finds the planted 600 kb Altai stretch and nothing else" "rows = [l.split() for l in open('o.txt')][1:]; s = [r for r in rows if r[0] == 'S' and float(r[4]) >= 4 and int(r[3]) - int(r[2]) >= 50000]; cov = sum(max(0, min(int(r[3]), 2600000) - max(int(r[2]), 2000000)) for r in s); out = [r for r in s if int(r[3]) <= 2000000 or int(r[2]) >= 2600000]; print(len(s), 'segments,', cov, 'bp of the planted stretch,', len(out), 'outside'); assert cov >= 540000 and not out" +IBDMIX_IMAGE - - awk 'BEGIN { srand(9); h = "chrom\tpos\tref\talt\tAltai\tS"; for (i = 1; i <= 100; i++) h = h sprintf("\tP%03d", i); print h > "g.txt"; print "S" > "s.txt"; for (i = 1; i <= 100; i++) printf "P%03d\n", i > "s.txt"; for (j = 0; j < 4000; j++) { pos = 1000000 + j * 1000; if (j % 2 == 0) { f = 0.02; a = 2; ac = 4 } else { f = 0.1 + 0.8 * rand(); a = (rand() < f) + (rand() < f); ac = int(f * 200 + 0.5) }; g = (rand() < f) + (rand() < f); if (j % 2 == 0 && pos >= 2000000 && pos < 2600000) g = 1; line = "22\t" pos "\tA\tG\t" a "\t" g; for (i = 1; i <= 100; i++) line = line "\t" (i <= int(ac / 2) ? 2 : (i == int(ac / 2) + 1 && ac % 2 ? 1 : 0)); print line > "g.txt" } }' && ibdmix -g g.txt -s s.txt -n Altai -o o.txt -d 4 -m 1 -a 0.01 -e 0.002 -c 2 > ibdmix.log 2>&1 smoke_check "IBDmix finds the planted 600 kb Altai stretch and nothing else" ibdmix o.txt # setup.sh --ancestry-fine-panel reads gnomAD's bucket with it; the row depends on storage.googleapis.com being up. BCFTOOLS_HTTPS_IMAGE net - bcftools query -l https://storage.googleapis.com/gcp-public-data--gnomad/resources/hgdp_1kg/phased_haplotypes_v2/hgdp1kgp_chr22.filtered.SNV_INDEL.phased.shapeit5.bcf 2>/dev/null > samples.txt && bcftools view -H -r chr22:20000000-20010000 https://storage.googleapis.com/gcp-public-data--gnomad/resources/hgdp_1kg/phased_haplotypes_v2/hgdp1kgp_chr22.filtered.SNV_INDEL.phased.shapeit5.bcf 2>/dev/null | wc -l > n.txt && bcftools plugin -l > plugins.txt same "people in the panel's chr22 file, read over https" "$(wc -l < samples.txt | tr -d ' ')" 4091; at_least "records of a 10 kb region read through the remote index" "$(tr -d ' ' < n.txt)" 100; contains "the prune plugin setup.sh thins and LD-prunes with" plugins.txt "^prune$" # pgsc_calc's images (steps 25 and 26). pgscatalog-format writes the custom GRCh38 file step 25's og prs format makes as pgsc_calc's first step does; the other commands pgsc_calc calls must exist. @@ -90,5 +90,5 @@ VCFANNO_IMAGE - vcf,revel vcfanno -p 2 /smoke/vcfanno.toml /in/sample.vcf.gz > v SLIVAR_IMAGE - vcf sh /smoke/slivar.sh contains "compound-hets as step 31 calls it writes a VCF" step31.vcf "^#CHROM"; at_least "records in a compound-het pair" "$(grep -v "^#" comphet.vcf | grep -c "slivar_comphet=")" 2 # EIGENSOFT (step 44): smartpca builds PC1 from two made-up groups and projects a third sample (lsqproject). # The sites sit on two chromosomes with genetic positions (Morgans): smartpca's jackknife needs more than one block. -EIGENSOFT_IMAGE - - awk 'BEGIN { srand(1); for (s = 1; s <= 300; s++) { printf "rs%d %d %.3f %d A G\n", s, (s <= 150 ? 1 : 2), s * 0.002, s * 1000 > "t.snp"; l = ""; for (i = 1; i <= 20; i++) { p = (i <= 10) ? 0.15 : 0.85; l = l ((rand() < p) + (rand() < p)) } print l ((rand() < 0.15) + (rand() < 0.15)) > "t.geno" } for (i = 1; i <= 20; i++) printf "I%d U %s\n", i, (i <= 10 ? "PopA" : "PopB") > "t.ind"; print "X1 U Proj" > "t.ind"; print "PopA" > "pop.txt"; print "PopB" > "pop.txt" }' && printf 'genotypename: t.geno\nsnpname: t.snp\nindivname: t.ind\npoplistname: pop.txt\nevecoutname: t.evec\nevaloutname: t.eval\nlsqproject: YES\nnumoutevec: 2\nnumoutlieriter: 0\n' > t.par && smartpca -p t.par > smartpca.log same "rows in the .evec, 20 that build the axes and 1 projected" "$(grep -vc '^ *#' t.evec)" 21; py "PC1 separates the two groups and puts the projected sample (drawn like PopA) with PopA" "r = [l.split() for l in open('t.evec') if not l.lstrip().startswith('#')]; a = [float(x[1]) for x in r if x[-1] == 'PopA']; b = [float(x[1]) for x in r if x[-1] == 'PopB']; p = [float(x[1]) for x in r if x[-1] == 'Proj'][0]; ma, mb = sum(a) / len(a), sum(b) / len(b); print(ma, mb, p); assert max(a) < min(b) or min(a) > max(b); assert abs(p - ma) < abs(p - mb)" +EIGENSOFT_IMAGE - - awk 'BEGIN { srand(1); for (s = 1; s <= 300; s++) { printf "rs%d %d %.3f %d A G\n", s, (s <= 150 ? 1 : 2), s * 0.002, s * 1000 > "t.snp"; l = ""; for (i = 1; i <= 20; i++) { p = (i <= 10) ? 0.15 : 0.85; l = l ((rand() < p) + (rand() < p)) } print l ((rand() < 0.15) + (rand() < 0.15)) > "t.geno" } for (i = 1; i <= 20; i++) printf "I%d U %s\n", i, (i <= 10 ? "PopA" : "PopB") > "t.ind"; print "X1 U Proj" > "t.ind"; print "PopA" > "pop.txt"; print "PopB" > "pop.txt" }' && printf 'genotypename: t.geno\nsnpname: t.snp\nindivname: t.ind\npoplistname: pop.txt\nevecoutname: t.evec\nevaloutname: t.eval\nlsqproject: YES\nnumoutevec: 2\nnumoutlieriter: 0\n' > t.par && smartpca -p t.par > smartpca.log same "rows in the .evec, 20 that build the axes and 1 projected" "$(grep -vc '^ *#' t.evec)" 21; smoke_check "PC1 separates the groups and puts the projected sample with PopA" eigen t.evec EXOMISER_IMAGE - - exomiser analyse --help > analyse.txt 2>&1; java -version > java.txt 2>&1; true contains "exomiser analyse starts and lists --analysis, the option step 48 passes" analyse.txt "--analysis"; at_least "the image's Java runs (major version)" "$(grep -oE 'version "[0-9]+' java.txt | grep -oE '[0-9]+$' | head -n 1)" 17 From 4c2ac36a570fa58cb2fc4c757bc5f6dd086977c3 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 14:53:53 +0100 Subject: [PATCH 16/45] test(ci): prove the migrated readers against archived controls Bead: pgp-bs9u.38 --- .github/workflows/nextflow.yml | 4 +- rust/crates/og-dev/src/smoke.rs | 113 ++++++++++++++---- rust/crates/og-dev/tests/ci_readers.rs | 4 +- scripts/ci/e2e-case-paths.tsv | 2 +- tests/parity/cases.tsv | 14 +++ tests/parity/gen/ci-pharmcat.sh | 4 + .../golden/dev-pharmcat-called/truncated/exit | 1 + .../dev-pharmcat-called/truncated/stderr | 1 + .../golden/dev-smoke-check/eigen/stdout | 1 + .../golden/dev-smoke-check/hap-ibd/stdout | 1 + .../golden/dev-smoke-check/ibdmix/stdout | 1 + .../golden/dev-smoke-check/parascopy/stdout | 1 + .../golden/dev-smoke-check/rfmix/stdout | 1 + tests/parity/inputs/ci-readers/o.ibd | 1 + tests/parity/inputs/ci-readers/o.msp.tsv | 4 + tests/parity/inputs/ci-readers/o.txt | 2 + tests/parity/inputs/ci-readers/oracle.sh | 27 ++++- tests/parity/inputs/ci-readers/smn.tsv | 1 + tests/parity/inputs/ci-readers/t.evec | 4 + tests/parity/oracle.ref | 1 + tests/test_ci_readers.sh | 14 +++ 21 files changed, 169 insertions(+), 33 deletions(-) create mode 100755 tests/parity/gen/ci-pharmcat.sh create mode 100644 tests/parity/golden/dev-pharmcat-called/truncated/exit create mode 100644 tests/parity/golden/dev-pharmcat-called/truncated/stderr create mode 100644 tests/parity/golden/dev-smoke-check/eigen/stdout create mode 100644 tests/parity/golden/dev-smoke-check/hap-ibd/stdout create mode 100644 tests/parity/golden/dev-smoke-check/ibdmix/stdout create mode 100644 tests/parity/golden/dev-smoke-check/parascopy/stdout create mode 100644 tests/parity/golden/dev-smoke-check/rfmix/stdout create mode 100644 tests/parity/inputs/ci-readers/o.ibd create mode 100644 tests/parity/inputs/ci-readers/o.msp.tsv create mode 100644 tests/parity/inputs/ci-readers/o.txt create mode 100644 tests/parity/inputs/ci-readers/smn.tsv create mode 100644 tests/parity/inputs/ci-readers/t.evec create mode 100755 tests/test_ci_readers.sh diff --git a/.github/workflows/nextflow.yml b/.github/workflows/nextflow.yml index d4aaf9022..4e80b740c 100644 --- a/.github/workflows/nextflow.yml +++ b/.github/workflows/nextflow.yml @@ -119,8 +119,8 @@ jobs: - name: Validate nextflow_schema.json and the samplesheet schema if: steps.scope.outputs.run == 'true' run: | - jq empty nextflow_schema.json > /dev/null - jq empty assets/schema_input.json > /dev/null + jq -s -e 'length == 1' nextflow_schema.json > /dev/null + jq -s -e 'length == 1' assets/schema_input.json > /dev/null # The stub run below pulls every image of the default tools: the pinned # leg ran out of disk in it twice on 2026-11-05 (#300, job 127077180393; diff --git a/rust/crates/og-dev/src/smoke.rs b/rust/crates/og-dev/src/smoke.rs index 41d2c80b8..379b1d7be 100644 --- a/rust/crates/og-dev/src/smoke.rs +++ b/rust/crates/og-dev/src/smoke.rs @@ -1,6 +1,6 @@ //! Small, local smoke fixtures and table checks. No subprocess or network. use og_io::{AtomicFile, Lines, Utf8}; -use pycompat::csv::{DictReader, Quoting}; +use pycompat::csv::{DictReader, Quoting, Reader}; use std::io::Write; fn lines(path: &str) -> Result>, String> { @@ -27,6 +27,9 @@ fn fasta(path: &str) -> Result, String> { for line in lines(path)? { let line = line.map_err(|e| e.to_string())?; if !line.starts_with('>') { + if seq.len().saturating_add(line.trim().len()) > 1_003_000 { + return Err("smoke reference exceeds 1003000 bases".into()); + } seq.extend(line.trim().bytes().map(|b| b.to_ascii_uppercase())); } } @@ -157,7 +160,10 @@ fn csv_cell(args: &[String], out: &mut dyn Write) -> Result<(), String> { for row in DictReader::new(csv_lines(&args[0])?, ',', Quoting::Minimal) { let row = row.map_err(|e| e.to_string())?; if want.iter().all(|(k, v)| row.get(k) == Some(Some(v))) { - let val = row.get(&args[2]).flatten().ok_or("missing CSV column")?; + let val = row + .get(&args[2]) + .ok_or("missing CSV column")? + .unwrap_or("None"); writeln!(out, "{val}").map_err(|e| e.to_string())?; break; } @@ -170,17 +176,90 @@ fn fields(line: &str) -> Vec<&str> { fn float(s: Option<&&str>) -> Result { pycompat::num::parse_float(s.ok_or("short table row")?).map_err(|e| e.to_string()) } +#[derive(Default)] +struct PcaGroup { + n: usize, + first: f64, + min: f64, + max: f64, + sum: f64, + correction: f64, +} +impl PcaGroup { + fn add(&mut self, x: f64) { + if self.n == 0 { + self.first = x; + self.min = x; + self.max = x; + } + // Python's min/max keep the first element when a comparison is + // unordered (NaN), unlike f64::min/max, which skip NaN. + if x < self.min { + self.min = x; + } + if x > self.max { + self.max = x; + } + let t = self.sum + x; + self.correction += if self.sum.abs() >= x.abs() { + (self.sum - t) + x + } else { + (x - t) + self.sum + }; + self.sum = t; + self.n += 1; + } + fn mean(&self) -> f64 { + (self.sum + + if self.correction != 0. && self.correction.is_finite() { + self.correction + } else { + 0. + }) + / self.n as f64 + } +} +fn parascopy(path: &str, out: &mut dyn Write) -> Result { + let mut found = false; + let mut n = 0; + for row in Reader::new(csv_lines(path)?, '\t', Quoting::Minimal) { + let row = row.map_err(|e| e.to_string())?; + if row.is_empty() || row[0].starts_with('#') { + continue; + } + if row.len() < 8 { + return Err("short Parascopy row".into()); + } + found |= row[0] == "chr5" + && !["", "*"].contains(&row[6].as_str()) + && !["", "*"].contains(&row[7].as_str()); + n += 1; + } + writeln!( + out, + "parascopy: {n} rows; {}", + if found { "OK" } else { "FAIL" } + ) + .map_err(|e| e.to_string())?; + Ok(found) +} fn check(kind: &str, path: &str, out: &mut dyn Write) -> Result { + if kind == "parascopy" { + return parascopy(path, out); + } // The windows keep only their first covering row, matching Python's next(). let mut n = 0; let mut ab = 0; let mut ab_len = 0.; let mut coverage = 0.; let mut outside = 0; - let mut groups = [Vec::new(), Vec::new(), Vec::new()]; + let mut groups = [ + PcaGroup::default(), + PcaGroup::default(), + PcaGroup::default(), + ]; let mut codes = std::collections::BTreeMap::new(); let mut at = [None, None]; - let mut aggregate = false; for (i, line) in lines(path)?.enumerate() { let line = line.map_err(|e| e.to_string())?; let r = fields(&line); @@ -222,7 +301,7 @@ fn check(kind: &str, path: &str, out: &mut dyn Write) -> Result { Some(&"Proj") => 2, _ => continue, }; - groups[g].push(float(r.get(1))?); + groups[g].add(float(r.get(1))?); } "rfmix" => { if i == 0 { @@ -251,17 +330,6 @@ fn check(kind: &str, path: &str, out: &mut dyn Write) -> Result { } } } - "parascopy" => { - if r.is_empty() || r[0].starts_with('#') { - continue; - } - if r.len() < 8 { - return Err("short Parascopy row".into()); - } - aggregate |= - r[0] == "chr5" && !["", "*"].contains(&r[6]) && !["", "*"].contains(&r[7]); - n += 1; - } _ => return Err(format!("unknown smoke check {kind}")), } } @@ -269,26 +337,19 @@ fn check(kind: &str, path: &str, out: &mut dyn Write) -> Result { "hap-ibd" => n == 1 && ab == 1 && (8. ..=11.).contains(&ab_len), "ibdmix" => coverage >= 540000. && outside == 0, "eigen" => { - if groups.iter().any(Vec::is_empty) { + if groups.iter().any(|g| g.n == 0) { return Err("missing PCA group".into()); } let a = &groups[0]; let b = &groups[1]; - let p = groups[2][0]; - let ma = pycompat::num::sum(a.iter().copied()) / a.len() as f64; - let mb = pycompat::num::sum(b.iter().copied()) / b.len() as f64; - (a.iter().copied().fold(f64::NEG_INFINITY, f64::max) - < b.iter().copied().fold(f64::INFINITY, f64::min) - || a.iter().copied().fold(f64::INFINITY, f64::min) - > b.iter().copied().fold(f64::NEG_INFINITY, f64::max)) - && (p - ma).abs() < (p - mb).abs() + let p = groups[2].first; + (a.max < b.min || a.min > b.max) && (p - a.mean()).abs() < (p - b.mean()).abs() } "rfmix" => ["POPA", "POPB"].iter().enumerate().all(|(i, p)| { codes .get(*p) .is_some_and(|c| at[i].as_ref() == Some(&[c.clone(), c.clone()])) }), - "parascopy" => aggregate, _ => false, }; writeln!(out, "{kind}: {n} rows; {}", if ok { "OK" } else { "FAIL" }) diff --git a/rust/crates/og-dev/tests/ci_readers.rs b/rust/crates/og-dev/tests/ci_readers.rs index 72b8f8db8..25e452502 100644 --- a/rust/crates/og-dev/tests/ci_readers.rs +++ b/rust/crates/og-dev/tests/ci_readers.rs @@ -219,8 +219,8 @@ fn smoke_tables_reject_removed_properties() { ), ( "parascopy", - "chr5 1 2 a b c 4 99\n", - "chr5 1 2 a b c * 99\n", + "chr5\t1\t2\ta\tb\tc\t4\t99\n", + "chr5\t1\t2\ta\tb\tc\t*\t99\n", ), ] { let p = t.file("table", good); diff --git a/scripts/ci/e2e-case-paths.tsv b/scripts/ci/e2e-case-paths.tsv index 0faa36c6f..145392ffb 100644 --- a/scripts/ci/e2e-case-paths.tsv +++ b/scripts/ci/e2e-case-paths.tsv @@ -178,4 +178,4 @@ vntyper-1 assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.t y-private-report-1-private-and-tree assets/live/index.html assets/viewer/index.html bin/collect_summary.py bin/og bin/render_report.py bin/results_index.py bin/run_delta.py bin/traits_report.py bin/yleaf_run.py modules/local/html_report/* modules/local/y_haplogroup/* scripts/16-indexcov.sh scripts/24-html-report.sh scripts/37-y-haplogroup.sh scripts/generate-report.sh scripts/ci/fake-docker/lib.sh scripts/setup.sh y-str-1 assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/live/index.html assets/mito_confirmed.tsv assets/stub/sample.vcf.gz assets/viewer/index.html assets/ystr_panel.tsv bin/collect_summary.py bin/og bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/html_report/* modules/local/indexcov/* modules/local/vcf_precheck/* modules/local/y_str/* scripts/16-indexcov.sh scripts/24-html-report.sh scripts/45-y-str.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh y-tree-current-1-placement assets/live/index.html assets/viewer/index.html bin/og bin/yleaf_run.py scripts/16-indexcov.sh scripts/37-y-haplogroup.sh scripts/ci/fake-docker/lib.sh scripts/setup.sh -rust-z2e-readers rust/crates/og-dev/src/pharmcat.rs rust/crates/og-dev/src/nf_trace.rs rust/crates/og-dev/src/smoke.rs tests/e2e/lib.sh scripts/ci/image-smoke.sh scripts/ci/nf-trace-requests.tsv +rust-z2e-readers bin/og tests/e2e/lib.sh scripts/ci/image-smoke.sh scripts/ci/nf-trace-requests.tsv diff --git a/tests/parity/cases.tsv b/tests/parity/cases.tsv index 98a436f8d..04f0538e9 100644 --- a/tests/parity/cases.tsv +++ b/tests/parity/cases.tsv @@ -966,3 +966,17 @@ dev-nf-attempts retry ci-readers/oracle.sh,ci-readers/attempts.tsv attempts.tsv dev-smoke-mini fixture ci-readers/oracle.sh,gen:ci-smoke.sh slice.fa orig.fa mini.fa 450000 3000 orig.fa,mini.fa bytes PYTHON_IMAGE dev-smoke-long fixture ci-readers/oracle.sh,gen:ci-smoke.sh hap1.fa hap2.fa long.fq.gz long.fq.gz gz-content PYTHON_IMAGE dev-smoke-catalog fixture ci-readers/oracle.sh,gen:ci-smoke.sh slice.fa eh.json 449000 eh.json json PYTHON_IMAGE +dev-smoke-check hap-ibd ci-readers/oracle.sh,ci-readers/o.ibd hap-ibd o.ibd - bytes PYTHON_IMAGE +dev-smoke-check ibdmix ci-readers/oracle.sh,ci-readers/o.txt ibdmix o.txt - bytes PYTHON_IMAGE +dev-smoke-check rfmix ci-readers/oracle.sh,ci-readers/o.msp.tsv rfmix o.msp.tsv - bytes PYTHON_IMAGE +dev-smoke-check eigen ci-readers/oracle.sh,ci-readers/t.evec eigen t.evec - bytes PYTHON_IMAGE +dev-smoke-check parascopy ci-readers/oracle.sh,ci-readers/smn.tsv parascopy smn.tsv - bytes PYTHON_IMAGE +dev-pharmcat report-3.2.0 ci-readers/oracle.sh,gen:ci-pharmcat.sh report-3.2.0.json - bytes PYTHON_IMAGE +dev-pharmcat report-3.4.0 ci-readers/oracle.sh,gen:ci-pharmcat.sh report-3.4.0.json - bytes PYTHON_IMAGE +dev-pharmcat pharmcat-docs-example ci-readers/oracle.sh,gen:ci-pharmcat.sh pharmcat-docs-example.json - bytes PYTHON_IMAGE +dev-pharmcat-called report-3.2.0 ci-readers/oracle.sh,gen:ci-pharmcat.sh report-3.2.0.json --called - bytes PYTHON_IMAGE +dev-pharmcat-called report-3.4.0 ci-readers/oracle.sh,gen:ci-pharmcat.sh report-3.4.0.json --called - bytes PYTHON_IMAGE +dev-pharmcat-called pharmcat-docs-example ci-readers/oracle.sh,gen:ci-pharmcat.sh pharmcat-docs-example.json --called - bytes PYTHON_IMAGE +dev-pharmcat-lines report-3.2.0 ci-readers/oracle.sh,gen:ci-pharmcat.sh report-3.2.0.json --called-lines - bytes PYTHON_IMAGE +dev-pharmcat-lines report-3.4.0 ci-readers/oracle.sh,gen:ci-pharmcat.sh report-3.4.0.json --called-lines - bytes PYTHON_IMAGE +dev-pharmcat-lines pharmcat-docs-example ci-readers/oracle.sh,gen:ci-pharmcat.sh pharmcat-docs-example.json --called-lines - bytes PYTHON_IMAGE diff --git a/tests/parity/gen/ci-pharmcat.sh b/tests/parity/gen/ci-pharmcat.sh new file mode 100755 index 000000000..96abbd1b4 --- /dev/null +++ b/tests/parity/gen/ci-pharmcat.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd) +cp "$ROOT"/tests/fixtures/pharmcat/report-3.2.0.json "$ROOT"/tests/fixtures/pharmcat/report-3.4.0.json "$ROOT"/tests/fixtures/pharmcat/pharmcat-docs-example.json . diff --git a/tests/parity/golden/dev-pharmcat-called/truncated/exit b/tests/parity/golden/dev-pharmcat-called/truncated/exit new file mode 100644 index 000000000..0cfbf0888 --- /dev/null +++ b/tests/parity/golden/dev-pharmcat-called/truncated/exit @@ -0,0 +1 @@ +2 diff --git a/tests/parity/golden/dev-pharmcat-called/truncated/stderr b/tests/parity/golden/dev-pharmcat-called/truncated/stderr new file mode 100644 index 000000000..419989349 --- /dev/null +++ b/tests/parity/golden/dev-pharmcat-called/truncated/stderr @@ -0,0 +1 @@ +unreadable report: Expecting value: byte 9 diff --git a/tests/parity/golden/dev-smoke-check/eigen/stdout b/tests/parity/golden/dev-smoke-check/eigen/stdout new file mode 100644 index 000000000..112b6bd29 --- /dev/null +++ b/tests/parity/golden/dev-smoke-check/eigen/stdout @@ -0,0 +1 @@ +eigen: 0 rows; OK diff --git a/tests/parity/golden/dev-smoke-check/hap-ibd/stdout b/tests/parity/golden/dev-smoke-check/hap-ibd/stdout new file mode 100644 index 000000000..efcc52c8d --- /dev/null +++ b/tests/parity/golden/dev-smoke-check/hap-ibd/stdout @@ -0,0 +1 @@ +hap-ibd: 1 rows; OK diff --git a/tests/parity/golden/dev-smoke-check/ibdmix/stdout b/tests/parity/golden/dev-smoke-check/ibdmix/stdout new file mode 100644 index 000000000..062ef2efc --- /dev/null +++ b/tests/parity/golden/dev-smoke-check/ibdmix/stdout @@ -0,0 +1 @@ +ibdmix: 1 rows; OK diff --git a/tests/parity/golden/dev-smoke-check/parascopy/stdout b/tests/parity/golden/dev-smoke-check/parascopy/stdout new file mode 100644 index 000000000..dcdac7a2a --- /dev/null +++ b/tests/parity/golden/dev-smoke-check/parascopy/stdout @@ -0,0 +1 @@ +parascopy: 1 rows; OK diff --git a/tests/parity/golden/dev-smoke-check/rfmix/stdout b/tests/parity/golden/dev-smoke-check/rfmix/stdout new file mode 100644 index 000000000..550866716 --- /dev/null +++ b/tests/parity/golden/dev-smoke-check/rfmix/stdout @@ -0,0 +1 @@ +rfmix: 2 rows; OK diff --git a/tests/parity/inputs/ci-readers/o.ibd b/tests/parity/inputs/ci-readers/o.ibd new file mode 100644 index 000000000..2d1547c47 --- /dev/null +++ b/tests/parity/inputs/ci-readers/o.ibd @@ -0,0 +1 @@ +A 1 B 1 20 1 10000000 10 diff --git a/tests/parity/inputs/ci-readers/o.msp.tsv b/tests/parity/inputs/ci-readers/o.msp.tsv new file mode 100644 index 000000000..91c77f4bc --- /dev/null +++ b/tests/parity/inputs/ci-readers/o.msp.tsv @@ -0,0 +1,4 @@ +#codes: POPA=0 POPB=1 +#h +20 0 10000000 0 0 0 0 0 +20 10000000 20000000 0 0 0 1 1 diff --git a/tests/parity/inputs/ci-readers/o.txt b/tests/parity/inputs/ci-readers/o.txt new file mode 100644 index 000000000..a153febf0 --- /dev/null +++ b/tests/parity/inputs/ci-readers/o.txt @@ -0,0 +1,2 @@ +h +S 22 2000000 2600000 4 diff --git a/tests/parity/inputs/ci-readers/oracle.sh b/tests/parity/inputs/ci-readers/oracle.sh index eeb13677e..a5c238164 100644 --- a/tests/parity/inputs/ci-readers/oracle.sh +++ b/tests/parity/inputs/ci-readers/oracle.sh @@ -2,6 +2,27 @@ # Extract the archived heredoc, not a second maintained implementation. set -euo pipefail mode=$1 prog=$2; shift 2 +if [ "$mode" = check ]; then + kind=$1 + case "$kind" in + hap-ibd) var=HAP_IBD_IMAGE; gzip -c o.ibd > o.ibd.gz ;; + ibdmix) var=IBDMIX_IMAGE ;; + rfmix) var=RFMIX_IMAGE ;; + eigen) var=EIGENSOFT_IMAGE ;; + parascopy) var=PARASCOPY_IMAGE ;; + *) exit 2 ;; + esac + expect=$(awk -F '\t' -v v="$var" '$1 == v { print $5 }' "$prog") + [ -n "$expect" ] || exit 2 + # Compare the assertions, not their debug dumps. The Rust diagnostics + # have explicit goldens; a failed Python assertion still fails the case. + py() { python3 -I -c "$2" > /dev/null; } + same() { :; } + at_least() { :; } + eval "$expect" + [ "$kind" != hap-ibd ] || rm -f o.ibd.gz + exit 0 +fi case "$mode" in diplotypes) start='^import json, sys$' ;; called-lines) start='^import json, sys$' ;; @@ -17,8 +38,10 @@ esac # The workflow's first inline reader is requests, the second is attempts. occurrence=1 [ "$mode" != attempts ] || occurrence=2 -awk -v re="$start" -v want="$occurrence" ' - { sub(/^ /, "") } +indent=0 +case "$mode" in requests|attempts) indent=1 ;; esac +awk -v re="$start" -v want="$occurrence" -v indent="$indent" ' + { if (indent) sub(/^ /, "") } $0 ~ re { n++; if (n == want) on=1 } on && /^PY$/ { exit } on { print } diff --git a/tests/parity/inputs/ci-readers/smn.tsv b/tests/parity/inputs/ci-readers/smn.tsv new file mode 100644 index 000000000..cc8e41ff4 --- /dev/null +++ b/tests/parity/inputs/ci-readers/smn.tsv @@ -0,0 +1 @@ +chr5 1 2 a b c 4 99 diff --git a/tests/parity/inputs/ci-readers/t.evec b/tests/parity/inputs/ci-readers/t.evec new file mode 100644 index 000000000..b0b52a5e7 --- /dev/null +++ b/tests/parity/inputs/ci-readers/t.evec @@ -0,0 +1,4 @@ +#h +a -2 0 PopA +b 2 0 PopB +p -1 0 Proj diff --git a/tests/parity/oracle.ref b/tests/parity/oracle.ref index 4932df874..9a33070d5 100644 --- a/tests/parity/oracle.ref +++ b/tests/parity/oracle.ref @@ -72,3 +72,4 @@ dev-nf-attempts 669a8196cf4c377200f9b4af9c235ef85fb774a1 file:.github/workflows/ dev-smoke-mini 669a8196cf4c377200f9b4af9c235ef85fb774a1 file:scripts/ci/image-smoke.sh sh -c 'exec bash ./oracle.sh mini-ref "$0" "$@"' dev smoke mini-ref dev-smoke-long 669a8196cf4c377200f9b4af9c235ef85fb774a1 file:scripts/ci/image-smoke.sh sh -c 'exec bash ./oracle.sh long-reads "$0" "$@"' dev smoke long-reads dev-smoke-catalog 669a8196cf4c377200f9b4af9c235ef85fb774a1 file:scripts/ci/image-smoke.sh sh -c 'exec bash ./oracle.sh eh-catalog "$0" "$@"' dev smoke eh-catalog +dev-smoke-check 669a8196cf4c377200f9b4af9c235ef85fb774a1 file:tests/smoke/commands.tsv sh -c 'exec bash ./oracle.sh check "$0" "$@"' dev smoke check diff --git a/tests/test_ci_readers.sh b/tests/test_ci_readers.sh new file mode 100755 index 000000000..677656142 --- /dev/null +++ b/tests/test_ci_readers.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# The migrated callers run on small public/synthetic inputs without Docker. +set -euo pipefail +REPO=$(cd "$(dirname "$0")/.." && pwd) +OG=${OG:-${REPO}/bin/og} +t=$(mktemp -d) +trap 'rm -rf "$t"' EXIT +export REPO OG +GENOME_DIR=$t SAMPLE=synthetic FIXTURE_DIR=$t E2E_WORK=$t E2E_NOTES=$t/notes \ + bash "$REPO/tests/e2e/rust-z2e-readers.sh" +bash "$REPO/scripts/ci/image-smoke.sh" --self-test +mkdir -p "$t/bash/vcf" +printf '{"genes":{"CYP2C19":{"sourceDiplotypes":[{"allele1":{"name":"*1"}}]}}}\n' > "$t/bash/vcf/s.report.json" +PARITY_ITEMS=pharmcat bash "$REPO/scripts/ci/parity-diff.sh" --self-test "$t/bash" s From 1da958749fb57372a075de4cc2f7c84570adc9d5 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 14:53:53 +0100 Subject: [PATCH 17/45] test(ci): plant an unreadable-report success control Bead: pgp-bs9u.38 --- rust/crates/og-dev/src/pharmcat.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/crates/og-dev/src/pharmcat.rs b/rust/crates/og-dev/src/pharmcat.rs index 10301dc3d..d7777b934 100644 --- a/rust/crates/og-dev/src/pharmcat.rs +++ b/rust/crates/og-dev/src/pharmcat.rs @@ -107,7 +107,7 @@ pub fn cli(args: &[String], out: &mut dyn Write, err: &mut dyn Write) -> u8 { Ok(r) => r, Err(e) => { let _ = writeln!(err, "unreadable report: {e}"); - return 2; + return 0; } }; let result = match args.get(2).map(String::as_str) { From 6b5618a2513308c58a9d0c9db0fdfc3bf028ca46 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 14:54:13 +0100 Subject: [PATCH 18/45] test(ci): keep the deliberate mini-reference golden Bead: pgp-bs9u.38 --- .../golden/dev-smoke-mini/fixture/mini.fa | 8385 +++++++++++++++++ 1 file changed, 8385 insertions(+) create mode 100644 tests/parity/golden/dev-smoke-mini/fixture/mini.fa diff --git a/tests/parity/golden/dev-smoke-mini/fixture/mini.fa b/tests/parity/golden/dev-smoke-mini/fixture/mini.fa new file mode 100644 index 000000000..24e34d335 --- /dev/null +++ b/tests/parity/golden/dev-smoke-mini/fixture/mini.fa @@ -0,0 +1,8385 @@ +>chr20 +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +CACACACACACACACACACACACAGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +AGACAAGCGAACTTTAGTGTTGCACAAAAGGTCTCGACAGGATTGAAAGTCTTATCGCAG +CTCATCACCCGGAACAACGTGTACTAGGTACAACCTGATATTTTACGCATATAATCTGCG +AGCCCTCCAAGCTGTGGTCCTTCGAGGGTTATAAACTGGCTTATATCACTTGCTGAGGGG +GCATTCAGTATAGAAGTTTTACTTAGCCTGATTGCACTCCCGCCTGACCGGTCAAGATAG +GGTTACTTCGCACGACATACCTCATTGCCGTTACCAACCCAAATTTTAGCAAGTCTTAAT +GAGCTCCATTAACCACACAACGGAGTACTATAGCGTTACACTGAAGGCGTCATAGGACGT +CCACGATCAGTAATCGTCGGGAACGCATATTATTAGGTAGACCAACTTGTGTAGTTCAAA +TTAGCTCGTGAGCTTCGGAAACGCATTAGAGAATCGCCCGTTGGCGCTTTTGCCCTTGAC +GCACAACACCGGCGCTATAGTAGGATCCCGCATGAGAGGTCTGACGTTGCCTTGAAATTC +TGCTCGTCAAGGACGTGATTCACGTGCCCGAGCTACTTGCCCCATATGGTGCAGGAGATC +AAAGAGTCTTTACGAGCCGATGCCGGTTTAATACTGTAGCAGGACGCCATCCCCATTGGA +ACCGACGGTTCTCATACTCAGGAATTGGGACATGGGAGGACTATTCACGTAGCTCCCCAA +CGAGTCAAAAAGTACAATACCGATTCAATCACTCTACGCATCCCTAGTAGCACTCCCGAC +CAAGAATGGTTCGTATCAGGAGGCGAGGCCACCAGTAAACGTTCACCTAAAAATTTTTCA +ATCTCCGGACGTGATAATGCACATGCCATTGCCAGCCACCTCCAGACTTGGTAGGATGCG +CCGGGACCCTTACACCTGCTTGGCGTGCTATCCACCGCAAAAAGGAAACATATTCCGGCG +TTACACGTCGTCAAGACCATTCACACAGTTCTACTTCGAGCCAGAATAAATCGGACGTTT +TTCTACTCCGTATGCTCGGCATGCGCGTTCGAGCTTTACGCAGGAACGGTAGCGCTCTAA +CGTTCCGCGGCCCAGCCGGCAACTCGCTGCAGAGAAAGTAATAACAATGCCCAAGGGTCG +AGTTAGAGAAACCACAGGTGACTGCTGAAAGTCCCCAGCTGTCAGCTTGATCCACTCCTT +ATACTGCAGAACTTCGCGCCTGGGCGGTATCGAAAGTCCTGCAATGCCGCCTGCAACACG +GCTCGGCTACGCTCCTTAACTATTGGTGGCAACCATGCCAAGAATCATTCCTGATTTTAG +TATCCTAGCATTTACCAACTCCCACTCTTATATGATGGTAGACAAGTTCAGAAGTGCCAT +AAGACACAGGAGGGTGGGAGGACTATCTCATTAAGTCGTATTAACAACGCTCCTACGCGG +AACCCCATGACCTTAGCATTAACATTTCAAATCCATGCATGGAGCACATGGCGAGAATTC +AAACCATCACAGCTGGGGCGCTAATGGCGCGACCGCTATTATCTATTATTATAGCCTCTG +TCCTCGCTCCTATACTGAGAAAATTTCCAGACAACCTTGGCTGGTAGGTAAATATCCGGT +CATTTAAGCCTCTTGCCCCCCTTTCATGCCATCTTATATATATTCTGTCTTCCTCCTTGC +CATTCGAGACCAAAGGTCCGCCCCTCCAATGCGAGAGTGGCGCCAAAGAGACCCCAATCC +TATAGTACTCTAATGTACCGAGCGTTCGAAGAAAGCATACGATAGCGGATTATTAGATCA +GCATACCCCACCACTTACTTACGACCTTAAGATCCAACTGTCAACCAGATTAATCGCTTC +AAAAGGTTCGCGCACATCTCAAACAGAGATTTCATAGAACACGCGCGACTCACATGTGTG +GCGAGGCACGTCGCTGGCAGACGTATCCTACGTCAATGGAATTGCTCCTTGCCAGTGCTT +CAGAAAAGGGCTGCGGTTAATCGTAAAATGGTGACTAGCAGTAATCCGAAGTGTCAGTTA +CACGACGTCGTTTCTACAGAAAGTGTTAGCGACAGCTTAAGGGGTAATACAGAAAGTCAC +GTAGTGTGCAGGAATGTTAGTGGACAACCTGAGATAAACCCACCCTATACGTGATGCATT +CTGTTGTCACGTTTCAAACCACTGCCATCGGATACTTCTCCCAGGACTTCACGTCGCATC +CCAGCATAGGTCTCCTTCATCGCCGTCGATGGCCATTAGGGATGGTTGAGTGTATAACAA +TACAAACGGGTTACCCATAGTGCATAAATTGCTGGGATGGCCTAACTACTCCCTTCCTTA +AAGCGCCACGCAAGGGAGCTCCGTCAGGTCCAGATTTTCCTTGACTTCGCATCTCAATGG +GGTCTTGTTGTCGGGGAATCTAGATGCGTCTTCTTGAGGACATCATGGCCTGACTGGAAC +GATGCGTGTGCGTGCCACGTTAATTAGATAGATCATCGACGTGTATCCACGATAATAATC +AGCATAATGTCTCTACAGCCCCGTGTCGGGAAAACATGGTAAAGTGCTCGTTAATTACCC +GCGGCGACGTCTACTGTCGAGAGGGTTCCCGCATTGTCCGTGTGAGGGGCCCATGCTCCC +ACTGTGACCCATTATGTACTACCGTAGTAGAACGACCGTCTGATTAGTCTGCTCCTTAGA +TAGGTCATAAGGTGTCACCGGGTGCCACAGCCTGTTTTAAGAATAAGCCCTCAGCACTAA +CAAATACCCGAGCATTCCGGTGATGTCCTGCTTTTGCCGATTCTACCCTTAAACCTGTGT +GGTCAGCGGTGAGAGTGGGGCGATGGCGCATTAAAGGAAGTGGTTACGGTCTTCTCCTAT +TCACCGTCGGCCTTTGTCCCAATTGCCAGAGATACAAAAAAGTGGGGCTGTTACCGGCTG +CTCGCTGCCAGTCCGTGAGAGGCGGCGGTGCGTAGGACCACCAACGTCCGCCAGTCGCGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +GGGGGGGGGGGGGGGGGGGG From 69a0b53dad00dbdde8d8354b32d145147c6ddb01 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 14:55:04 +0100 Subject: [PATCH 19/45] fix(ci): restore unreadable-report failure after the red control Reverts the planted success control in 1da95874. Bead: pgp-bs9u.38 --- rust/crates/og-dev/src/pharmcat.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/crates/og-dev/src/pharmcat.rs b/rust/crates/og-dev/src/pharmcat.rs index d7777b934..10301dc3d 100644 --- a/rust/crates/og-dev/src/pharmcat.rs +++ b/rust/crates/og-dev/src/pharmcat.rs @@ -107,7 +107,7 @@ pub fn cli(args: &[String], out: &mut dyn Write, err: &mut dyn Write) -> u8 { Ok(r) => r, Err(e) => { let _ = writeln!(err, "unreadable report: {e}"); - return 0; + return 2; } }; let result = match args.get(2).map(String::as_str) { From acdef18326663aba80889eb19bab67a84f443958 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 14:58:15 +0100 Subject: [PATCH 20/45] test(ci): repeat the unreadable-report red control in Guard Bead: pgp-bs9u.38 --- .github/workflows/guard.yml | 11 +++++++++++ rust/crates/og-dev/Cargo.toml | 4 ++++ rust/crates/og-dev/src/pharmcat.rs | 6 +++++- rust/crates/og-dev/tests/ci_readers.rs | 23 +++++++++++++++++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) diff --git a/.github/workflows/guard.yml b/.github/workflows/guard.yml index 304360fbd..30e960246 100644 --- a/.github/workflows/guard.yml +++ b/.github/workflows/guard.yml @@ -279,6 +279,17 @@ jobs: grep -E 'planted_failure.*FAILED' "$log" || { cat "$log"; echo "ERROR: the run failed, but not on the planted test"; exit 1; } echo "Control: the planted test failed, as it must." + - name: Control (an unreadable PharmCAT report reported as success must fail) + run: | + log="${RUNNER_TEMP}/ci-reader-control.log" + rc=0 + cargo test --locked --no-fail-fast -p og-dev --features planted-ci-reader --test ci_readers \ + unreadable_pharmcat_is_unassessed > "$log" 2>&1 || rc=$? + cat "$log" + [ "$rc" -ne 0 ] || { echo "ERROR: the planted unreadable-report success passed"; exit 1; } + grep -E 'unreadable_pharmcat_is_unassessed.*FAILED' "$log" + echo "Control: the unreadable-report success was reported as a failed test." + - name: cargo test run: cargo test --workspace --locked --no-fail-fast diff --git a/rust/crates/og-dev/Cargo.toml b/rust/crates/og-dev/Cargo.toml index 20bcd7e4c..80e87d78d 100644 --- a/rust/crates/og-dev/Cargo.toml +++ b/rust/crates/og-dev/Cargo.toml @@ -6,6 +6,10 @@ edition.workspace = true license.workspace = true publish.workspace = true +# Only the CI red control enables this fault. Normal builds keep it off. +[features] +planted-ci-reader = [] + [dependencies] pycompat.workspace = true og-io.workspace = true diff --git a/rust/crates/og-dev/src/pharmcat.rs b/rust/crates/og-dev/src/pharmcat.rs index 10301dc3d..87303f19c 100644 --- a/rust/crates/og-dev/src/pharmcat.rs +++ b/rust/crates/og-dev/src/pharmcat.rs @@ -107,7 +107,11 @@ pub fn cli(args: &[String], out: &mut dyn Write, err: &mut dyn Write) -> u8 { Ok(r) => r, Err(e) => { let _ = writeln!(err, "unreadable report: {e}"); - return 2; + return if cfg!(feature = "planted-ci-reader") { + 0 + } else { + 2 + }; } }; let result = match args.get(2).map(String::as_str) { diff --git a/rust/crates/og-dev/tests/ci_readers.rs b/rust/crates/og-dev/tests/ci_readers.rs index 25e452502..18a09ea89 100644 --- a/rust/crates/og-dev/tests/ci_readers.rs +++ b/rust/crates/og-dev/tests/ci_readers.rs @@ -292,3 +292,26 @@ fn smoke_generators_keep_coordinates_and_reverse_complements() { 2 ); } + +#[test] +fn pca_nonfinite_values_follow_python_comparisons() { + let t = Temp::new(); + for (name, extra) in [ + ("nan", "n NaN 0 PopA\n"), + ("inf", "n inf 0 PopA\n"), + ("overflow", "n 1e400 0 PopA\n"), + ] { + let p = t.file( + name, + &format!("#h\n{extra}a -2 0 PopA\nb 2 0 PopB\np -1 0 Proj\n"), + ); + assert_eq!(run(smoke::cli, &["check", "eigen", &p]).0, 1, "{name}"); + } + let p = t.file("short.csv", "Type,Value\nSNP\n"); + assert_eq!( + run(smoke::cli, &["csv-cell", &p, "Type=SNP", "Value"]).1, + "None\n" + ); + let p = t.file("quoted.tsv", "# a comment\n\"chr5\"\t1\t2\ta\tb\tc\t\t99\n"); + assert_eq!(run(smoke::cli, &["check", "parascopy", &p]).0, 1); +} From 985b2f1402fe3cf7a11deba22ff8f39c038837fb Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 15:05:35 +0100 Subject: [PATCH 21/45] feat(traits): evaluate trait and haplotype catalogs in Rust Bead: pgp-bs9u.25 --- rust/crates/og-steps/src/lib.rs | 3 + rust/crates/og-steps/src/traits.rs | 1351 ++++++++++++++++++++++++++++ rust/og/src/main.rs | 4 + tests/parity/cases.tsv | 27 + tests/parity/gen/traits.sh | 42 + tests/parity/oracle.ref | 1 + 6 files changed, 1428 insertions(+) create mode 100644 rust/crates/og-steps/src/traits.rs create mode 100644 tests/parity/gen/traits.sh diff --git a/rust/crates/og-steps/src/lib.rs b/rust/crates/og-steps/src/lib.rs index f72a1b34f..fae338970 100644 --- a/rust/crates/og-steps/src/lib.rs +++ b/rust/crates/og-steps/src/lib.rs @@ -26,3 +26,6 @@ pub mod pgx_rules; pub mod prs; pub mod pypgx_summary; pub mod sv; + +/// Step 42 traits and genotype rules. +pub mod traits; diff --git a/rust/crates/og-steps/src/traits.rs b/rust/crates/og-steps/src/traits.rs new file mode 100644 index 000000000..145e69151 --- /dev/null +++ b/rust/crates/og-steps/src/traits.rs @@ -0,0 +1,1351 @@ +//! Step 42: bounded site genotypes, haplotype rules and read-backed phase. +use indexmap::IndexMap; +use og_io::{AtomicFile, Lines, Utf8}; +use pycompat::csv::{Quoting, Reader, Writer}; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use std::io::Write; + +const COLUMNS: &[&str] = &[ + "chrom", "pos", "rsid", "gene", "ref", "alt", "trait", "hom_ref", "het", "hom_alt", "ancestry", + "note", "pmids", +]; +const OUT: &[&str] = &[ + "rsid", "gene", "chrom", "pos", "trait", "genotype", "status", "reading", "ancestry", "note", + "pmids", +]; +const RULE_OUT: &[&str] = &[ + "rule", + "gene", + "trait", + "rsids", + "chrom", + "pos", + "genotypes", + "gq", + "phase", + "result", + "status", + "reading", + "ancestry", + "note", + "pmids", +]; +type Row = BTreeMap; +type Key = (String, String); +type Genotypes = HashMap; +type Phased = HashMap; +type Result = std::result::Result; + +#[derive(Clone, Debug)] +struct Site { + row: Row, + line: usize, +} +impl Site { + fn key(&self) -> Key { + (self.row["chrom"].clone(), self.row["pos"].clone()) + } +} +#[derive(Clone, Debug)] +struct Hap { + name: String, + alleles: Vec, + line: usize, +} +#[derive(Clone, Debug)] +struct Combo { + key: String, + label: String, + reading: String, + line: usize, +} +#[derive(Clone, Debug)] +struct Rule { + id: String, + meta: Row, + line: usize, + sites: Vec, + haps: Vec, + combos: Vec, +} +#[derive(Clone, Debug, PartialEq, Eq)] +enum Status { + Called, + NotCovered, + OtherAllele, + Unphased, + OtherHaplotype, +} +impl Status { + fn text(&self) -> &'static str { + match self { + Self::Called => "called", + Self::NotCovered => "not_covered", + Self::OtherAllele => "other_allele", + Self::Unphased => "unphased", + Self::OtherHaplotype => "other_haplotype", + } + } +} +#[derive(Clone, Debug)] +struct SiteGt { + alleles: Vec, + status: Status, + gq: String, + snv: bool, +} +impl Default for SiteGt { + fn default() -> Self { + Self { + alleles: vec![], + status: Status::NotCovered, + gq: ".".into(), + snv: false, + } + } +} +fn row(keys: &[&str], values: &[String]) -> Row { + keys.iter() + .zip(values) + .map(|(k, v)| (k.to_string(), v.clone())) + .collect() +} +fn records(path: &str) -> Result)>> { + // Parse even the header as a CSV record. Comments do not enter the parser. + let mut lines = Lines::new(og_io::open(path).map_err(|e| e.to_string())?, Utf8::Strict); + let mut text = String::new(); + let mut number = 0; + let mut out = Vec::new(); + while lines.read_line(&mut text).map_err(|e| e.to_string())? { + number += 1; + if text.trim_end_matches('\n').is_empty() || text.starts_with('#') { + continue; + } + let cells = Reader::new( + std::iter::once(Ok::<_, std::io::Error>(text.clone())), + '\t', + Quoting::Minimal, + ) + .next() + .ok_or("empty record")? + .map_err(|e| e.to_string())?; + out.push((number, cells)); + } + Ok(out) +} +fn read_table(path: &str) -> Result> { + let mut input = records(path)?.into_iter(); + let (n, header) = input + .next() + .ok_or_else(|| format!("{path}: no header line"))?; + if header != COLUMNS { + return Err(format!("{path}:{n}: unexpected traits header")); + } + input + .map(|(line, c)| { + if c.len() != COLUMNS.len() { + return Err(format!( + "{path}:{line}: {} columns, expected {}", + c.len(), + COLUMNS.len() + )); + } + Ok(Site { + row: row(COLUMNS, &c), + line, + }) + }) + .collect() +} +fn read_rules(path: &str) -> Result> { + let mut rules: IndexMap = IndexMap::new(); + for (line, c) in records(path)? { + let fields: &[&str] = match c[0].as_str() { + "rule" => &["gene", "trait", "needs_phase", "ancestry", "note", "pmids"], + "site" => &["rsid", "chrom", "pos", "ref", "alt"], + "haplotype" => &["name", "alleles"], + "combination" => &["diplotypes", "label", "reading"], + kind => { + return Err(format!( + "{path}:{line}: kind '{kind}' is not one of rule, site, haplotype, combination" + )); + } + }; + if c.len() != fields.len() + 2 { + return Err(format!( + "{path}:{line}: a {} line has {} columns, expected {}", + c[0], + c.len(), + fields.len() + 2 + )); + } + let id = &c[1]; + if c[0] == "rule" { + if rules.contains_key(id) { + return Err(format!("{path}:{line}: rule {id} has two rule lines")); + } + rules.insert( + id.clone(), + Rule { + id: id.clone(), + meta: row(fields, &c[2..]), + line, + sites: vec![], + haps: vec![], + combos: vec![], + }, + ); + } else { + let r = rules.get_mut(id).ok_or_else(|| { + format!( + "{path}:{line}: a {} line of rule '{id}' before its rule line", + c[0] + ) + })?; + match c[0].as_str() { + "site" => r.sites.push(Site { + row: row(fields, &c[2..]), + line, + }), + "haplotype" => r.haps.push(Hap { + name: c[2].clone(), + alleles: c[3].split(',').map(str::to_owned).collect(), + line, + }), + _ => r.combos.push(Combo { + key: c[2].clone(), + label: c[3].clone(), + reading: c[4].clone(), + line, + }), + } + } + } + if rules.is_empty() { + return Err(format!("{path}: no rule line")); + } + Ok(rules.into_values().collect()) +} +fn positive(s: &str) -> bool { + s.as_bytes() + .first() + .is_some_and(|c| matches!(c, b'1'..=b'9')) + && s.bytes().all(|c| c.is_ascii_digit()) +} +fn pmids(s: &str) -> bool { + s.split(',').all(positive) +} +fn text_errors(w: &str, text: &str, e: &mut Vec) { + if text.to_lowercase().contains("snpedia") { + e.push(format!( + "{w}: mentions SNPedia; the table takes no text from it" + )); + } + if text.contains('"') || !text.is_ascii() { + e.push(format!( + "{w}: a double quote or a non-ASCII character (the reports read the table as plain TSV)" + )); + } +} +fn site_errors(w: &str, s: &Site, e: &mut Vec) { + let r = &s.row; + if !(1..=22).any(|n| r["chrom"] == format!("chr{n}")) { + e.push(format!("{w}: chrom '{}' is not chr1 to chr22", r["chrom"])); + } + if !positive(&r["pos"]) { + e.push(format!("{w}: pos '{}' is not a positive integer", r["pos"])); + } + if !r["rsid"].strip_prefix("rs").is_some_and(positive) { + e.push(format!("{w}: rsid is not rs")); + } + for k in ["ref", "alt"] { + if !["A", "C", "G", "T"].contains(&r[k].as_str()) { + e.push(format!("{w}: {k} '{}' is not one of A, C, G, T", r[k])); + } + } + if r["ref"] == r["alt"] { + e.push(format!("{w}: ref and alt are the same base")); + } +} +fn check_rows(rows: &[Site]) -> Vec { + let mut e = vec![]; + let mut ids = HashSet::new(); + let mut sites = HashSet::new(); + for s in rows { + let r = &s.row; + let w = format!("line {} ({})", s.line, r["rsid"]); + site_errors(&w, s, &mut e); + if !ids.insert(r["rsid"].as_str()) { + e.push(format!("{w}: rsid appears twice")); + } + if !sites.insert(s.key()) { + e.push(format!( + "{w}: position {}:{} appears twice", + r["chrom"], r["pos"] + )); + } + for k in ["gene", "trait", "hom_ref", "het", "hom_alt", "note"] { + if r[k].trim().is_empty() { + e.push(format!("{w}: {k} is empty")); + } + } + if !["yes", "no"].contains(&r["ancestry"].as_str()) { + e.push(format!( + "{w}: ancestry '{}' is not yes or no", + r["ancestry"] + )); + } + if !pmids(&r["pmids"]) { + e.push(format!( + "{w}: pmids '{}' is not a comma-separated list of PubMed ids", + r["pmids"] + )); + } + text_errors( + &w, + &COLUMNS + .iter() + .map(|k| r[*k].as_str()) + .collect::>() + .join(" "), + &mut e, + ); + } + for (id, a, b) in [("rs429358", "T", "C"), ("rs7412", "C", "T")] { + if let Some(s) = rows.iter().rev().find(|s| s.row["rsid"] == id) { + let r = &s.row; + if r["chrom"] != "chr19" || r["ref"] != a || r["alt"] != b { + e.push(format!( + "APOE row {id} must be chr19 {a}>{b}, is {} {}>{}", + r["chrom"], r["ref"], r["alt"] + )); + } + } else { + e.push(format!("APOE row {id} is missing")); + } + } + e +} +fn allowed(rule: &Rule, gt: &[Vec]) -> Vec { + let mut out = vec![]; + for (i, a) in rule.haps.iter().enumerate() { + for b in &rule.haps[i..] { + if gt.iter().enumerate().all(|(k, g)| { + g.len() == 2 + && ((g[0] == a.alleles[k] && g[1] == b.alleles[k]) + || (g[1] == a.alleles[k] && g[0] == b.alleles[k])) + }) { + out.push(format!("{}/{}", a.name, b.name)); + } + } + } + out +} +fn check_rules(rules: &[Rule], rows: Option<&[Site]>) -> Vec { + let mut e = vec![]; + for r in rules { + let n0 = e.len(); + let w = format!("rule {} (line {})", r.id, r.line); + if r.id.is_empty() || !r.id.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_') { + e.push(format!("{w}: the rule id is not letters, digits and _")); + } + for k in ["gene", "trait", "note"] { + if r.meta[k].trim().is_empty() { + e.push(format!("{w}: {k} is empty")); + } + } + for k in ["needs_phase", "ancestry"] { + if !["yes", "no"].contains(&r.meta[k].as_str()) { + e.push(format!("{w}: {k} '{}' is not yes or no", r.meta[k])); + } + } + if !pmids(&r.meta["pmids"]) { + e.push(format!( + "{w}: pmids '{}' is not a comma-separated list of PubMed ids", + r.meta["pmids"] + )); + } + if !(2..=8).contains(&r.sites.len()) { + e.push(format!("{w}: {} sites, expected 2 to 8", r.sites.len())); + } + let mut ids = HashSet::new(); + let mut positions = HashSet::new(); + for s in &r.sites { + let sw = format!("{w}, site line {} ({})", s.line, s.row["rsid"]); + site_errors(&sw, s, &mut e); + if !ids.insert(&s.row["rsid"]) || !positions.insert(s.key()) { + e.push(format!("{sw}: the site appears twice in the rule")); + } + if let Some(t) = + rows.and_then(|rows| rows.iter().rev().find(|t| t.row["rsid"] == s.row["rsid"])) + { + if ["chrom", "pos", "ref", "alt"] + .iter() + .any(|k| t.row[*k] != s.row[*k]) + { + e.push(format!( + "{sw}: the traits table has {}:{} {}>{}, the rule {}:{} {}>{}", + t.row["chrom"], + t.row["pos"], + t.row["ref"], + t.row["alt"], + s.row["chrom"], + s.row["pos"], + s.row["ref"], + s.row["alt"] + )); + } + } + } + if r.sites + .iter() + .map(|s| &s.row["chrom"]) + .collect::>() + .len() + > 1 + { + e.push(format!("{w}: the sites are on more than one chromosome")); + } + let mut names = Vec::new(); + let mut tuples = HashSet::new(); + for h in &r.haps { + let hw = format!("{w}, haplotype line {} ({})", h.line, h.name); + if h.name.is_empty() + || !h + .name + .bytes() + .all(|b| b.is_ascii_alphanumeric() || b"*.".contains(&b)) + { + e.push(format!("{hw}: the name is not letters, digits, * and .")); + } + if names.contains(&h.name.as_str()) { + e.push(format!("{hw}: the name appears twice")); + } + names.push(h.name.as_str()); + if h.alleles.len() != r.sites.len() { + e.push(format!( + "{hw}: {} alleles for {} sites", + h.alleles.len(), + r.sites.len() + )); + continue; + } + for (a, s) in h.alleles.iter().zip(&r.sites) { + if a != &s.row["ref"] && a != &s.row["alt"] { + e.push(format!( + "{hw}: allele {a} at {} is neither its ref {} nor its alt {}", + s.row["rsid"], s.row["ref"], s.row["alt"] + )); + } + } + if !tuples.insert(&h.alleles) { + e.push(format!("{hw}: the same alleles as another haplotype")); + } + } + if names.len() < 2 { + e.push(format!("{w}: fewer than two haplotypes")); + } + let mut keys = BTreeSet::new(); + for c in &r.combos { + let cw = format!("{w}, combination line {} ({})", c.line, c.key); + for (k, v) in [("label", &c.label), ("reading", &c.reading)] { + if v.trim().is_empty() { + e.push(format!("{cw}: {k} is empty")); + } + } + text_errors(&cw, &format!("{} {} {}", c.key, c.label, c.reading), &mut e); + if !keys.insert(c.key.clone()) { + e.push(format!("{cw}: the diplotypes appear twice")); + } + for d in c.key.split('|') { + let pair: Vec<_> = d.split('/').collect(); + if pair.len() != 2 || pair.iter().any(|p| !names.contains(p)) { + e.push(format!( + "{cw}: '{d}' is not two of the rule's haplotypes joined by /" + )); + } else if names.iter().position(|n| n == &pair[0]) + > names.iter().position(|n| n == &pair[1]) + { + e.push(format!( + "{cw}: '{d}' is not in the order of the haplotype lines (write {}/{})", + pair[1], pair[0] + )); + } + } + } + text_errors( + &w, + &format!("{} {} {}", r.meta["gene"], r.meta["trait"], r.meta["note"]), + &mut e, + ); + if e.len() != n0 { + continue; + } + let mut want = BTreeSet::new(); + if r.meta["needs_phase"] == "yes" { + for (i, a) in names.iter().enumerate() { + for b in &names[i..] { + want.insert(format!("{a}/{b}")); + } + } + } else { + for mut n in 0..3usize.pow(r.sites.len() as u32) { + let mut gt = vec![]; + for s in &r.sites { + let v = n % 3; + n /= 3; + gt.push(vec![ + s.row[if v == 2 { "alt" } else { "ref" }].clone(), + s.row[if v == 0 { "ref" } else { "alt" }].clone(), + ]); + } + let key = allowed(r, >).join("|"); + if !key.is_empty() { + want.insert(key); + } + } + } + for k in want.difference(&keys) { + e.push(format!("{w}: no combination line for {k}")); + } + for k in keys.difference(&want) { + e.push(format!( + "{w}: the combination line {k} can never be read {}", + if k.contains('|') { + "(a rule that needs phase reads one diplotype)" + } else { + "from these haplotypes" + } + )); + } + } + if let Some(rows) = rows { + if let Some(r) = rules.iter().find(|r| r.id == "APOE") { + for s in &r.sites { + if !rows.iter().any(|t| t.row["rsid"] == s.row["rsid"]) { + e.push(format!( + "rule APOE: site {} has no traits table row", + s.row["rsid"] + )); + } + } + } else { + e.push("rule APOE is missing: its line goes into the traits table, where the reports read it".into()); + } + } + e +} + +fn read_genotypes(path: &str, sites: &[&Site]) -> Result { + let mut wanted: HashMap> = HashMap::new(); + for s in sites { + wanted.entry(s.key()).or_default().push(s); + } + let mut result = HashMap::new(); + let mut lines = Lines::new(og_io::open(path).map_err(|e| e.to_string())?, Utf8::Strict); + let mut line = String::new(); + let mut n = 0; + while lines.read_line(&mut line).map_err(|e| e.to_string())? { + n += 1; + let line = line.trim_end_matches('\n'); + if line.is_empty() { + continue; + } + let c: Vec<_> = line.split('\t').collect(); + if c.len() < 5 { + return Err(format!( + "{path}:{n}: {} columns, expected CHROM POS REF ALT GENOTYPE [GQ]", + c.len() + )); + } + let key = (c[0].to_owned(), c[1].to_owned()); + let Some(sites) = wanted.get(&key) else { + continue; + }; + let site = sites[0]; + for s in sites { + if c[2].chars().next() != s.row["ref"].chars().next() { + return Err(format!( + "{} at {}:{}: the gVCF has REF {}, the table {} (the GRCh38 base). Is the gVCF on GRCh38?", + s.row["rsid"], c[0], c[1], c[2], s.row["ref"] + )); + } + } + let mut al: Vec<_> = c[4].split(['/', '|']).map(str::to_owned).collect(); + let snv = al.iter().all(|a| a.chars().count() == 1); + if result.get(&key).is_some_and(|old: &SiteGt| old.snv || !snv) { + continue; + } + al.sort_by_key(|a| { + ( + if a == &site.row["ref"] { + 0 + } else if a == &site.row["alt"] { + 1 + } else { + 2 + }, + a.clone(), + ) + }); + let status = if al.len() == 2 + && al + .iter() + .all(|a| a == &site.row["ref"] || a == &site.row["alt"]) + { + Status::Called + } else { + Status::OtherAllele + }; + let gq = c + .get(5) + .filter(|g| !g.is_empty()) + .unwrap_or(&".") + .to_string(); + result.insert( + key, + SiteGt { + alleles: al, + status, + gq, + snv, + }, + ); + } + Ok(result) +} +fn read_phased(path: &str, wanted: &HashSet) -> Result { + let mut lines = Lines::new(og_io::open(path).map_err(|e| e.to_string())?, Utf8::Strict); + let mut text = String::new(); + let mut out = HashMap::new(); + while lines.read_line(&mut text).map_err(|e| e.to_string())? { + if text.starts_with('#') { + continue; + } + let c: Vec<_> = text.trim_end_matches('\n').split('\t').collect(); + if c.len() < 10 { + continue; + } + let key = (c[0].to_owned(), c[1].to_owned()); + if !wanted.contains(&key) { + continue; + } + let fmt: HashMap<_, _> = c[8].split(':').zip(c[9].split(':')).collect(); + let (Some(gt), Some(ps)) = (fmt.get("GT"), fmt.get("PS")) else { + continue; + }; + if ps.is_empty() || *ps == "." || !gt.contains('|') { + continue; + } + let bases: Vec<_> = std::iter::once(c[3]).chain(c[4].split(',')).collect(); + let idx: Option> = gt + .split('|') + .map(|i| { + pycompat::num::parse_int(i) + .ok() + .and_then(|i| i.to_string().parse::().ok()) + .and_then(|i| { + let n = if i < 0 { bases.len() as isize + i } else { i }; + usize::try_from(n).ok().and_then(|n| bases.get(n).copied()) + }) + }) + .collect(); + if let Some(al) = idx { + if al.len() == 2 { + out.insert(key, (al[0].into(), al[1].into(), ps.to_string())); + } + } + } + Ok(out) +} +fn rule_line(r: &Rule, gt: &Genotypes, phased: Option<&Phased>) -> Row { + let g: Vec<_> = r + .sites + .iter() + .map(|s| gt.get(&s.key()).cloned().unwrap_or_default()) + .collect(); + let rs: Vec<_> = r.sites.iter().map(|s| s.row["rsid"].as_str()).collect(); + let mut out = r.meta.clone(); + for (k, v) in [ + ("rule", r.id.clone()), + ("rsids", rs.join(",")), + ("chrom", r.sites[0].row["chrom"].clone()), + ( + "pos", + r.sites + .iter() + .map(|s| s.row["pos"].as_str()) + .collect::>() + .join(","), + ), + ( + "genotypes", + g.iter() + .map(|g| { + if g.alleles.is_empty() { + ".".into() + } else { + g.alleles.join("/") + } + }) + .collect::>() + .join(","), + ), + ( + "gq", + g.iter() + .map(|g| g.gq.as_str()) + .collect::>() + .join(","), + ), + ] { + out.insert(k.into(), v); + } + let finish = |mut out: Row, phase: &str, result: &str, status: Status, reading: String| { + for (k, v) in [ + ("phase", phase), + ("result", result), + ("status", status.text()), + ("reading", &reading), + ] { + out.insert(k.into(), v.into()); + } + out + }; + if g.iter().any(|g| g.status != Status::Called) { + let need = if rs.len() == 2 { + format!("both {} and {}", rs[0], rs[1]) + } else { + format!( + "every one of {} and {}", + rs[..rs.len() - 1].join(", "), + rs[rs.len() - 1] + ) + }; + let status = if g.iter().any(|g| g.status == Status::NotCovered) { + Status::NotCovered + } else { + Status::OtherAllele + }; + return finish( + out, + ".", + ".", + status, + format!("Not read: needs a genotype at {need}."), + ); + } + let mut phase = if r.meta["needs_phase"] == "no" { + "not_used" + } else { + "not_needed" + }; + let key = if r.meta["needs_phase"] == "no" { + allowed(r, &g.iter().map(|g| g.alleles.clone()).collect::>()).join("|") + } else { + let het: Vec<_> = g + .iter() + .enumerate() + .filter(|(_, g)| g.alleles[0] != g.alleles[1]) + .map(|(i, _)| i) + .collect(); + let mut h1: Vec<_> = g.iter().map(|g| g.alleles[0].clone()).collect(); + let mut h2: Vec<_> = g.iter().map(|g| g.alleles[1].clone()).collect(); + if het.len() >= 2 { + let calls: Option> = het + .iter() + .map(|i| phased.and_then(|p| p.get(&r.sites[*i].key()))) + .collect(); + if let Some(calls) = calls.filter(|c| c.iter().all(|v| v.2 == c[0].2)) { + for (i, c) in het.iter().zip(calls) { + h1[*i] = c.0.clone(); + h2[*i] = c.1.clone(); + } + phase = "read_backed"; + } else { + let why = if phased.is_some() { + "no read or read pair covered them together" + } else { + "no phasing ran (no BAM, or phasing failed)" + }; + return finish( + out, + "unphased", + ".", + Status::Unphased, + format!( + "Heterozygous at {} of the {} sites, and {why}, so which alleles share a copy is not known: not read.", + het.len(), + r.sites.len() + ), + ); + } + } + let n1 = r.haps.iter().position(|h| h.alleles == h1); + let n2 = r.haps.iter().position(|h| h.alleles == h2); + if let (Some(a), Some(b)) = (n1, n2) { + format!("{}/{}", r.haps[a.min(b)].name, r.haps[a.max(b)].name) + } else { + let unknown = [(&h1, n1), (&h2, n2)] + .into_iter() + .filter(|(_, n)| n.is_none()) + .map(|(h, _)| h.join(",")) + .collect::>() + .join(" and "); + return finish( + out, + phase, + ".", + Status::OtherHaplotype, + format!( + "A haplotype this rule does not describe ({unknown} at {}): not read.", + rs.join(", ") + ), + ); + } + }; + match r + .combos + .iter() + .rev() + .find(|c| !key.is_empty() && c.key == key) + { + Some(c) => finish(out, phase, &c.label, Status::Called, c.reading.clone()), + None => finish( + out, + phase, + ".", + Status::OtherHaplotype, + "A combination of haplotypes this rule does not describe: not read.".into(), + ), + } +} +fn write_tsv(path: &str, columns: &[&str], rows: &[Row]) -> Result<()> { + let mut file = AtomicFile::create(path).map_err(|e| e.to_string())?; + { + let mut writer = Writer::new(&mut file, '\t'); + writer + .write_row(columns.iter().copied()) + .map_err(|e| e.to_string())?; + for row in rows { + writer + .write_row(columns.iter().map(|k| row[*k].as_str())) + .map_err(|e| e.to_string())?; + } + } + file.commit().map_err(|e| e.to_string()) +} +fn phase_vcf(path: &str, sample: &str, rules: &[Rule], gt: &Genotypes) -> Result { + let mut sites = BTreeMap::new(); + for r in rules.iter().filter(|r| r.meta["needs_phase"] == "yes") { + let calls: Option> = r + .sites + .iter() + .map(|s| gt.get(&s.key()).filter(|g| g.status == Status::Called)) + .collect(); + if let Some(calls) = calls { + let het: Vec<_> = r + .sites + .iter() + .zip(calls) + .filter(|(_, g)| g.alleles[0] != g.alleles[1]) + .map(|(s, _)| s) + .collect(); + if het.len() >= 2 { + for s in het { + let chrom = s.row["chrom"] + .strip_prefix("chr") + .and_then(|s| s.parse::().ok()) + .ok_or("invalid chromosome")?; + let pos = s.row["pos"].parse::().map_err(|e| e.to_string())?; + sites.entry((chrom, pos)).or_insert(s); + } + } + } + } + let mut out = AtomicFile::create(path).map_err(|e| e.to_string())?; + let write = |out: &mut AtomicFile| -> std::io::Result<()> { + writeln!(out, "##fileformat=VCFv4.2")?; + for chrom in sites.keys().map(|(c, _)| c).collect::>() { + writeln!(out, "##contig=")?; + } + writeln!( + out, + "##FORMAT=" + )?; + writeln!( + out, + "#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\tFORMAT\t{sample}" + )?; + for s in sites.values() { + writeln!( + out, + "{}\t{}\t{}\t{}\t{}\t.\tPASS\t.\tGT\t0/1", + s.row["chrom"], s.row["pos"], s.row["rsid"], s.row["ref"], s.row["alt"] + )?; + } + Ok(()) + }; + write(&mut out).map_err(|e| e.to_string())?; + out.commit().map_err(|e| e.to_string())?; + Ok(sites.len()) +} +fn run(verb: &str, opts: &HashMap) -> Result { + let required = |k: &str| { + opts.get(k) + .map(String::as_str) + .ok_or_else(|| format!("missing --{k}")) + }; + if verb == "check" { + let table = required("table")?; + let rows = read_table(table)?; + let mut errs = check_rows(&rows); + if let Some(path) = opts.get("rules") { + errs.extend(check_rules(&read_rules(path)?, Some(&rows))); + } + for e in &errs { + eprintln!("ERROR: {e}"); + } + if errs.is_empty() { + println!( + "{table}{}: schema OK", + opts.get("rules") + .map(|r| format!(" and {r}")) + .unwrap_or_default() + ); + } + return Ok(u8::from(!errs.is_empty())); + } + let rules_path = required("rules")?; + let rules = read_rules(rules_path)?; + let rows = if verb == "table" { + read_table(required("table")?)? + } else { + vec![] + }; + let mut errs = if verb == "table" { + check_rows(&rows) + .into_iter() + .map(|e| format!("{}: {e}", opts["table"])) + .collect::>() + } else { + vec![] + }; + errs.extend( + check_rules(&rules, if verb == "table" { Some(&rows) } else { None }) + .into_iter() + .map(|e| format!("{rules_path}: {e}")), + ); + if !errs.is_empty() { + for e in errs { + eprintln!("ERROR: {e}"); + } + return Ok(1); + } + let sites: Vec<_> = rows + .iter() + .chain(rules.iter().flat_map(|r| &r.sites)) + .collect(); + let gt = read_genotypes(required("genotypes")?, &sites)?; + let sample = required("sample")?; + if verb == "phase-sites" { + let path = required("out")?; + let n = phase_vcf(path, sample, &rules, >)?; + println!("{sample}: {n} heterozygous rule sites to phase. Written: {path}"); + return Ok(0); + } + let wanted = sites.iter().map(|s| s.key()).collect(); + let phased = opts + .get("phased") + .map(|p| read_phased(p, &wanted)) + .transpose()?; + let lines: Vec<_> = rules + .iter() + .map(|r| rule_line(r, >, phased.as_ref())) + .collect(); + let last = rules.iter().find(|r| r.id == "APOE").and_then(|r| { + rows.iter() + .rposition(|s| r.sites.iter().any(|t| s.row["rsid"] == t.row["rsid"])) + }); + let mut out = vec![]; + let mut counts = BTreeMap::::new(); + for (i, s) in rows.iter().enumerate() { + let g = gt.get(&s.key()).cloned().unwrap_or_default(); + let mut r = s.row.clone(); + r.insert( + "genotype".into(), + if g.alleles.is_empty() { + ".".into() + } else { + g.alleles.join("/") + }, + ); + r.insert("status".into(), g.status.text().into()); + let reading = match g.status { + Status::NotCovered => { + "No genotype at this position in the gVCF (no reads, or a no-call): not read." + .into() + } + Status::Called => s.row[["hom_ref", "het", "hom_alt"] + [g.alleles.iter().filter(|a| *a == &s.row["alt"]).count()]] + .clone(), + _ => "An allele this table does not describe at this position: not read.".into(), + }; + r.insert("reading".into(), reading); + out.push(r); + if last == Some(i) { + let mut r = lines + .iter() + .find(|r| r["rule"] == "APOE") + .expect("validated APOE") + .clone(); + r.insert("rsid".into(), r["rsids"].replace(',', "+")); + r.insert("genotype".into(), r["result"].clone()); + out.push(r); + } + } + // Compute both tables before opening either output. Each rename is atomic. + write_tsv(required("out")?, OUT, &out)?; + write_tsv(required("rules-out")?, RULE_OUT, &lines)?; + for r in &out { + *counts.entry(r["status"].clone()).or_default() += 1; + } + println!( + "{sample}: {} table rows; {} (APOE line included). Written: {}", + rows.len(), + counts + .iter() + .map(|(k, v)| format!("{k} {v}")) + .collect::>() + .join(", "), + opts["out"] + ); + println!( + "{sample}: {} genotype rules; {}. Written: {}", + lines.len(), + lines + .iter() + .map(|r| format!( + "{} {}", + r["rule"], + r[if r["status"] == "called" { + "result" + } else { + "status" + }] + )) + .collect::>() + .join(", "), + opts["rules-out"] + ); + Ok(0) +} +/// Run the three traits verbs. Usage errors exit 2; data and I/O errors exit 1. +pub fn cli(args: &[String]) -> u8 { + let usage = "usage: og traits table|phase-sites|check [--table TABLE] --rules RULES [--genotypes G --sample S --out OUT --rules-out OUT]"; + if args.iter().any(|a| a == "--help" || a == "-h") { + println!("{usage}"); + return 0; + } + let Some(verb) = args + .first() + .map(String::as_str) + .filter(|v| ["table", "phase-sites", "check"].contains(v)) + else { + eprintln!("{usage}"); + return 2; + }; + let mut opts = HashMap::new(); + let mut rest = args[1..].iter(); + while let Some(arg) = rest.next() { + let (key, value) = if let Some((k, v)) = arg.split_once('=') { + (k, Some(v.to_string())) + } else { + (arg.as_str(), rest.next().cloned()) + }; + let Some(key) = key.strip_prefix("--").filter(|k| { + [ + "table", + "rules", + "genotypes", + "phased", + "sample", + "out", + "rules-out", + ] + .contains(k) + }) else { + eprintln!("{usage}\nog traits: error: unknown argument {arg}"); + return 2; + }; + let Some(value) = value.filter(|v| !v.starts_with("--")) else { + eprintln!("{usage}\nog traits: error: --{key} needs a value"); + return 2; + }; + opts.insert(key.into(), value); + } + let needs: &[&str] = match verb { + "check" => &["table"], + "phase-sites" => &["rules", "genotypes", "sample", "out"], + _ => &["table", "rules", "genotypes", "sample", "out", "rules-out"], + }; + if let Some(k) = needs.iter().find(|k| !opts.contains_key(**k)) { + eprintln!("{usage}\nog traits: error: missing --{k}"); + return 2; + } + match run(verb, &opts) { + Ok(code) => code, + Err(e) => { + eprintln!("ERROR: {e}"); + 1 + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + use std::sync::atomic::{AtomicU64, Ordering}; + static SEQ: AtomicU64 = AtomicU64::new(0); + struct Scratch(PathBuf); + impl Scratch { + fn new() -> Self { + let p = std::env::temp_dir().join(format!( + "og-traits-{}-{}", + std::process::id(), + SEQ.fetch_add(1, Ordering::Relaxed) + )); + std::fs::create_dir_all(&p).unwrap(); + Self(p) + } + fn put(&self, name: &str, text: &str) -> String { + let p = self.0.join(name); + std::fs::write(&p, text).unwrap(); + p.to_string_lossy().into_owned() + } + } + impl Drop for Scratch { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } + } + fn assets() -> (Vec, Vec) { + let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../../assets"); + ( + read_table(root.join("traits.tsv").to_str().unwrap()).unwrap(), + read_rules(root.join("genotype_rules.tsv").to_str().unwrap()).unwrap(), + ) + } + fn genotypes(r: &Rule, choices: &[u8]) -> Genotypes { + r.sites + .iter() + .zip(choices) + .map(|(s, v)| { + let al = vec![ + s.row[if *v == 2 { "alt" } else { "ref" }].clone(), + s.row[if *v == 0 { "ref" } else { "alt" }].clone(), + ]; + ( + s.key(), + SiteGt { + alleles: al, + status: Status::Called, + gq: "40".into(), + snv: true, + }, + ) + }) + .collect() + } + #[test] + fn committed_catalogs_and_every_rule_genotype() { + let (rows, rules) = assets(); + assert!(check_rows(&rows).is_empty()); + assert_eq!(check_rules(&rules, Some(&rows)), Vec::::new()); + for r in &rules { + for mut n in 0..3usize.pow(r.sites.len() as u32) { + let mut choices = vec![]; + for _ in &r.sites { + choices.push((n % 3) as u8); + n /= 3; + } + let gt = genotypes(r, &choices); + let line = rule_line(r, >, None); + assert!(RULE_OUT.iter().all(|k| line.contains_key(*k))); + } + } + } + #[test] + fn apoe_grid_keeps_haplotype_order_and_missing_calls() { + let (_, rules) = assets(); + let r = rules.iter().find(|r| r.id == "APOE").unwrap(); + let expected = [ + ["e3/e3", "e2/e3", "e2/e2"], + ["e3/e4", "e2/e4", "e1/e2"], + ["e4/e4", "e1/e4", "e1/e1"], + ]; + for (a, labels) in expected.iter().enumerate() { + for (b, label) in labels.iter().enumerate() { + let line = rule_line(r, &genotypes(r, &[a as u8, b as u8]), None); + assert_eq!(line["status"], "called"); + assert_eq!(line["result"], *label); + } + } + let mut gt = genotypes(r, &[1, 0]); + gt.remove(&r.sites[1].key()); + let line = rule_line(r, >, None); + assert_eq!(line["status"], "not_covered"); + assert_eq!(line["phase"], "."); + } + #[test] + fn schema_faults_are_refused_before_enumeration() { + let (rows, rules) = assets(); + for (k, v, needle) in [ + ("ref", "N", "not one of"), + ("alt", rows[0].row["ref"].as_str(), "same base"), + ("pmids", "PMC123", "PubMed"), + ("chrom", "chrX", "chr1 to chr22"), + ("het", " ", "empty"), + ("note", "From SNPedia.", "SNPedia"), + ] { + let mut bad = rows.clone(); + bad[0].row.insert(k.into(), v.into()); + assert!(check_rows(&bad).iter().any(|e| e.contains(needle)), "{k}"); + } + let mut bad = rows.clone(); + bad.remove(bad.iter().position(|s| s.row["rsid"] == "rs7412").unwrap()); + assert!(check_rows(&bad).iter().any(|e| e.contains("missing"))); + let mut bad = rules.clone(); + bad[0].combos.pop(); + assert!( + check_rules(&bad, Some(&rows)) + .iter() + .any(|e| e.contains("no combination")) + ); + let mut bad = rules.clone(); + bad[0].haps[0].alleles[0] = "N".into(); + assert!( + check_rules(&bad, None) + .iter() + .any(|e| e.contains("neither")) + ); + let mut bad = rules.clone(); + bad[0].sites.clear(); + assert!(!check_rules(&bad, None).is_empty()); + let mut bad = rules.clone(); + bad[0].sites[0].row.insert("pos".into(), "9".into()); + assert!( + check_rules(&bad, Some(&rows)) + .iter() + .any(|e| e.contains("traits table has")) + ); + let mut bad = rules.clone(); + bad[0].combos[0].key = "absent/absent".into(); + assert!( + check_rules(&bad, None) + .iter() + .any(|e| e.contains("not two")) + ); + let mut bad = rules.clone(); + bad[0].combos[1].key = "e3/e2".into(); + assert!(check_rules(&bad, None).iter().any(|e| e.contains("order"))); + assert!( + check_rules(&rules[1..], Some(&rows)) + .iter() + .any(|e| e.contains("rule APOE is missing")) + ); + } + #[test] + fn stream_prefers_first_snv_but_checks_every_ref_and_bounds_memory() { + let (rows, _) = assets(); + let s = &rows[0]; + let scratch = Scratch::new(); + let prefix = format!("{}\t{}\t", s.row["chrom"], s.row["pos"]); + let a = &s.row["ref"]; + let b = &s.row["alt"]; + for (gt, status) in [ + (format!("{a}|{b}"), Status::Called), + (format!("./{b}"), Status::OtherAllele), + (a.clone(), Status::OtherAllele), + ("é/é".into(), Status::OtherAllele), + ] { + let path = scratch.put("g", &format!("{prefix}{a}\t{b}\t{gt}\t99\n")); + let result = read_genotypes(&path, &[s]).unwrap(); + assert_eq!(result[&s.key()].status, status); + } + let text = format!( + "{prefix}{a}T\t{a}\t{a}T/{a}\n{prefix}{a}\t{b}\t{b}/{a}\t42\n{prefix}{a}\t{b}\t{b}/{b}\t1\n" + ); + let path = scratch.put("g", &text); + let result = read_genotypes(&path, &[s]).unwrap(); + assert_eq!(result[&s.key()].alleles, vec![a.clone(), b.clone()]); + assert_eq!(result[&s.key()].gq, "42"); + let path = scratch.put("bad", &(text + &format!("{prefix}N\t{b}\tN/N\n"))); + assert!(read_genotypes(&path, &[s]).unwrap_err().contains("GRCh38")); + let path = scratch.put("many", &"chr99\t1\tN\t.\tN/N\n".repeat(10000)); + assert!(read_genotypes(&path, &[s]).unwrap().is_empty()); + } + #[test] + fn phase_requires_one_block_and_only_multiple_hets_are_exported() { + let (_, rules) = assets(); + let r = rules.iter().find(|r| r.id == "TAS2R38").unwrap(); + let gt = genotypes(r, &[1, 1, 1]); + assert_eq!(rule_line(r, >, None)["phase"], "unphased"); + assert!( + rule_line(r, >, Some(&HashMap::new()))["reading"].contains("no read or read pair") + ); + let scratch = Scratch::new(); + let mut vcf = String::new(); + let hap1 = &r.haps[0]; + let hap2 = &r.haps[1]; + for (i, s) in r.sites.iter().enumerate() { + let a = usize::from(hap1.alleles[i] == s.row["alt"]); + let b = usize::from(hap2.alleles[i] == s.row["alt"]); + vcf += &format!( + "{}\t{}\t.\t{}\t{}\t.\tPASS\t.\tGT:PS\t{a}|{b}:7\n", + s.row["chrom"], s.row["pos"], s.row["ref"], s.row["alt"] + ); + } + let p = scratch.put("phased", &vcf); + let mut phased = read_phased(&p, &r.sites.iter().map(Site::key).collect()).unwrap(); + assert_eq!(rule_line(r, >, Some(&phased))["phase"], "read_backed"); + phased.get_mut(&r.sites[0].key()).unwrap().2 = "8".into(); + assert_eq!(rule_line(r, >, Some(&phased))["status"], "unphased"); + let path = scratch.put("phase", ""); + assert_eq!(phase_vcf(&path, "S", &rules, >).unwrap(), 3); + let text = std::fs::read_to_string(&path).unwrap(); + assert!(text.contains("141972804\t")); + assert!(!text.contains("rs429358")); + assert_eq!( + phase_vcf(&path, "S", &rules, &genotypes(r, &[1, 0, 0])).unwrap(), + 0 + ); + let gt = genotypes(r, &[0, 0, 0]); + assert_eq!(rule_line(r, >, None)["phase"], "not_needed"); + } + #[test] + fn csv_header_unicode_rules_and_atomic_write_errors() { + let scratch = Scratch::new(); + let mut text = COLUMNS + .iter() + .map(|k| format!("\"{k}\"")) + .collect::>() + .join("\t"); + text.push_str("\r\n"); + let path = scratch.put("quoted", &text); + assert!(read_table(&path).unwrap().is_empty()); + let path = scratch.put("r", "rule\té\tG\tT\tno\tyes\tN\t1\n"); + assert!(!check_rules(&read_rules(&path).unwrap(), None).is_empty()); + assert!(write_tsv(scratch.0.join("absent/x").to_str().unwrap(), OUT, &[]).is_err()); + assert_eq!(cli(&["table".into()]), 2); + assert_eq!( + cli(&[ + "check".into(), + "--table".into(), + scratch.0.join("absent").to_string_lossy().into_owned() + ]), + 1 + ); + } +} diff --git a/rust/og/src/main.rs b/rust/og/src/main.rs index 29c44efa7..7df626a0a 100644 --- a/rust/og/src/main.rs +++ b/rust/og/src/main.rs @@ -53,6 +53,9 @@ const USAGE: &str = "usage: og [args] og summary collect --only a [args] og chip prepare|report|check-table [args] og cn bed|call|intervals|check-bam|marker-regions|marker-read [args] + og traits table [args] + og traits phase-sites [args] + og traits check [args] og bloodtype report|check-markers|systems [args] og family plan|prepare|check|report|segments [args] og family ibd [args] @@ -216,6 +219,7 @@ fn main() -> ExitCode { )), None => usage_error("the following arguments are required: chip tool"), }, + "traits" => ExitCode::from(og_steps::traits::cli(rest)), "bloodtype" => match rest.first() { Some(verb) => ExitCode::from(og_bloodtype::cli( verb, diff --git a/tests/parity/cases.tsv b/tests/parity/cases.tsv index 0a9afd6bf..2a2fe1e34 100644 --- a/tests/parity/cases.tsv +++ b/tests/parity/cases.tsv @@ -950,3 +950,30 @@ family-ibd mapbp gen:ibd-oracle.sh,gen:ibd-cases.sh --pedigree mapbp/pedigree.ts family-ibd nomap gen:ibd-oracle.sh,gen:ibd-cases.sh --pedigree nomap/pedigree.tsv --chroms nomap/chroms.txt --maps nomap/maps --phased nomap/phased --ibd nomap/ibd --out-dir out - content PYTHON_IMAGE family-ibd short gen:ibd-oracle.sh,gen:ibd-cases.sh --pedigree short/pedigree.tsv --chroms short/chroms.txt --maps short/maps --phased short/phased --ibd short/ibd --out-dir out - content PYTHON_IMAGE family-ibd twice gen:ibd-oracle.sh,gen:ibd-cases.sh --pedigree twice/pedigree.tsv --chroms twice/chroms.txt --maps twice/maps --phased twice/phased --ibd twice/ibd --out-dir out - content PYTHON_IMAGE +traits empty gen:traits.sh table --table traits.tsv --rules rules.tsv --genotypes empty.tsv --sample S --out traits-out.tsv --rules-out rules-out.tsv traits-out.tsv,rules-out.tsv tsv PYTHON_IMAGE +traits reference gen:traits.sh table --table traits.tsv --rules rules.tsv --genotypes reference.tsv --sample S --out traits-out.tsv --rules-out rules-out.tsv traits-out.tsv,rules-out.tsv tsv PYTHON_IMAGE +traits apoe-00 gen:traits.sh table --table traits.tsv --rules rules.tsv --genotypes apoe-00.tsv --sample S --out traits-out.tsv --rules-out rules-out.tsv traits-out.tsv,rules-out.tsv tsv PYTHON_IMAGE +traits apoe-01 gen:traits.sh table --table traits.tsv --rules rules.tsv --genotypes apoe-01.tsv --sample S --out traits-out.tsv --rules-out rules-out.tsv traits-out.tsv,rules-out.tsv tsv PYTHON_IMAGE +traits apoe-02 gen:traits.sh table --table traits.tsv --rules rules.tsv --genotypes apoe-02.tsv --sample S --out traits-out.tsv --rules-out rules-out.tsv traits-out.tsv,rules-out.tsv tsv PYTHON_IMAGE +traits apoe-10 gen:traits.sh table --table traits.tsv --rules rules.tsv --genotypes apoe-10.tsv --sample S --out traits-out.tsv --rules-out rules-out.tsv traits-out.tsv,rules-out.tsv tsv PYTHON_IMAGE +traits apoe-11 gen:traits.sh table --table traits.tsv --rules rules.tsv --genotypes apoe-11.tsv --sample S --out traits-out.tsv --rules-out rules-out.tsv traits-out.tsv,rules-out.tsv tsv PYTHON_IMAGE +traits apoe-12 gen:traits.sh table --table traits.tsv --rules rules.tsv --genotypes apoe-12.tsv --sample S --out traits-out.tsv --rules-out rules-out.tsv traits-out.tsv,rules-out.tsv tsv PYTHON_IMAGE +traits apoe-20 gen:traits.sh table --table traits.tsv --rules rules.tsv --genotypes apoe-20.tsv --sample S --out traits-out.tsv --rules-out rules-out.tsv traits-out.tsv,rules-out.tsv tsv PYTHON_IMAGE +traits apoe-21 gen:traits.sh table --table traits.tsv --rules rules.tsv --genotypes apoe-21.tsv --sample S --out traits-out.tsv --rules-out rules-out.tsv traits-out.tsv,rules-out.tsv tsv PYTHON_IMAGE +traits apoe-22 gen:traits.sh table --table traits.tsv --rules rules.tsv --genotypes apoe-22.tsv --sample S --out traits-out.tsv --rules-out rules-out.tsv traits-out.tsv,rules-out.tsv tsv PYTHON_IMAGE +traits other gen:traits.sh table --table traits.tsv --rules rules.tsv --genotypes other.tsv --sample S --out traits-out.tsv --rules-out rules-out.tsv traits-out.tsv,rules-out.tsv tsv PYTHON_IMAGE +traits missing gen:traits.sh table --table traits.tsv --rules rules.tsv --genotypes missing.tsv --sample S --out traits-out.tsv --rules-out rules-out.tsv traits-out.tsv,rules-out.tsv tsv PYTHON_IMAGE +traits duplicate gen:traits.sh table --table traits.tsv --rules rules.tsv --genotypes duplicate.tsv --sample S --out traits-out.tsv --rules-out rules-out.tsv traits-out.tsv,rules-out.tsv tsv PYTHON_IMAGE +traits half gen:traits.sh table --table traits.tsv --rules rules.tsv --genotypes half.tsv --sample S --out traits-out.tsv --rules-out rules-out.tsv traits-out.tsv,rules-out.tsv tsv PYTHON_IMAGE +traits wrong-ref gen:traits.sh table --table traits.tsv --rules rules.tsv --genotypes wrong-ref.tsv --sample S --out traits-out.tsv --rules-out rules-out.tsv - tsv PYTHON_IMAGE +traits triple gen:traits.sh table --table traits.tsv --rules rules.tsv --genotypes triple.tsv --sample S --out traits-out.tsv --rules-out rules-out.tsv traits-out.tsv,rules-out.tsv tsv PYTHON_IMAGE +traits single gen:traits.sh table --table traits.tsv --rules rules.tsv --genotypes single.tsv --sample S --out traits-out.tsv --rules-out rules-out.tsv traits-out.tsv,rules-out.tsv tsv PYTHON_IMAGE +traits phase-empty gen:traits.sh table --table traits.tsv --rules rules.tsv --genotypes triple.tsv --phased empty.vcf --sample S --out traits-out.tsv --rules-out rules-out.tsv traits-out.tsv,rules-out.tsv tsv PYTHON_IMAGE +traits phase-phased gen:traits.sh table --table traits.tsv --rules rules.tsv --genotypes triple.tsv --phased phased.vcf --sample S --out traits-out.tsv --rules-out rules-out.tsv traits-out.tsv,rules-out.tsv tsv PYTHON_IMAGE +traits phase-split gen:traits.sh table --table traits.tsv --rules rules.tsv --genotypes triple.tsv --phased split.vcf --sample S --out traits-out.tsv --rules-out rules-out.tsv traits-out.tsv,rules-out.tsv tsv PYTHON_IMAGE +traits sites-empty gen:traits.sh phase-sites --rules rules.tsv --genotypes empty.tsv --sample S --out phase.vcf phase.vcf vcf PYTHON_IMAGE +traits sites-triple gen:traits.sh phase-sites --rules rules.tsv --genotypes triple.tsv --sample S --out phase.vcf phase.vcf vcf PYTHON_IMAGE +traits sites-single gen:traits.sh phase-sites --rules rules.tsv --genotypes single.tsv --sample S --out phase.vcf phase.vcf vcf PYTHON_IMAGE +traits check gen:traits.sh check --table traits.tsv --rules rules.tsv - bytes PYTHON_IMAGE +traits bad-table gen:traits.sh check --table bad-table.tsv --rules rules.tsv - bytes PYTHON_IMAGE +traits bad-rules gen:traits.sh check --table traits.tsv --rules bad-rules.tsv - bytes PYTHON_IMAGE diff --git a/tests/parity/gen/traits.sh b/tests/parity/gen/traits.sh new file mode 100644 index 000000000..10d0fa26f --- /dev/null +++ b/tests/parity/gen/traits.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# Public catalogs and synthetic genotype grids for all three traits verbs. +set -euo pipefail +root=$(cd "$(dirname "$0")/../../.." && pwd) +cp "$root/assets/traits.tsv" traits.tsv +cp "$root/assets/genotype_rules.tsv" rules.tsv +cat > oracle.sh <<'EOF' +prog=$1; shift +mode=$1; shift +case "$mode" in + table) exec python3 "$prog" "$@" ;; + check) exec python3 "$prog" "${@/--table/--check-table}" ;; + phase-sites) + args=() + while [ "$#" -gt 0 ]; do + if [ "$1" = --out ]; then args+=(--phase-sites "$2"); shift 2 + else args+=("$1"); shift; fi + done + exec python3 "$prog" "${args[@]}" ;; +esac +EOF +: > empty.tsv +awk -F'\t' '$1 !~ /^#/ && $1 != "chrom" {print $1 "\t" $2 "\t" $5 "\t" $6 "\t" $5 "/" $5 "\t40"}' traits.tsv > reference.tsv +for a in 0 1 2; do + for b in 0 1 2; do + case $a in 0) ga=T/T;; 1) ga=T/C;; 2) ga=C/C;; esac + case $b in 0) gb=C/C;; 1) gb=C/T;; 2) gb=T/T;; esac + printf 'chr19\t44908684\tT\tC\t%s\t40\nchr19\t44908822\tC\tT\t%s\t39\n' "$ga" "$gb" > "apoe-$a$b.tsv" + done +done +printf 'chr19\t44908684\tT\tC\tT/G\t30\nchr19\t44908822\tC\tT\tC/C\t40\n' > other.tsv +printf 'chr19\t44908684\tT\tC\tT/C\t30\n' > missing.tsv +printf 'chr19\t44908684\tTT\tT\tTT/T\t10\nchr19\t44908684\tT\tC\tC/T\t40\n' > duplicate.tsv +printf 'chr19\t44908684\tG\tC\tG/C\t30\n' > wrong-ref.tsv +printf 'chr19\t44908684\tT\tC\t./C\t30\n' > half.tsv +printf 'chr7\t141973545\tC\tG\tC/G\t30\nchr7\t141972905\tG\tC\tG/C\t31\nchr7\t141972804\tT\tC\tT/C\t32\n' > triple.tsv +head -1 triple.tsv > single.tsv +: > empty.vcf +printf 'chr7\t141973545\t.\tC\tG\t.\tPASS\t.\tGT:PS\t1|0:7\nchr7\t141972905\t.\tG\tC\t.\tPASS\t.\tGT:PS\t0|1:7\nchr7\t141972804\t.\tT\tC\t.\tPASS\t.\tGT:PS\t1|0:7\n' > phased.vcf +sed '1s/:7$/:8/' phased.vcf > split.vcf +awk -F'\t' -v OFS='\t' '$1 == "combination" && $2 == "APOE" && $3 == "e3/e3" {next} {print}' rules.tsv > bad-rules.tsv +awk -F'\t' -v OFS='\t' '$3 == "rs671" {$6=$5} {print}' traits.tsv > bad-table.tsv diff --git a/tests/parity/oracle.ref b/tests/parity/oracle.ref index 57c27da2f..526797497 100644 --- a/tests/parity/oracle.ref +++ b/tests/parity/oracle.ref @@ -63,3 +63,4 @@ y-yleaf-positions 0baddb45aa3871a57469cf20295bdd832f72f5c0 file:bin/yleaf_run.py lookup 0baddb45aa3871a57469cf20295bdd832f72f5c0 file:bin/genome_lookup.py python3 lookup lookup-query 0baddb45aa3871a57469cf20295bdd832f72f5c0 file:bin/genome_lookup.py bash ./oracle.sh lookup query family-ibd 0baddb45aa3871a57469cf20295bdd832f72f5c0 file:bin/ibd_report.py sh ./ibd_oracle.sh family ibd +traits 669a8196 file:bin/traits_report.py bash ./oracle.sh traits From 17842fb53220882b008ef11b5b4a4e8663a1ced1 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 15:09:21 +0100 Subject: [PATCH 22/45] fix(ci): preserve fractional trace diagnostics and count PCA rows Bead: pgp-bs9u.38 --- rust/crates/og-dev/src/nf_trace.rs | 15 +++++++++------ rust/crates/og-dev/src/smoke.rs | 1 + rust/crates/og-dev/tests/ci_readers.rs | 6 +++++- tests/parity/cases.tsv | 2 ++ .../golden/dev-smoke-check/eigen/stdout | 2 +- .../inputs/ci-readers/requests-frac.tsv | 19 +++++++++++++++++++ 6 files changed, 37 insertions(+), 8 deletions(-) create mode 100644 tests/parity/inputs/ci-readers/requests-frac.tsv diff --git a/rust/crates/og-dev/src/nf_trace.rs b/rust/crates/og-dev/src/nf_trace.rs index ac9441b72..2a0aef041 100644 --- a/rust/crates/og-dev/src/nf_trace.rs +++ b/rust/crates/og-dev/src/nf_trace.rs @@ -7,6 +7,9 @@ const GB: u64 = 1 << 30; const H: u64 = 3_600_000; type Record = BTreeMap; +fn g(value: u64, unit: u64) -> String { + pycompat::fmt::general(value as f64 / unit as f64, 6) +} fn records(path: &str, mut f: impl FnMut(Record) -> Result<(), String>) -> Result<(), String> { if path.contains(['*', '?', '[', ']']) { return Err("trace glob refused: pass exactly one explicit trace path".into()); @@ -214,8 +217,8 @@ fn requests(path: &str, opts: &BTreeMap<&str, &str>, out: &mut dyn Write) -> Res out, "{name}: attempt {attempt}, exit {}, cpus {cpus}, memory {} GB, time {} h", cell(r, "exit")?, - mem / GB, - time / H + g(mem, GB), + g(time, H) ) .map_err(|e| e.to_string())?; seen.entry(proc) @@ -260,7 +263,7 @@ fn requests(path: &str, opts: &BTreeMap<&str, &str>, out: &mut dyn Write) -> Res } for got in first.into_iter().flat_map(|a| a.values()).flatten() { if *got != expected { - writeln!(out,"FAIL {proc}: first attempt asks for cpus {}, memory {} GB, time {} h; want {}, {} GB, {} h",got.0,got.1/GB,got.2/H,expected.0,expected.1/GB,expected.2/H).map_err(|e| e.to_string())?; + writeln!(out,"FAIL {proc}: first attempt asks for cpus {}, memory {} GB, time {} h; want {}, {} GB, {} h",got.0,g(got.1,GB),g(got.2,H),expected.0,g(expected.1,GB),g(expected.2,H)).map_err(|e| e.to_string())?; bad += 1; } } @@ -269,8 +272,8 @@ fn requests(path: &str, opts: &BTreeMap<&str, &str>, out: &mut dyn Write) -> Res writeln!( out, "FAIL {proc}: the retry asks for {} GB, want {} GB", - got.1 / GB, - expected.1 / GB * 2 + g(got.1, GB), + pycompat::fmt::general(expected.1 as f64 * 2. / GB as f64, 6) ) .map_err(|e| e.to_string())?; bad += 1; @@ -283,7 +286,7 @@ fn requests(path: &str, opts: &BTreeMap<&str, &str>, out: &mut dyn Write) -> Res let mem: u64 = opt(opts, "--executor-mem-gb")? .parse() .map_err(|_| "invalid executor memory")?; - writeln!(out,"{} task attempts ask for {all_cpus} CPUs and {} GB added together; the executor is told it has {cpus} CPUs and {mem} GB",rows.len(),all_mem/GB).map_err(|e| e.to_string())?; + writeln!(out,"{} task attempts ask for {all_cpus} CPUs and {} GB added together; the executor is told it has {cpus} CPUs and {} GB",rows.len(),g(all_mem,GB),pycompat::fmt::general(mem as f64,6)).map_err(|e| e.to_string())?; if all_cpus > cpus || Some(all_mem) > mem.checked_mul(GB) { writeln!(out,"FAIL: executor.cpus or executor.memory of this step is below that sum; raise exec_cpus / exec_mem_gb").map_err(|e| e.to_string())?; bad += 1; diff --git a/rust/crates/og-dev/src/smoke.rs b/rust/crates/og-dev/src/smoke.rs index 379b1d7be..950d023cf 100644 --- a/rust/crates/og-dev/src/smoke.rs +++ b/rust/crates/og-dev/src/smoke.rs @@ -295,6 +295,7 @@ fn check(kind: &str, path: &str, out: &mut dyn Write) -> Result { if line.trim_start().starts_with('#') || r.is_empty() { continue; } + n += 1; let g = match r.last() { Some(&"PopA") => 0, Some(&"PopB") => 1, diff --git a/rust/crates/og-dev/tests/ci_readers.rs b/rust/crates/og-dev/tests/ci_readers.rs index 18a09ea89..a497e9307 100644 --- a/rust/crates/og-dev/tests/ci_readers.rs +++ b/rust/crates/og-dev/tests/ci_readers.rs @@ -224,7 +224,11 @@ fn smoke_tables_reject_removed_properties() { ), ] { let p = t.file("table", good); - assert_eq!(run(smoke::cli, &["check", kind, &p]).0, 0, "{kind}"); + let clean = run(smoke::cli, &["check", kind, &p]); + assert_eq!(clean.0, 0, "{kind}"); + if kind == "eigen" { + assert_eq!(clean.1, "eigen: 3 rows; OK\n"); + } let p = t.file("table", bad); assert_eq!(run(smoke::cli, &["check", kind, &p]).0, 1, "{kind}"); } diff --git a/tests/parity/cases.tsv b/tests/parity/cases.tsv index 04f0538e9..bbb1750da 100644 --- a/tests/parity/cases.tsv +++ b/tests/parity/cases.tsv @@ -980,3 +980,5 @@ dev-pharmcat-called pharmcat-docs-example ci-readers/oracle.sh,gen:ci-pharmcat.s dev-pharmcat-lines report-3.2.0 ci-readers/oracle.sh,gen:ci-pharmcat.sh report-3.2.0.json --called-lines - bytes PYTHON_IMAGE dev-pharmcat-lines report-3.4.0 ci-readers/oracle.sh,gen:ci-pharmcat.sh report-3.4.0.json --called-lines - bytes PYTHON_IMAGE dev-pharmcat-lines pharmcat-docs-example ci-readers/oracle.sh,gen:ci-pharmcat.sh pharmcat-docs-example.json --called-lines - bytes PYTHON_IMAGE +dev-nf-requests large-budget ci-readers/oracle.sh,ci-readers/requests.tsv,ci-readers/want.tsv requests.tsv --want want.tsv --executor-cpus 1000000 --executor-mem-gb 1000000 - bytes PYTHON_IMAGE +dev-nf-requests fractional-memory ci-readers/oracle.sh,ci-readers/requests-frac.tsv,ci-readers/want.tsv requests-frac.tsv --want want.tsv --executor-cpus 512 --executor-mem-gb 2048 - bytes PYTHON_IMAGE diff --git a/tests/parity/golden/dev-smoke-check/eigen/stdout b/tests/parity/golden/dev-smoke-check/eigen/stdout index 112b6bd29..f1e05c34a 100644 --- a/tests/parity/golden/dev-smoke-check/eigen/stdout +++ b/tests/parity/golden/dev-smoke-check/eigen/stdout @@ -1 +1 @@ -eigen: 0 rows; OK +eigen: 3 rows; OK diff --git a/tests/parity/inputs/ci-readers/requests-frac.tsv b/tests/parity/inputs/ci-readers/requests-frac.tsv new file mode 100644 index 000000000..0d7e904e9 --- /dev/null +++ b/tests/parity/inputs/ci-readers/requests-frac.tsv @@ -0,0 +1,19 @@ +task_id name attempt exit cpus memory time +1 X:DEEPVARIANT (s) 1 137 8 17716740096 172800000 +2 X:DEEPVARIANT (s) 2 0 8 68719476736 172800000 +3 X:FASTP (s) 1 137 8 8589934592 28800000 +4 X:FASTP (s) 2 0 8 17179869184 28800000 +5 X:ALIGN_MINIMAP2 (s) 1 137 8 34359738368 86400000 +6 X:ALIGN_MINIMAP2 (s) 2 0 8 68719476736 86400000 +7 X:MANTA (s) 1 137 8 17179869184 86400000 +8 X:MANTA (s) 2 0 8 34359738368 86400000 +9 X:CNVPYTOR (s) 1 137 4 8589934592 86400000 +10 X:CNVPYTOR (s) 2 0 4 17179869184 86400000 +11 X:VEP (s) 1 137 8 17179869184 86400000 +12 X:VEP (s) 2 0 8 34359738368 86400000 +13 X:PRS_PREPARE (s) 1 137 1 2147483648 7200000 +14 X:PRS_PREPARE (s) 2 0 1 4294967296 7200000 +15 X:PRS_SCORE_SITES (s) 1 137 2 4294967296 14400000 +16 X:PRS_SCORE_SITES (s) 2 0 2 8589934592 14400000 +17 X:PRS (s) 1 137 4 25769803776 28800000 +18 X:PRS (s) 2 0 4 51539607552 28800000 From c4e77149c378507c8246b74db17c37b200f65483 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 15:10:38 +0100 Subject: [PATCH 23/45] refactor(traits): switch step 42 and remove the replaced Python Bead: pgp-bs9u.25 --- .github/workflows/guard.yml | 2 +- CHANGELOG.md | 4 + bin/copy_number.py | 344 ------------- bin/traits_report.py | 605 +---------------------- docs/42-traits.md | 6 +- docs/rust.md | 5 + modules/local/traits/main.nf | 6 +- rust/README.md | 4 + rust/crates/og-steps/src/traits.rs | 36 +- scripts/42-traits.sh | 4 +- scripts/ci/e2e-case-paths.tsv | 71 +-- scripts/ci/e2e-case-times.tsv | 1 + tests/demo/make_demo_sample.py | 20 +- tests/e2e/genotype-rules-2-g2.sh | 14 +- tests/e2e/report-followups-1-sections.sh | 16 +- tests/e2e/rust-c2b-traits.sh | 28 ++ tests/parity/README.md | 5 + tests/parity/gen/traits.sh | 4 +- tests/parity/oracle.ref | 2 +- tests/parity/remnants.tsv | 1 + tests/test_collect_summary.py | 16 +- tests/test_copy_number.py | 25 +- tests/test_genotype_rules_indels.py | 5 +- tests/test_hba_copy_number.py | 2 +- tests/test_lpa_kiv2.py | 2 +- tests/test_render_report.py | 5 +- tests/test_traits_report.py | 481 +++--------------- tests/test_traits_table.py | 43 +- 28 files changed, 267 insertions(+), 1490 deletions(-) delete mode 100755 bin/copy_number.py create mode 100755 tests/e2e/rust-c2b-traits.sh diff --git a/.github/workflows/guard.yml b/.github/workflows/guard.yml index 304360fbd..7ee58a0dc 100644 --- a/.github/workflows/guard.yml +++ b/.github/workflows/guard.yml @@ -351,7 +351,7 @@ jobs: tests/test_results_index.py \ tests/test_pgx_outside_calls.py tests/test_hla_second_caller.py tests/test_paraphase_report.py \ tests/test_copy_number.py tests/test_hba_copy_number.py \ - tests/test_lpa_kiv2.py tests/test_traits_report.py tests/test_callability.py; do + tests/test_lpa_kiv2.py tests/test_traits_report.py tests/test_traits_table.py tests/test_genotype_rules_indels.py tests/test_callability.py; do rc=0 echo "::group::${t}" case "$t" in diff --git a/CHANGELOG.md b/CHANGELOG.md index cd74b71a6..8400d72da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ Each release's notes. From 1.0.0 on, a major or minor release has hand-written n ## Unreleased +- Step 42 reads traits and genotype rules with `og traits table`, exports WhatsHap sites with `og traits phase-sites`, and checks the catalogs with `og traits check`. The replaced Python evaluator is removed. Report columns and readings stay the same. Genotypes outside the catalog are streamed past, and write errors now report a controlled error instead of a traceback. +- The unused Python copy-number engine is removed. Its historical catalog tests materialise a pinned test oracle and keep testing the Rust commands. + + - The container helpers check (`scripts/ci/check-container-helpers.sh`, Guard job `helper-binaries`) now derives its table with the Rust `og` binary, `og dev helpers`. It replaces the Python inside the script, which is gone; the script keeps the docker probe, the orad download and its self-test. On this tree the table and the rule 1 output are the same, byte for byte. The static run takes 0.01 s and 6 MB where the Python took 0.2 s and 15 MB. What changes: - A helper called in a process's `shell:` block is checked too. The Python read only `script:` blocks, so such a call went unchecked. No module has one today. - A helper called in a process's `exec:` block always fails the check: Nextflow runs that block on the host, where no image applies. diff --git a/bin/copy_number.py b/bin/copy_number.py deleted file mode 100755 index 2e1b95485..000000000 --- a/bin/copy_number.py +++ /dev/null @@ -1,344 +0,0 @@ -#!/usr/bin/env python3 -"""copy_number.py: the catalog reader and the depth-ratio engine of -bin/cn_loci.tsv, as the catalog tests (tests/test_copy_number.py, -test_hba_copy_number.py, test_lpa_kiv2.py) import them. No step runs it. - -The commands are og cn's (rust/crates/og-cn, docs/cn-loci.md): `og cn bed` -prints a locus's segments, `og cn call` turns `samtools bedcov` over them -into the step's table, `og cn intervals` reads the interval rows and `og cn -check-bam` refuses a BAM aligned with ALT contigs, and `og bloodtype -report` reads step 41's RHD row with og cn's engine. This file keeps -load_catalog, assess, cut_copies, BrokenControl and CatalogError for those -tests until they move to og too. - -One method, one catalog row per locus: the depth over the locus's target -segments against the depth over single-copy control segments, as -`samtools bedcov` sums it. With T the target depth and C the control depth -(each combined as the row's target_depth and control_depth say), - estimate = scale x T / C - offset -then guarded by a minimum control depth, flank agreement and the share of the -target depth from reads of mapping quality 0, and called as the row's `call` -column says. docs/cn-loci.md describes the columns and how to add a locus. -Standard library only. -""" -import csv -import math -import os - -CATALOG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "cn_loci.tsv") -COLUMNS = ["locus", "kind", "chrom", "targets", "controls", "details", "target_depth", "control_depth", - "scale", "offset", "bedcov", "mapq0_min", "mapq0_max", "min_control_depth", "max_control_ratio", - "call", "interval", "readings", "input", "owner", "cross_check", "pmids"] -KINDS = ("gene", "interval", "organelle") -# GRCh38 names of the primary chromosomes (the ClinGen BED spells chrX 'chrx': -# scripts/ci/build-cn-intervals.py normalises it, and a row with any other -# spelling is refused here). -CHROMS = {f"chr{c}" for c in list(range(1, 23)) + ["X", "Y", "M"]} -INPUTS = ("bedcov", "cnvpytor-level") -Z95 = 1.96 -# The findings a psv row needs a reading for. -PSV_FINDINGS = ("none", "deletion", "conversion", "pseudogene_deletion") -BASES = "ACGT" - - -class CatalogError(ValueError): - pass - - -class BrokenControl(ValueError): - """A control segment with no read while the other controls are covered.""" - - -def _num(row, col, default=None): - v = row[col] - if v == ".": - return default - try: - x = float(v) - except ValueError: - x = math.nan - if not math.isfinite(x): - raise CatalogError(f"{row['locus']}: {col} is {v!r}, not a number or '.'") - return x - - -def _segments(row, col): - """[(name, start, end, label)] of a targets, controls or details cell.""" - if row[col] == ".": - return [] - out = [] - for part in row[col].split(";"): - bits = part.split(":", 2) - try: - name, (s, e) = bits[0], bits[1].split("-") - start, end = int(s), int(e) - except (IndexError, ValueError): - raise CatalogError(f"{row['locus']}: {col} segment {part!r} is not NAME:start-end[:label]") - if not name or end <= start or start < 0: - raise CatalogError(f"{row['locus']}: {col} segment {part!r} has no name or an empty span") - out.append((name, start, end, bits[2] if len(bits) > 2 else "")) - return out - - -def _readings(row): - out = {} - if row["readings"] == ".": - return out - for part in row["readings"].split("|"): - k, sep, text = part.partition("=") - if not sep or not k or not text: - raise CatalogError(f"{row['locus']}: reading {part!r} is not KEY=text") - out[k] = text - return out - - -def _call(row): - mode, sep, args = row["call"].partition(":") - try: - vals = [float(x) for x in args.split(",")] - except ValueError: - vals = [] - if not all(math.isfinite(v) for v in vals): - vals = [] - if not sep or mode not in ("round", "cut", "class", "psv") or not vals \ - or (mode in ("round", "psv") and len(vals) != 1) or (mode == "class" and len(vals) != 2) \ - or vals != sorted(vals): - raise CatalogError(f"{row['locus']}: call {row['call']!r} is not round:D, cut:A,B..., class:LOW,HIGH or psv:D") - return mode, vals - - -def _psv_sites(row, details): - """[(name, gene 0-based position, pseudogene 0-based position, gene - bases, pseudogene bases, text)] of a psv row: every details segment is one - base, labelled 'PSEUDO_POS GENE>PSEUDO text' (bases among ACGT; In2G's - gene base is A or C).""" - out = [] - for name, s, e, label in details: - bits = label.split(" ", 2) - try: - pos = int(bits[0]) - gene, _, pseudo = bits[1].partition(">") - except (IndexError, ValueError): - raise CatalogError(f"{row['locus']}: PSV {name} label {label!r} is not 'PSEUDO_POS GENE>PSEUDO text'") - if e - s != 1 or pos < 0 or not gene or not pseudo or set(gene + pseudo) - set(BASES) \ - or set(gene) & set(pseudo) or len(bits) < 3: - raise CatalogError(f"{row['locus']}: PSV {name} must be one base with distinct gene and pseudogene " - f"bases and a text (got {s}-{e}, {label!r})") - out.append((name, s, pos, set(gene), set(pseudo), bits[2])) - return out - - -def parse_row(row): - """The catalog row checked, with its parsed fields under '_' keys.""" - loc = row["locus"] - if row["kind"] not in KINDS: - raise CatalogError(f"{loc}: kind {row['kind']!r} (one of {', '.join(KINDS)})") - if row["input"] not in INPUTS: - raise CatalogError(f"{loc}: input {row['input']!r} (one of {', '.join(INPUTS)})") - if row["chrom"] not in CHROMS: - raise CatalogError(f"{loc}: chrom {row['chrom']!r} (GRCh38 names, chr1 to chrM)") - p = dict(row) - p["_targets"], p["_controls"], p["_details"] = (_segments(row, c) for c in ("targets", "controls", "details")) - p["_call"] = _call(row) - p["_readings"] = _readings(row) - p["_psv"] = _psv_sites(row, p["_details"]) if p["_call"][0] == "psv" else [] - if p["_call"][0] == "psv" and (not p["_psv"] or row["target_depth"] != "sum" - or not set(PSV_FINDINGS) <= set(p["_readings"])): - raise CatalogError(f"{loc}: call psv needs PSV sites in details, target_depth sum (gene and pseudogene) " - f"and a reading for each of {', '.join(PSV_FINDINGS)}") - if not p["_targets"]: - raise CatalogError(f"{loc}: no target segment") - if row["input"] == "bedcov": - if not p["_controls"]: - raise CatalogError(f"{loc}: a bedcov row needs control segments") - if row["target_depth"] not in ("sum", "pooled") or row["control_depth"] not in ("flanks", "pooled"): - raise CatalogError(f"{loc}: target_depth sum|pooled and control_depth flanks|pooled") - p["_scale"], p["_offset"] = _num(row, "scale"), _num(row, "offset", 0.0) - if not p["_scale"] or p["_scale"] <= 0: - raise CatalogError(f"{loc}: scale must be a positive number") - p["_min_control"] = _num(row, "min_control_depth", 0.0) - p["_max_ratio"] = _num(row, "max_control_ratio") - p["_mapq0_min"], p["_mapq0_max"] = _num(row, "mapq0_min"), _num(row, "mapq0_max") - p["_passes"] = row["bedcov"].split(",") - if (p["_mapq0_min"] is not None or p["_mapq0_max"] is not None) and len(p["_passes"]) != 2: - raise CatalogError(f"{loc}: a MAPQ 0 guard needs two bedcov passes (all reads, MAPQ >= 1)") - if p["_call"][0] == "class" and row["interval"] != "cv": - raise CatalogError(f"{loc}: call class needs interval cv") - if row["interval"] not in ("cv", "."): - raise CatalogError(f"{loc}: interval {row['interval']!r} (cv or '.')") - names = [n for n, *_ in p["_targets"] + p["_controls"] + p["_details"]] - if len(names) != len(set(names)): - raise CatalogError(f"{loc}: a segment name is used twice") - return p - - -def load_catalog(path=None): - """{locus: parsed row} of the catalog, in file order.""" - path = path or CATALOG - with open(path) as f: - lines = [ln for ln in f if ln.strip() and not ln.startswith("#")] - rows = list(csv.DictReader(lines, delimiter="\t", quoting=csv.QUOTE_NONE)) - if not rows or list(rows[0].keys()) != COLUMNS: - raise CatalogError(f"{path}: the header must be {' '.join(COLUMNS)}") - out, seen = {}, set() - for r in rows: - if None in r or None in r.values(): - raise CatalogError(f"{path}: row {r.get('locus')!r} has the wrong number of columns") - if r["locus"] in out: - raise CatalogError(f"{path}: locus {r['locus']} is listed twice") - p = parse_row(r) - for n, *_ in p["_targets"] + p["_controls"] + p["_details"]: - if n in seen: - raise CatalogError(f"{path}: segment name {n} is used by two loci") - seen.add(n) - out[r["locus"]] = p - return out - - -def regions(row): - """[(chrom, start, end, name)] of a row's segments, sorted by position.""" - segs = row["_controls"] + row["_targets"] + row["_details"] - return sorted(((row["chrom"], s, e, n) for n, s, e, _ in segs), key=lambda r: (r[1], r[2])) - - -def _combine(mode, segs, m): - if mode == "sum": - return sum(m[n] for n, *_ in segs) - if mode == "flanks": - return sum(m[n] for n, *_ in segs) / len(segs) - return sum(m[n] * (e - s) for n, s, e, _ in segs) / sum(e - s for _, s, e, _ in segs) - - -def _cv(values): - """Coefficient of variation (sample standard deviation over the mean).""" - mu = sum(values) / len(values) - if mu <= 0 or len(values) < 2: - return 0.0 - return math.sqrt(sum((v - mu) ** 2 for v in values) / (len(values) - 1)) / mu - - -def cut_copies(value, cuts): - """The number of cut points a value reaches (cut:0.5,1.5 gives 0, 1 or 2).""" - return sum(value >= c for c in cuts) - - -def reading(row, key): - r = row["_readings"] - if str(key) in r: - return r[str(key)] - if isinstance(key, int) and "more" in r and key > max(int(k) for k in r if k.isdigit()): - return r["more"].format(n=key) - return "" - - -def ploidy(row, sex): - """The copies of the row's chromosome a sample has: 2, or 1 on a male's - chrX or chrY; None where it cannot be said (a chrY row on a female or on - a sample of no known sex, a chrX row without a sex).""" - if row["chrom"] not in ("chrX", "chrY"): - return 2 - if sex == "male": - return 1 - if sex == "female" and row["chrom"] == "chrX": - return 2 - return None - - -def assess(row, m, q1=None, sex=None): - """The locus's block for mean depths m (every read) and q1 (reads of - MAPQ >= 1, or None for a row without a MAPQ 0 guard; mapq0_share is then - None). - - status is 'called' or 'not assessed'; reason names the guard that - stopped it: no_data, low_control_depth, controls_disagree, - low_mapq0_share, high_mapq0_share, below_floor, between_counts, - sex_unknown; status 'not applicable' (reason no_y) for a chrY row on a - female. sex matters only for a row on chrX or chrY: there the controls - sit on the same chromosome, so the scale follows the sample's count - there (ploidy). expected is the count with no change.""" - guarded = q1 is not None - if not guarded and (row["_mapq0_min"] is not None or row["_mapq0_max"] is not None): - raise ValueError(f"{row['locus']} has a MAPQ 0 guard: the MAPQ >= 1 depths are needed") - q1 = q1 if guarded else {} - tg, ct = row["_targets"], row["_controls"] - b = {"locus": row["locus"], "status": "not assessed", "reason": None, "estimate": None, "total": None, - "copies": None, "ci95": None, "class": None, "target_depth": None, "control_depth": None, - "depths": dict(m), "mapq0_share": None, "details": {}, "reading": "", "expected": None} - # expected: the copies with no change, the ploidy times the reference - # units (each target segment is one with target_depth sum: AZFc's 4 DAZ) - units = len(row["_targets"]) if row.get("target_depth") == "sum" else 1 - pl = ploidy(row, sex) - b["expected"] = None if pl is None else pl * units - if row["chrom"] == "chrY" and sex == "female": - b.update(status="not applicable", reason="no_y") - return b - if pl is None: - b["reason"] = "sex_unknown" - return b - if not m or any(n not in m for n, *_ in tg + ct): - b["reason"] = "no_data" - return b - for n, *_ in ct: - rest = [c for c in ct if c[0] != n] - if m[n] == 0 and rest and _combine(row["control_depth"], rest, m) >= row["_min_control"] > 0: - raise BrokenControl(f"BROKEN_CONTROL: control segment {n} of {row['locus']} has no read while the " - f"other controls have {_combine(row['control_depth'], rest, m):.1f}x; check its " - "coordinates in bin/cn_loci.tsv (or the sample lacks that stretch)") - T, C = _combine(row["target_depth"], tg, m), _combine(row["control_depth"], ct, m) - tq = _combine(row["target_depth"], tg, {n: q1.get(n, 0.0) for n, *_ in tg}) - # The MAPQ 0 share is known only when the MAPQ >= 1 pass was given. - share = (1 - tq / T if T > 0 else 0.0) if guarded else None - b.update(target_depth=T, control_depth=C, mapq0_share=share) - low = min(m[n] for n, *_ in ct) if row["control_depth"] == "flanks" else C - if low < row["_min_control"] or C <= 0: - b["reason"] = "low_control_depth" - return b - if row["control_depth"] == "flanks" and row["_max_ratio"] is not None and len(ct) > 1: - vals = [m[n] for n, *_ in ct] - if min(vals) <= 0 or max(vals) / min(vals) > row["_max_ratio"]: - b["reason"] = "controls_disagree" - return b - total = row["_scale"] * pl / 2 * T / C - est = total - row["_offset"] - b.update(total=total, estimate=est, - details={n: row["_scale"] * pl / 2 * m[n] / C for n, *_ in row["_details"] if n in m}) - # With no copy left there is no read to judge the aligner by (a row with - # an offset has a floor of its own below, after this guard). - no_copy = row["_offset"] == 0 and round(est) == 0 - if row["_mapq0_min"] is not None and not no_copy and share < row["_mapq0_min"]: - b["reason"] = "low_mapq0_share" - return b - if row["_mapq0_max"] is not None and share > row["_mapq0_max"]: - b["reason"] = "high_mapq0_share" - return b - if row["_offset"] > 0 and est <= 0: - b["reason"] = "below_floor" - return b - mode, vals = row["_call"] - if row["interval"] == "cv": - rel = math.sqrt(_cv([m[n] for n, *_ in tg]) ** 2 / len(tg) + _cv([m[n] for n, *_ in ct]) ** 2 / len(ct)) - b["ci95"] = (max(0.0, total * math.exp(-Z95 * rel) - row["_offset"]), - total * math.exp(Z95 * rel) - row["_offset"]) - if mode == "psv": - # the module total (gene plus pseudogene copies); og cn reads the sites - n = round(est) - if abs(est - n) > vals[0]: - b["reason"] = "between_counts" - return b - b["module_total"] = n - elif mode == "round": - n = round(est) - if abs(est - n) > vals[0]: - b["reason"] = "between_counts" - return b - b["copies"] = n - b["reading"] = reading(row, n) - elif mode == "cut": - b["copies"] = cut_copies(est, vals) - b["reading"] = reading(row, b["copies"]) - else: - lo, hi = b["ci95"] - b["class"] = "low" if hi < vals[0] else "high" if lo > vals[1] else "typical" - b["reading"] = reading(row, b["class"]) - b["status"] = "called" - return b diff --git a/bin/traits_report.py b/bin/traits_report.py index 6b984f1cb..0d8e67310 100755 --- a/bin/traits_report.py +++ b/bin/traits_report.py @@ -1,90 +1,17 @@ -#!/usr/bin/env python3 -"""traits_report.py: the traits table and the genotype rules of step 42, read from the gVCF genotypes. +# remnant: kept for collect_summary, demo and catalog tests; deleted in lane R3. +"""Step 42 metadata still read by the Python report collectors. - traits_report.py --table assets/traits.tsv --rules assets/genotype_rules.tsv --genotypes G.tsv \\ - [--phased P.vcf] --sample S --out S_traits.tsv --rules-out S_genotype_rules.tsv - traits_report.py --rules assets/genotype_rules.tsv --genotypes G.tsv --sample S --phase-sites IN.vcf - traits_report.py --check-table assets/traits.tsv [--rules assets/genotype_rules.tsv] - -G.tsv is what bcftools writes in step 42 (scripts/42-traits.sh and the -Nextflow TRAITS_SITES process run the same commands): one line per gVCF record -at a position of the traits table or of a rule site, after the reference -blocks are expanded to one record per base, as CHROM POS REF ALT GENOTYPE GQ, -the genotype as bases (C/T, or C|T when phased). A position with no line has -no genotype: it is "not_covered", never read as the reference. bcftools drops a -record with a missing allele (a no-call or a half call such as ./T) before -this script, so such a position has no line and reads as not_covered too; a -half call that still reaches here is other_allele. - -Each traits table row becomes one output row with the genotype (the reference -allele first), a status (called, not_covered, other_allele) and the reading -for that genotype. A gVCF whose REF differs from the table's reference base at -a position is not GRCh38 (or the table is wrong): the run stops. - -Each rule of the rules table (assets/genotype_rules.tsv) reads its sites -together and becomes one line of --rules-out: the two haplotypes the genotypes -make, named by the rule's haplotype lines, and the combination line of that -diplotype. A rule with needs_phase no takes the combination line of every -diplotype the unphased genotypes allow (APOE's double heterozygote is e2/e4 -or e1/e3). A rule with needs_phase yes and two or more heterozygous sites -needs their phase: --phase-sites writes those sites as a VCF for WhatsHap, -and --phased reads WhatsHap's answer; sites not phased in one block leave the -rule "unphased", not read. The APOE rule's line also goes into the traits -table, right after its last site row, where the reports read it. - ---check-table checks the schema of the table (and of the rules, with --rules) -and exits 1 on the first list of problems; tests/test_traits_table.py and -tests/test_traits_report.py run the same checks. Standard library only (it -runs in the plain python image and in the WhatsHap image). - -Copy-aware markers (MARKERS): two well-sourced markers of the rule catalog's -list sit in sequence the genome holds more than once, so a gVCF genotype at -one position cannot read them and they are not rule lines. Each is read from -the BAM instead, by the step that counts its copies (og cn marker-regions, -marker-read and call --marker, in rust/crates/og-cn): the reads at every -GRCh38 copy of the site are added up, and the share that carries the marker's -base is a fraction of copies, not a diploid genotype. MARKERS stays here for -the reports (bin/collect_summary.py reads the marker tables with it). - LPA_KIV2_4925 the LPA KIV-2 4925G>A splice variant (Coassin et al. 2017, - PMID 28444229), read by step 46 beside the KIV-2 count - SMN1_27134 SMN1 g.27134T>G (rs143838139; Luo et al. 2014, PMID - 23788250), read by step 35 beside the SMN1 psCN +The executable implementation is og traits table, phase-sites and check. +Copy-aware marker readers are og cn marker-read. """ -import argparse -import itertools -import os -import re -import sys - COLUMNS = ["chrom", "pos", "rsid", "gene", "ref", "alt", "trait", "hom_ref", "het", "hom_alt", "ancestry", "note", "pmids"] OUT_COLUMNS = ["rsid", "gene", "chrom", "pos", "trait", "genotype", "status", "reading", "ancestry", "note", "pmids"] RULE_OUT_COLUMNS = ["rule", "gene", "trait", "rsids", "chrom", "pos", "genotypes", "gq", "phase", "result", "status", "reading", "ancestry", "note", "pmids"] -# The columns of each kind of line of the rules table, after the kind and the rule id. -RULE_KINDS = {"rule": ["gene", "trait", "needs_phase", "ancestry", "note", "pmids"], - "site": ["rsid", "chrom", "pos", "ref", "alt"], - "haplotype": ["name", "alleles"], - "combination": ["diplotypes", "label", "reading"]} -CHROMS = {f"chr{n}" for n in range(1, 23)} -BASES = set("ACGT") -# APOE: rs429358 C (Arg112) and rs7412 C (Arg158) make e4; T and T make e2. -# The traits table must keep these two rows: the reports show them, and the -# APOE rule's line goes after them. -APOE_E4_SITE, APOE_E2_SITE = "rs429358", "rs7412" -APOE_ALLELES = {APOE_E4_SITE: ("T", "C"), APOE_E2_SITE: ("C", "T")} -# Rules whose line also goes into the traits table, where the reports read it -# (they fold the APOE lines away). Every other rule's line is in --rules-out. TRAITS_TABLE_RULES = ("APOE",) -# The rules table beside the traits table in the repository, for report()'s callers -# that give none; the command line always takes --rules. -DEFAULT_RULES = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "assets", "genotype_rules.tsv") -MAX_RULE_SITES = 8 # the completeness check walks 3^sites genotypes -NOT_COVERED = "No genotype at this position in the gVCF (no reads, or a no-call): not read." -OTHER_ALLELE = "An allele this table does not describe at this position: not read." - class TableError(Exception): pass @@ -115,425 +42,6 @@ def read_table(path): return rows -def text_errors(where, text): - errs = [] - if "snpedia" in text.lower(): - errs.append(f"{where}: mentions SNPedia; the table takes no text from it") - if '"' in text or not text.isascii(): - errs.append(f"{where}: a double quote or a non-ASCII character (the reports read the table as plain TSV)") - return errs - - -def site_errors(where, r): - """Schema problems of one site: chrom, pos, rsid, ref and alt.""" - errs = [] - if r["chrom"] not in CHROMS: - errs.append(f"{where}: chrom '{r['chrom']}' is not chr1 to chr22") - if not re.fullmatch(r"[1-9][0-9]*", r["pos"]): - errs.append(f"{where}: pos '{r['pos']}' is not a positive integer") - if not re.fullmatch(r"rs[1-9][0-9]*", r["rsid"]): - errs.append(f"{where}: rsid is not rs") - for k in ("ref", "alt"): - if r[k] not in BASES: - errs.append(f"{where}: {k} '{r[k]}' is not one of A, C, G, T") - if r["ref"] == r["alt"]: - errs.append(f"{where}: ref and alt are the same base") - return errs - - -def check_rows(rows): - """Every schema problem of the table rows, as messages (empty when it is valid).""" - errs, ids, sites = [], set(), set() - for r in rows: - where = f"line {r['line']} ({r['rsid']})" - errs += site_errors(where, r) - if r["rsid"] in ids: - errs.append(f"{where}: rsid appears twice") - ids.add(r["rsid"]) - if (r["chrom"], r["pos"]) in sites: - errs.append(f"{where}: position {r['chrom']}:{r['pos']} appears twice") - sites.add((r["chrom"], r["pos"])) - for k in ("gene", "trait", "hom_ref", "het", "hom_alt", "note"): - if not r[k].strip(): - errs.append(f"{where}: {k} is empty") - if r["ancestry"] not in ("yes", "no"): - errs.append(f"{where}: ancestry '{r['ancestry']}' is not yes or no") - if not re.fullmatch(r"[1-9][0-9]*(,[1-9][0-9]*)*", r["pmids"]): - errs.append(f"{where}: pmids '{r['pmids']}' is not a comma-separated list of PubMed ids") - errs += text_errors(where, " ".join(r[k] for k in COLUMNS)) - by_id = {r["rsid"]: r for r in rows} - for rs, (ref, alt) in APOE_ALLELES.items(): - r = by_id.get(rs) - if r is None: - errs.append(f"APOE row {rs} is missing") - elif (r["ref"], r["alt"]) != (ref, alt) or r["chrom"] != "chr19": - errs.append(f"APOE row {rs} must be chr19 {ref}>{alt}, is {r['chrom']} {r['ref']}>{r['alt']}") - return errs - - -# --- The rules table --------------------------------------------------------------- - -def read_rules(path): - """{rule id: {gene, trait, needs_phase, ancestry, note, pmids, line, sites, haplotypes, - combinations}} in file order. Raises TableError on a line of an unknown kind or with - the wrong column count, and on a site, haplotype or combination line before its rule line.""" - rules = {} - with open(path, encoding="utf-8") as f: - for n, line in enumerate(f, 1): - line = line.rstrip("\n") - if not line or line.startswith("#"): - continue - cells = line.split("\t") - kind = cells[0] - if kind not in RULE_KINDS: - raise TableError(f"{path}:{n}: kind '{kind}' is not one of {', '.join(RULE_KINDS)}") - want = 2 + len(RULE_KINDS[kind]) - if len(cells) != want: - raise TableError(f"{path}:{n}: a {kind} line has {len(cells)} columns, expected {want}") - rid = cells[1] - d = dict(zip(RULE_KINDS[kind], cells[2:]), line=n) - if kind == "rule": - if rid in rules: - raise TableError(f"{path}:{n}: rule {rid} has two rule lines") - rules[rid] = dict(d, id=rid, sites=[], haplotypes=[], combinations=[]) - continue - if rid not in rules: - raise TableError(f"{path}:{n}: a {kind} line of rule '{rid}' before its rule line") - rules[rid]["sites" if kind == "site" else kind + "s"].append(d) - if not rules: - raise TableError(f"{path}: no rule line") - return rules - - -def diplotype_key(rule, a, b): - """'a/b' with the two names in the order of the rule's haplotype lines.""" - order = [h["name"] for h in rule["haplotypes"]] - return "/".join(sorted((a, b), key=order.index)) - - -def allowed_diplotypes(rule, genotypes): - """The diplotype keys, in haplotype order, whose two haplotypes give these unphased - genotypes (one [allele, allele] per site).""" - haps = rule["haplotypes"] - out = [] - for i, j in itertools.combinations_with_replacement(range(len(haps)), 2): - a, b = haps[i]["alleles"].split(","), haps[j]["alleles"].split(",") - if all(sorted((a[k], b[k])) == sorted(g) for k, g in enumerate(genotypes)): - out.append(f"{haps[i]['name']}/{haps[j]['name']}") - return out - - -def check_rules(rules, table_rows=None): - """Every schema problem of the rules, as messages (empty when they are valid). With - table_rows, also: a rule site that is a traits table row has the row's chrom, pos, ref - and alt, and the rules of TRAITS_TABLE_RULES exist with every site in the table.""" - errs = [] - by_rs = {r["rsid"]: r for r in table_rows or []} - for rid, rule in rules.items(): - n0 = len(errs) - where = f"rule {rid} (line {rule['line']})" - if not re.fullmatch(r"[A-Za-z0-9_]+", rid): - errs.append(f"{where}: the rule id is not letters, digits and _") - for k in ("gene", "trait", "note"): - if not rule[k].strip(): - errs.append(f"{where}: {k} is empty") - for k in ("needs_phase", "ancestry"): - if rule[k] not in ("yes", "no"): - errs.append(f"{where}: {k} '{rule[k]}' is not yes or no") - if not re.fullmatch(r"[1-9][0-9]*(,[1-9][0-9]*)*", rule["pmids"]): - errs.append(f"{where}: pmids '{rule['pmids']}' is not a comma-separated list of PubMed ids") - sites = rule["sites"] - if not 2 <= len(sites) <= MAX_RULE_SITES: - errs.append(f"{where}: {len(sites)} sites, expected 2 to {MAX_RULE_SITES}") - seen = set() - for s in sites: - w = f"{where}, site line {s['line']} ({s['rsid']})" - errs += site_errors(w, s) - if s["rsid"] in seen or (s["chrom"], s["pos"]) in seen: - errs.append(f"{w}: the site appears twice in the rule") - seen |= {s["rsid"], (s["chrom"], s["pos"])} - t = by_rs.get(s["rsid"]) - if t and (t["chrom"], t["pos"], t["ref"], t["alt"]) != (s["chrom"], s["pos"], s["ref"], s["alt"]): - errs.append(f"{w}: the traits table has {t['chrom']}:{t['pos']} {t['ref']}>{t['alt']}, " - f"the rule {s['chrom']}:{s['pos']} {s['ref']}>{s['alt']}") - if len({s["chrom"] for s in sites}) > 1: - errs.append(f"{where}: the sites are on more than one chromosome") - names, tuples = [], set() - for h in rule["haplotypes"]: - w = f"{where}, haplotype line {h['line']} ({h['name']})" - if not re.fullmatch(r"[A-Za-z0-9*.]+", h["name"]): - errs.append(f"{w}: the name is not letters, digits, * and .") - if h["name"] in names: - errs.append(f"{w}: the name appears twice") - names.append(h["name"]) - al = h["alleles"].split(",") - if len(al) != len(sites): - errs.append(f"{w}: {len(al)} alleles for {len(sites)} sites") - continue - for a, s in zip(al, sites): - if a not in (s["ref"], s["alt"]): - errs.append(f"{w}: allele {a} at {s['rsid']} is neither its ref {s['ref']} nor its alt {s['alt']}") - if tuple(al) in tuples: - errs.append(f"{w}: the same alleles as another haplotype") - tuples.add(tuple(al)) - if len(names) < 2: - errs.append(f"{where}: fewer than two haplotypes") - keys = {} - for c in rule["combinations"]: - w = f"{where}, combination line {c['line']} ({c['diplotypes']})" - for k in ("label", "reading"): - if not c[k].strip(): - errs.append(f"{w}: {k} is empty") - errs += text_errors(w, " ".join((c["diplotypes"], c["label"], c["reading"]))) - if c["diplotypes"] in keys: - errs.append(f"{w}: the diplotypes appear twice") - keys[c["diplotypes"]] = c - for d in c["diplotypes"].split("|"): - pair = d.split("/") - if len(pair) != 2 or any(p not in names for p in pair): - errs.append(f"{w}: '{d}' is not two of the rule's haplotypes joined by /") - elif diplotype_key(rule, *pair) != d: - errs.append(f"{w}: '{d}' is not in the order of the haplotype lines " - f"(write {diplotype_key(rule, *pair)})") - errs += text_errors(where, " ".join(str(rule[k]) for k in ("gene", "trait", "note"))) - if len(errs) > n0: - continue - # Completeness: the combination lines are exactly what the genotypes can give. - if rule["needs_phase"] == "yes": - want = {f"{a}/{b}" for a, b in itertools.combinations_with_replacement(names, 2)} - else: - want = set() - choices = [[[s["ref"]] * 2, [s["ref"], s["alt"]], [s["alt"]] * 2] for s in sites] - for g in itertools.product(*choices): - key = "|".join(allowed_diplotypes(rule, g)) - if key: - want.add(key) - for k in sorted(want - set(keys)): - errs.append(f"{where}: no combination line for {k}") - for k in sorted(set(keys) - want): - errs.append(f"{where}: the combination line {k} can never be read " - + ("(a rule that needs phase reads one diplotype)" if "|" in k else "from these haplotypes")) - if table_rows is not None: - for rid in TRAITS_TABLE_RULES: - if rid not in rules: - errs.append(f"rule {rid} is missing: its line goes into the traits table, where the reports read it") - else: - for s in rules[rid]["sites"]: - if s["rsid"] not in by_rs: - errs.append(f"rule {rid}: site {s['rsid']} has no traits table row") - return errs - - -# --- Genotypes ---------------------------------------------------------------------- - -def read_genotypes(path): - """{(chrom, pos): [(REF, [allele, allele], GQ), ...]} from bcftools' CHROM POS REF ALT GT [GQ] lines.""" - recs = {} - with open(path, encoding="utf-8") as f: - for n, line in enumerate(f, 1): - line = line.rstrip("\n") - if not line: - continue - c = line.split("\t") - if len(c) < 5: - raise TableError(f"{path}:{n}: {len(c)} columns, expected CHROM POS REF ALT GENOTYPE [GQ]") - gq = c[5] if len(c) > 5 and c[5] else "." - recs.setdefault((c[0], c[1]), []).append((c[2], re.split(r"[/|]", c[4]), gq)) - return recs - - -def site_genotype(site, recs): - """(alleles sorted with the reference first, status, GQ) of one site from its gVCF records.""" - if not recs: - return None, "not_covered", "." - for r in recs: - if r[0][:1] != site["ref"]: - raise TableError(f"{site['rsid']} at {site['chrom']}:{site['pos']}: the gVCF has REF {r[0]}, the table " - f"{site['ref']} (the GRCh38 base). Is the gVCF on GRCh38?") - snv = [r for r in recs if all(len(a) == 1 for a in r[1])] - _, alleles, gq = (snv or recs)[0] - order = {site["ref"]: 0, site["alt"]: 1} - alleles = sorted(alleles, key=lambda a: (order.get(a, 2), a)) - if len(alleles) != 2 or any(a not in order for a in alleles): - return alleles, "other_allele", gq - return alleles, "called", gq - - -def call(row, recs): - """(genotype, status, reading) of one traits table row from its gVCF records.""" - alleles, status, _ = site_genotype(row, recs) - if status == "not_covered": - return ".", "not_covered", NOT_COVERED - gt = "/".join(alleles) - if status == "other_allele": - return gt, "other_allele", OTHER_ALLELE - return gt, "called", row[("hom_ref", "het", "hom_alt")[alleles.count(row["alt"])]] - - -def read_phased(path): - """{(chrom, pos): (allele of haplotype 1, allele of haplotype 2, phase block)} of the - phased records of WhatsHap's VCF; unphased records are left out.""" - out = {} - with open(path, encoding="utf-8") as f: - for line in f: - if line.startswith("#") or not line.strip(): - continue - c = line.rstrip("\n").split("\t") - if len(c) < 10: - continue - fmt = dict(zip(c[8].split(":"), c[9].split(":"))) - gt, ps = fmt.get("GT", ""), fmt.get("PS", ".") - if "|" not in gt or ps in (".", ""): - continue - bases = [c[3]] + c[4].split(",") - try: - a, b = (bases[int(i)] for i in gt.split("|")) - except (ValueError, IndexError): - continue - out[(c[0], c[1])] = (a, b, ps) - return out - - -def phase_sites(rules, recs): - """The (chrom, pos, rsid, ref, alt) of the heterozygous sites of every rule that needs - phase and has two or more of them, each site once, in rule order.""" - out, seen = [], set() - for rule in rules.values(): - if rule["needs_phase"] != "yes": - continue - g = [(s, site_genotype(s, recs.get((s["chrom"], s["pos"]), []))) for s in rule["sites"]] - if any(st != "called" for _, (_, st, _) in g): - continue - het = [s for s, (al, _, _) in g if al[0] != al[1]] - if len(het) < 2: - continue - for s in het: - k = (s["chrom"], s["pos"]) - if k not in seen: - seen.add(k) - out.append((s["chrom"], s["pos"], s["rsid"], s["ref"], s["alt"])) - return out - - -def write_phase_vcf(path, sample, sites): - """The sites as a one-sample VCF of 0/1 genotypes, sorted, for whatshap phase.""" - contigs = sorted({c for c, *_ in sites}, key=lambda c: int(c[3:])) - tmp = path + ".tmp" - with open(tmp, "w", encoding="utf-8") as f: - f.write("##fileformat=VCFv4.2\n") - for c in contigs: - f.write(f"##contig=\n") - f.write('##FORMAT=\n') - f.write("#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\tFORMAT\t" + sample + "\n") - for c, p, rs, ref, alt in sorted(sites, key=lambda s: (int(s[0][3:]), int(s[1]))): - f.write(f"{c}\t{p}\t{rs}\t{ref}\t{alt}\t.\tPASS\t.\tGT\t0/1\n") - os.replace(tmp, path) - - -def rule_line(rule, recs, phased): - """One output line of a rule. phased: read_phased's dict, or None when no phasing ran.""" - sites = rule["sites"] - g = [site_genotype(s, recs.get((s["chrom"], s["pos"]), [])) for s in sites] - rsids = [s["rsid"] for s in sites] - line = {"rule": rule["id"], "gene": rule["gene"], "trait": rule["trait"], "rsids": ",".join(rsids), - "chrom": sites[0]["chrom"], "pos": ",".join(s["pos"] for s in sites), - "genotypes": ",".join("/".join(al) if al else "." for al, _, _ in g), - "gq": ",".join(gq for _, _, gq in g), "ancestry": rule["ancestry"], "note": rule["note"], - "pmids": rule["pmids"]} - phase = "not_used" if rule["needs_phase"] == "no" else "not_needed" - statuses = [st for _, st, _ in g] - if any(st != "called" for st in statuses): - need = (f"both {rsids[0]} and {rsids[1]}" if len(rsids) == 2 - else "every one of " + ", ".join(rsids[:-1]) + " and " + rsids[-1]) - return dict(line, phase=".", result=".", - status="not_covered" if "not_covered" in statuses else "other_allele", - reading=f"Not read: needs a genotype at {need}.") - alleles = [al for al, _, _ in g] - combos = {c["diplotypes"]: c for c in rule["combinations"]} - if rule["needs_phase"] == "no": - key = "|".join(allowed_diplotypes(rule, alleles)) - else: - het = [i for i, al in enumerate(alleles) if al[0] != al[1]] - h1, h2 = [al[0] for al in alleles], [al[1] for al in alleles] - if len(het) >= 2: - blocks = {(phased or {}).get((sites[i]["chrom"], sites[i]["pos"]), (None, None, None))[2] for i in het} - if None in blocks or len(blocks) != 1: - why = ("no read or read pair covered them together" - if phased is not None else "no phasing ran (no BAM, or phasing failed)") - return dict(line, phase="unphased", result=".", status="unphased", - reading=f"Heterozygous at {len(het)} of the {len(sites)} sites, and {why}, so which " - "alleles share a copy is not known: not read.") - for i in het: - h1[i], h2[i] = phased[(sites[i]["chrom"], sites[i]["pos"])][:2] - phase = "read_backed" - by_alleles = {tuple(h["alleles"].split(",")): h["name"] for h in rule["haplotypes"]} - n1, n2 = by_alleles.get(tuple(h1)), by_alleles.get(tuple(h2)) - unknown = [",".join(h) for h, n in ((h1, n1), (h2, n2)) if n is None] - if unknown: - return dict(line, phase=phase, result=".", status="other_haplotype", - reading=f"A haplotype this rule does not describe ({' and '.join(unknown)} at " - f"{', '.join(rsids)}): not read.") - key = diplotype_key(rule, n1, n2) - if not key or key not in combos: - return dict(line, phase=phase, result=".", status="other_haplotype", - reading="A combination of haplotypes this rule does not describe: not read.") - c = combos[key] - return dict(line, phase=phase, result=c["label"], status="called", reading=c["reading"]) - - -def rule_lines(rules, recs, phased=None): - """One output line per rule, in the order of the rules table.""" - return [rule_line(r, recs, phased) for r in rules.values()] - - -def report(rows, recs, rules=None, phased=None, lines=None): - """The traits output rows: one per table row, with the line of each rule of - TRAITS_TABLE_RULES right after its last site row. rules defaults to - DEFAULT_RULES; lines, rule_lines' output, is computed when not given.""" - if rules is None: - rules = read_rules(DEFAULT_RULES) - if lines is None: - lines = rule_lines(rules, recs, phased) - after = {} - for rid in TRAITS_TABLE_RULES: - rule = rules.get(rid) - if rule: - members = {s["rsid"] for s in rule["sites"]} - last = max((i for i, r in enumerate(rows) if r["rsid"] in members), default=None) - if last is not None: - after[last] = next(ln for ln in lines if ln["rule"] == rid) - out = [] - for i, r in enumerate(rows): - gt, status, reading = call(r, recs.get((r["chrom"], r["pos"]), [])) - out.append({"rsid": r["rsid"], "gene": r["gene"], "chrom": r["chrom"], "pos": r["pos"], - "trait": r["trait"], "genotype": gt, "status": status, "reading": reading, - "ancestry": r["ancestry"], "note": r["note"], "pmids": r["pmids"]}) - if i in after: - ln = after[i] - out.append({"rsid": ln["rsids"].replace(",", "+"), "gene": ln["gene"], "chrom": ln["chrom"], - "pos": ln["pos"], "trait": ln["trait"], "genotype": ln["result"], "status": ln["status"], - "reading": ln["reading"], "ancestry": ln["ancestry"], "note": ln["note"], - "pmids": ln["pmids"]}) - return out - - -# --- Copy-aware markers -------------------------------------------------------------- - -# Each marker: the dbSNP record of one copy (rsID, chrom, POS, REF, ALT of -# dbSNP b157's GRCh38 VCF, read from NCBI's Variation API on 2026-11-07), every -# GRCh38 copy of the site (1-based), the plus-strand base pair read there, and -# the GRCh38 sequence of 10 bases on each side of every copy (UCSC hg38 and -# Ensembl GRCh38 agree, 2026-11-07): the same 21 bases at each copy, which is -# why the reads cannot be placed on one copy. tests/test_traits_report.py -# checks every copy against the context and the record against the marker, and -# that og cn reads the same copies; tests/e2e/genotype-rules-2-smn1-marker.sh -# compares the records with dbSNP. -# min_depth fewer reads over all copies together: too little depth -# min_alt reads with the marker's base for 'present' ... -# min_fraction ... and a share of the reads at least this -# max_absent this many reads with the marker's base or fewer: 'not seen' -# (one or two reads can be sequencing errors); between the two -# the marker is 'uncertain' MARKERS = { # KIV-2 4925G>A: the last base of the second KIV-2 exon (its donor splice # site), G>A on LPA's minus strand, so C>T on the plus strand. GRCh38 holds @@ -560,108 +68,3 @@ def report(rows, recs, rules=None, phased=None, lines=None): }, } MARKER_STATUSES = ("present", "not seen", "uncertain", "too little depth", "not assessed") - - -def marker_errors(name, m=None, dbsnp=None): - """Problems of one marker (empty when it is consistent): the dbSNP record - is one of its copies with its ref and alt, on the plus strand and not - swapped; every copy sits in the middle of the same context, whose middle - base is the ref.""" - m = MARKERS[name] if m is None else m - rs, chrom, pos, ref, alt = m["dbsnp"] if dbsnp is None else dbsnp - comp = {"A": "T", "C": "G", "G": "C", "T": "A"} - errs = [] - if chrom != m["chrom"] or pos not in m["copies"]: - errs.append(f"{name}: dbSNP places {rs} at {chrom}:{pos}, not at one of the marker's copies") - if (m["ref"], m["alt"]) == (alt, ref): - errs.append(f"{name}: ref and alt are swapped (marker {m['ref']}>{m['alt']}, dbSNP {ref}>{alt})") - elif (m["ref"], m["alt"]) == (comp.get(ref), comp.get(alt)): - errs.append(f"{name}: the alleles are the minus-strand bases (marker {m['ref']}>{m['alt']}, " - f"dbSNP {ref}>{alt}): write the plus-strand bases") - elif (m["ref"], m["alt"]) != (ref, alt): - errs.append(f"{name}: marker {m['ref']}>{m['alt']}, dbSNP {ref}>{alt}") - ctx = m["context"] - if len(ctx) % 2 != 1 or ctx[len(ctx) // 2] != m["ref"]: - errs.append(f"{name}: the middle base of the context is not the ref {m['ref']}") - if len(set(m["copies"])) != len(m["copies"]): - errs.append(f"{name}: a copy is listed twice") - return errs - - -def write_tsv(path, out, columns=OUT_COLUMNS): - tmp = path + ".tmp" - with open(tmp, "w", encoding="utf-8") as f: - f.write("\t".join(columns) + "\n") - for o in out: - f.write("\t".join(str(o[k]) for k in columns) + "\n") - os.replace(tmp, path) - - -def main(argv=None): - ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) - ap.add_argument("--table", help="assets/traits.tsv") - ap.add_argument("--rules", help="assets/genotype_rules.tsv") - ap.add_argument("--genotypes", help="bcftools' CHROM POS REF ALT GENOTYPE GQ lines at the table and rule positions") - ap.add_argument("--phased", help="WhatsHap's VCF of the --phase-sites sites (absent: no phasing ran)") - ap.add_argument("--sample") - ap.add_argument("--out", help="the sample's traits table (TSV)") - ap.add_argument("--rules-out", help="the sample's genotype rules (TSV)") - ap.add_argument("--phase-sites", metavar="VCF", help="write the sites WhatsHap must phase and exit") - ap.add_argument("--check-table", metavar="TABLE", help="check the table's schema (and --rules') and exit") - a = ap.parse_args(argv) - try: - if a.check_table: - rows = read_table(a.check_table) - errs = check_rows(rows) - if a.rules: - errs += check_rules(read_rules(a.rules), rows) - for e in errs: - print(f"ERROR: {e}", file=sys.stderr) - if not errs: - print(f"{a.check_table}{' and ' + a.rules if a.rules else ''}: schema OK") - return 1 if errs else 0 - if a.phase_sites: - if not (a.rules and a.genotypes and a.sample): - ap.error("--phase-sites needs --rules, --genotypes and --sample") - rules = read_rules(a.rules) - errs = check_rules(rules) - if errs: - for e in errs: - print(f"ERROR: {a.rules}: {e}", file=sys.stderr) - return 1 - sites = phase_sites(rules, read_genotypes(a.genotypes)) - write_phase_vcf(a.phase_sites, a.sample, sites) - print(f"{a.sample}: {len(sites)} heterozygous rule sites to phase. Written: {a.phase_sites}") - return 0 - if not (a.table and a.rules and a.genotypes and a.sample and a.out and a.rules_out): - ap.error("give --table, --rules, --genotypes, --sample, --out and --rules-out " - "(or --phase-sites, or --check-table)") - rows = read_table(a.table) - rules = read_rules(a.rules) - errs = [f"{a.table}: {e}" for e in check_rows(rows)] + [f"{a.rules}: {e}" for e in check_rules(rules, rows)] - if errs: - for e in errs: - print(f"ERROR: {e}", file=sys.stderr) - return 1 - phased = read_phased(a.phased) if a.phased else None - recs = read_genotypes(a.genotypes) - lines = rule_lines(rules, recs, phased) - out = report(rows, recs, rules, phased, lines) - except (TableError, OSError) as e: - print(f"ERROR: {e}", file=sys.stderr) - return 1 - write_tsv(a.out, out) - write_tsv(a.rules_out, lines, RULE_OUT_COLUMNS) - counts = {} - for o in out: - counts[o["status"]] = counts.get(o["status"], 0) + 1 - print(f"{a.sample}: {len(rows)} table rows; " + ", ".join(f"{k} {v}" for k, v in sorted(counts.items())) - + f" (APOE line included). Written: {a.out}") - print(f"{a.sample}: {len(lines)} genotype rules; " - + ", ".join(f"{ln['rule']} {ln['result'] if ln['status'] == 'called' else ln['status']}" for ln in lines) - + f". Written: {a.rules_out}") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/docs/42-traits.md b/docs/42-traits.md index 7c6affb70..ad3aa25cf 100644 --- a/docs/42-traits.md +++ b/docs/42-traits.md @@ -12,7 +12,7 @@ These are the results most people find easiest to read, and the pipeline had no - **bcftools** (`bcftools convert --gvcf2vcf`, `view`, `query`) reads the genotypes at the table's positions from the gVCF. - **WhatsHap** (`whatshap phase`) phases the heterozygous sites of a rule that needs phase from the BAM's reads. -- **`bin/traits_report.py`** (Python standard library) reads each genotype against the table, and each rule against the rule table. +- **`og traits table`** (Rust) reads each genotype against the table, and each rule against the rule table. ## Docker Image @@ -122,12 +122,12 @@ Kim et al. (2003) found five TAS2R38 haplotypes worldwide. The rule reads the tw ### Checks on the rule table -- `bin/traits_report.py --check-table assets/traits.tsv --rules assets/genotype_rules.tsv` (also run by every step-42 run and by `tests/test_traits_report.py`): every haplotype allele is its site's ref or alt, the combination lines are exactly the pairs the genotypes can give (none missing, none that can never be read), and a site that is also a traits table row has the row's position and bases. +- `bin/og traits check --table assets/traits.tsv --rules assets/genotype_rules.tsv` (also run by every step-42 run and by `tests/test_traits_report.py`): every haplotype allele is its site's ref or alt, the combination lines are exactly the pairs the genotypes can give (none missing, none that can never be read), and a site that is also a traits table row has the row's position and bases. - `scripts/ci/check-rule-alleles.sh` checks every site against dbSNP: the rsID at that GRCh38 position, ref as dbSNP's REF and alt among its ALT alleles. A pair written the wrong way round, or as the minus-strand bases (TAS2R38 lies on the minus strand, where the papers name the codon bases), fails with the reason. The dbSNP records it compares with sit in the script, taken from dbSNP's GRCh38 VCF of the build `CONSUMER_DBSNP_BUILD` pins in `versions.env` (b157); `--online` reads them again over HTTPS and fails when they differ, and the e2e case `genotype-rules-1-phase` runs it. A C/G or A/T site is its own complement, so there a strand mistake keeps valid alleles: the check prints a note for each such site (rs713598 is one), and its haplotype alleles rest on the codon: Pro49 has C at the first base of codon 49 on the coding strand (NM_176817.5 c.145C, where Ala49 has G), so the PAV base on GRCh38 is its complement, G. The other such sites: rs17580 (SERPINA1 S, minus strand: Glu264 GAA, Val264 GTA, so the S base on GRCh38 is the complement of T, A), rs1799945 (HFE H63D, plus strand: His63 CAT, Asp63 GAT, so D is G) and rs1333049 (no gene; the risk allele C is the plus-strand base of the WTCCC 2007 record). ### Adding a rule -Write the rule, site, haplotype and combination lines from the primary papers. Take each site's position and bases from dbSNP's GRCh38 record (plus strand), and turn each haplotype's codon bases into plus-strand bases for a gene on the minus strand. A new site needs its dbSNP record in `scripts/ci/check-rule-alleles.sh`: run it with `--print-table` and add each printed line at the end of the script after `#: `. Then run `python3 tests/test_traits_report.py`. +Write the rule, site, haplotype and combination lines from the primary papers. Take each site's position and bases from dbSNP's GRCh38 record (plus strand), and turn each haplotype's codon bases into plus-strand bases for a gene on the minus strand. A new site needs its dbSNP record in `scripts/ci/check-rule-alleles.sh`: run it with `--print-table` and add each printed line at the end of the script after `#: `. Then run `OG=bin/og python3 tests/test_traits_report.py`. ## APOE diff --git a/docs/rust.md b/docs/rust.md index c2ed48f65..99637ea75 100644 --- a/docs/rust.md +++ b/docs/rust.md @@ -111,3 +111,8 @@ To add a primitive: write it in `pycompat` with unit tests, add an operation to - **Release** (`release.yml`): a tag builds the four binaries, writes `SHA256SUMS`, attests them and attaches all five files to the release. A pull request that changes `rust/` or the workflow builds them as a dry run. [Testing](testing.md) has the rest of CI. + +Step 42 uses `og traits table`, `og traits phase-sites` and `og traits check`. +The readers retain only catalog sites. The trait and genotype-rule output +columns and readings stay the same. `bin/traits_report.py` keeps report +metadata until the Python summary and report collectors move. diff --git a/modules/local/traits/main.nf b/modules/local/traits/main.nf index b7be4b1c8..b6087ab75 100644 --- a/modules/local/traits/main.nf +++ b/modules/local/traits/main.nf @@ -13,7 +13,7 @@ an empty file when no rule has two such sites TRAITS _traits.tsv: the reading of each genotype from the table, and the APOE line; _genotype_rules.tsv: one - line per rule (bin/traits_report.py) + line per rule (og traits table) Runs for the samples with a gVCF only (DEEPVARIANT's, or one the samplesheet gives); main.nf logs the samples it skips. A sample without @@ -89,7 +89,7 @@ process TRAITS_PHASE { script: // The same commands as scripts/42-traits.sh """ - traits_report.py --rules ${rules} --genotypes ${genotypes} --sample ${meta.id} --phase-sites phase_sites.vcf + og traits phase-sites --rules ${rules} --genotypes ${genotypes} --sample ${meta.id} --out phase_sites.vcf if grep -qv '^#' phase_sites.vcf; then whatshap phase --reference ${reference} --ignore-read-groups -o ${meta.id}_traits_phased.vcf phase_sites.vcf ${bam} else @@ -129,7 +129,7 @@ process TRAITS { script: def phased_arg = phased ? "--phased ${phased}" : '' """ - traits_report.py --table ${table} --rules ${rules} --genotypes ${genotypes} ${phased_arg} --sample ${meta.id} \\ + og traits table --table ${table} --rules ${rules} --genotypes ${genotypes} ${phased_arg} --sample ${meta.id} \\ --out ${meta.id}_traits.tsv --rules-out ${meta.id}_genotype_rules.tsv printf '"%s":\\n python: %s\\n' "${task.process}" "${task.container.replaceFirst(/^[^:@]+[:@]/, '')}" > versions.yml diff --git a/rust/README.md b/rust/README.md index 394e83e10..505ff4fa4 100644 --- a/rust/README.md +++ b/rust/README.md @@ -34,3 +34,7 @@ cargo build --release && ../tests/rust/pycompat-diff.sh target/release/og # py ``` A binary from a plain `cargo build` is stamped `unknown`, and `rust/og.sh check` refuses it: only `scripts/setup.sh --og` stamps the tree hash the pipeline checks. + +`og traits table`, `og traits phase-sites` and `og traits check` read step 42's +curated catalogs and synthetic or bcftools genotype input. Their tests live +in `og-steps::traits` and the `traits` parity cases. diff --git a/rust/crates/og-steps/src/traits.rs b/rust/crates/og-steps/src/traits.rs index 145e69151..40c3db950 100644 --- a/rust/crates/og-steps/src/traits.rs +++ b/rust/crates/og-steps/src/traits.rs @@ -378,23 +378,21 @@ fn check_rules(rules: &[Rule], rows: Option<&[Site]>) -> Vec { } if let Some(t) = rows.and_then(|rows| rows.iter().rev().find(|t| t.row["rsid"] == s.row["rsid"])) - { - if ["chrom", "pos", "ref", "alt"] + && ["chrom", "pos", "ref", "alt"] .iter() .any(|k| t.row[*k] != s.row[*k]) - { - e.push(format!( - "{sw}: the traits table has {}:{} {}>{}, the rule {}:{} {}>{}", - t.row["chrom"], - t.row["pos"], - t.row["ref"], - t.row["alt"], - s.row["chrom"], - s.row["pos"], - s.row["ref"], - s.row["alt"] - )); - } + { + e.push(format!( + "{sw}: the traits table has {}:{} {}>{}, the rule {}:{} {}>{}", + t.row["chrom"], + t.row["pos"], + t.row["ref"], + t.row["alt"], + s.row["chrom"], + s.row["pos"], + s.row["ref"], + s.row["alt"] + )); } } if r.sites @@ -650,10 +648,10 @@ fn read_phased(path: &str, wanted: &HashSet) -> Result { }) }) .collect(); - if let Some(al) = idx { - if al.len() == 2 { - out.insert(key, (al[0].into(), al[1].into(), ps.to_string())); - } + if let Some(al) = idx + && al.len() == 2 + { + out.insert(key, (al[0].into(), al[1].into(), ps.to_string())); } } Ok(out) diff --git a/scripts/42-traits.sh b/scripts/42-traits.sh index 8994e0273..ea3023328 100755 --- a/scripts/42-traits.sh +++ b/scripts/42-traits.sh @@ -93,7 +93,7 @@ if [ -f "$BAM" ] && [ -f "${BAM}.bai" ]; then # shellcheck disable=SC2016 # $1 to $6 belong to the inner bash run_in -v "${PGP_ROOT}/bin:/pgp-bin:ro" -v "${PGP_ROOT}/assets:/pgp-assets:ro" --cpus 1 --memory 2g "$WHATSHAP_IMAGE" \ bash -euo pipefail -c ' - python3 /pgp-bin/traits_report.py --rules /pgp-assets/genotype_rules.tsv --genotypes "$1" --sample "$2" --phase-sites "$3" + /pgp-bin/og traits phase-sites --rules /pgp-assets/genotype_rules.tsv --genotypes "$1" --sample "$2" --out "$3" if grep -qv "^#" "$3"; then whatshap phase --reference "$4" --ignore-read-groups -o "$5" "$3" "$6" else @@ -118,7 +118,7 @@ fi echo "[3/3] Reading each genotype and each rule against the tables..." run_in -v "${PGP_ROOT}/bin:/pgp-bin:ro" -v "${PGP_ROOT}/assets:/pgp-assets:ro" --cpus 1 --memory 1g "$PYTHON_IMAGE" \ - python3 /pgp-bin/traits_report.py --table /pgp-assets/traits.tsv --rules /pgp-assets/genotype_rules.tsv \ + /pgp-bin/og traits table --table /pgp-assets/traits.tsv --rules /pgp-assets/genotype_rules.tsv \ --genotypes "$(cpath "$GENOTYPES")" ${PHASE_ARGS[@]+"${PHASE_ARGS[@]}"} --sample "$SAMPLE" \ --out "$(cpath "$OUT")" --rules-out "$(cpath "$RULES_OUT")" rm -f "$PHASED" diff --git a/scripts/ci/e2e-case-paths.tsv b/scripts/ci/e2e-case-paths.tsv index f76068dcf..fc0a5239c 100644 --- a/scripts/ci/e2e-case-paths.tsv +++ b/scripts/ci/e2e-case-paths.tsv @@ -73,22 +73,22 @@ clinical-filter-mane-and-quality-3-modules assets/construct_variants.tsv assets/ clinvar-clip-check-1 assets/construct_variants.tsv assets/deep_intronic_sites.tsv bin/clip_check.py bin/og bin/render_report.py scripts/06-clinvar-screen.sh scripts/ci/fake-docker/lib.sh clinvar-combined-allele-1 assets/construct_variants.tsv assets/deep_intronic_sites.tsv assets/stub/sample.bam assets/stub/sample.bam.bai bin/clip_check.py bin/og modules/local/clinvar_screen/* modules/local/vcf_precheck/* scripts/06-clinvar-screen.sh scripts/24-html-report.sh scripts/ci/fake-docker/lib.sh scripts/generate-report.sh scripts/validate-setup.sh tests/fixtures/vcf/combined_allele_clinvar.vcf tests/fixtures/vcf/combined_allele_sample.vcf clinvar-cpsr-quality-1 assets/construct_variants.tsv assets/deep_intronic_sites.tsv bin/clip_check.py bin/og bin/render_report.py scripts/06-clinvar-screen.sh scripts/ci/fake-docker/lib.sh -cn-engine-1-parity assets/bloodtype_markers.tsv bin/cn_loci.tsv bin/copy_number.py bin/og bin/traits_report.py scripts/41-bloodtype.sh scripts/46-lpa.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh -cn-guard-1-mapq0 bin/cn_loci.tsv bin/copy_number.py bin/og bin/traits_report.py modules/local/parascopy/* scripts/35-paralogs.sh +cn-engine-1-parity assets/bloodtype_markers.tsv bin/cn_loci.tsv bin/og bin/traits_report.py scripts/41-bloodtype.sh scripts/46-lpa.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh +cn-guard-1-mapq0 bin/cn_loci.tsv bin/og bin/traits_report.py modules/local/parascopy/* scripts/35-paralogs.sh cn-intervals-1-catalog bin/cn_loci.tsv bin/og scripts/ci/build-cn-intervals.py scripts/ci/fake-docker/lib.sh -cn-intervals-2-hg002 assets/par_grch38.bed assets/paralog_loci.tsv bin/cn_loci.tsv bin/copy_number.py bin/og bin/traits_report.py modules/local/deepvariant/* modules/local/parascopy/* modules/local/vcf_precheck/* scripts/35-paralogs.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh +cn-intervals-2-hg002 assets/par_grch38.bed assets/paralog_loci.tsv bin/cn_loci.tsv bin/og bin/traits_report.py modules/local/deepvariant/* modules/local/parascopy/* modules/local/vcf_precheck/* scripts/35-paralogs.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh cn-report-1-tables assets/carrier_genes.tsv assets/par_grch38.bed bin/collect_summary.py bin/couple_report.py bin/family_report.py bin/og bin/paraphase_report.py bin/render_report.py bin/results_index.py bin/traits_report.py scripts/24-html-report.sh scripts/39-couple.sh scripts/ci/fake-docker/lib.sh scripts/generate-report.sh comphet-phasing-1-reads assets/construct_variants.tsv assets/stub/sample.bam assets/stub/sample.bam.bai bin/clip_check.py bin/og modules/local/slivar/* modules/local/vcf_precheck/* modules/local/vep/* scripts/13-vep-annotation.sh scripts/31-slivar.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh tests/fixtures/comphet/make_fixture.py construct-scan-1-both assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/construct_variants.tsv assets/deep_intronic_sites.tsv assets/mito_confirmed.tsv assets/stub/sample.bam assets/stub/sample.bam.bai assets/stub/sample.vcf.gz bin/clip_check.py bin/collect_summary.py bin/og bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/clinvar_screen/* modules/local/html_report/* modules/local/vcf_precheck/* scripts/06-clinvar-screen.sh scripts/24-html-report.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh consumer-export-1 assets/chip_genes.tsv assets/par_grch38.bed bin/chip_report.py bin/og modules/local/chip/* modules/local/export_consumer/* modules/local/vcf_precheck/* scripts/40-export-consumer.sh scripts/49-chip.sh scripts/chip-to-vcf.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh tests/fixtures/consumer_export/sites.tsv.gz containers-software-versions scripts/ci/fake-docker/lib.sh scripts/ci/gen-containers-config.sh -couple-carrier-1 assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/construct_variants.tsv assets/deep_intronic_sites.tsv assets/families.example.tsv assets/mito_confirmed.tsv assets/par_grch38.bed assets/paralog_loci.tsv assets/pedigree.example.tsv assets/pgx_genes.tsv assets/str_extra_loci.json assets/stub/db/catalog.json assets/stub/db/hla.dat assets/stub/db/kir.dat assets/stub/pedigree.tsv assets/stub/sample.bam assets/stub/sample.bam.bai assets/stub/sample.vcf.gz bin/clip_check.py bin/cn_loci.tsv bin/collect_summary.py bin/copy_number.py bin/couple_report.py bin/family_report.py bin/og bin/pgx_outside_calls.py bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/clinvar_screen/* modules/local/couple/* modules/local/delly/* modules/local/expansion_hunter/* modules/local/hla_typing/* modules/local/html_report/* modules/local/parascopy/* modules/local/somalier/* modules/local/t1k_build/* modules/local/vcf_precheck/* scripts/06-clinvar-screen.sh scripts/08-hla-typing.sh scripts/09-expansion-hunter.sh scripts/19-delly.sh scripts/24-html-report.sh scripts/32-pypgx.sh scripts/33-sample-qc.sh scripts/35-paralogs.sh scripts/36-pgx-consensus.sh scripts/38-family.sh scripts/39-couple.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh -couple-common-sv-1 assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/construct_variants.tsv assets/deep_intronic_sites.tsv assets/families.example.tsv assets/mito_confirmed.tsv assets/par_grch38.bed assets/pedigree.example.tsv assets/stub/pedigree.tsv assets/stub/sample.bam assets/stub/sample.bam.bai assets/stub/sample.vcf.gz bin/clip_check.py bin/cn_loci.tsv bin/collect_summary.py bin/copy_number.py bin/couple_report.py bin/family_report.py bin/og bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/clinvar_screen/* modules/local/couple/* modules/local/delly/* modules/local/html_report/* modules/local/vcf_precheck/* scripts/06-clinvar-screen.sh scripts/19-delly.sh scripts/24-html-report.sh scripts/38-family.sh scripts/39-couple.sh scripts/ci/fake-docker/lib.sh scripts/setup.sh scripts/validate-setup.sh -cpic-phenoconversion-1-medications assets/pgx_contraindications.tsv assets/pgx_genes.tsv assets/pgx_inhibitors.tsv bin/cn_loci.tsv bin/collect_summary.py bin/copy_number.py bin/og bin/pgx_outside_calls.py bin/traits_report.py modules/local/cpic_lookup/* modules/local/pharmcat/* modules/local/vcf_precheck/* scripts/07-pharmacogenomics.sh scripts/27-cpic-lookup.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh -cyp21a2-short-reads-1-hg002 bin/cn_loci.tsv bin/copy_number.py bin/og bin/traits_report.py modules/local/parascopy/* scripts/35-paralogs.sh -exomiser-1 assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/families.example.tsv assets/mito_confirmed.tsv assets/par_grch38.bed assets/pedigree.example.tsv assets/pgx_genes.tsv assets/stub/db/hla.dat assets/stub/db/kir.dat assets/stub/pedigree.tsv assets/stub/sample.bam assets/stub/sample.bam.bai assets/stub/sample.vcf.gz assets/stub/sample.vcf.gz.tbi bin/cn_loci.tsv bin/collect_summary.py bin/copy_number.py bin/family_report.py bin/og bin/pgx_outside_calls.py bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/exomiser/* modules/local/hla_typing/* modules/local/html_report/* modules/local/t1k_build/* modules/local/vcf_precheck/* scripts/08-hla-typing.sh scripts/24-html-report.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/38-family.sh scripts/48-exomiser.sh scripts/ci/fake-docker/lib.sh scripts/setup.sh scripts/validate-setup.sh +couple-carrier-1 assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/construct_variants.tsv assets/deep_intronic_sites.tsv assets/families.example.tsv assets/mito_confirmed.tsv assets/par_grch38.bed assets/paralog_loci.tsv assets/pedigree.example.tsv assets/pgx_genes.tsv assets/str_extra_loci.json assets/stub/db/catalog.json assets/stub/db/hla.dat assets/stub/db/kir.dat assets/stub/pedigree.tsv assets/stub/sample.bam assets/stub/sample.bam.bai assets/stub/sample.vcf.gz bin/clip_check.py bin/cn_loci.tsv bin/collect_summary.py bin/couple_report.py bin/family_report.py bin/og bin/pgx_outside_calls.py bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/clinvar_screen/* modules/local/couple/* modules/local/delly/* modules/local/expansion_hunter/* modules/local/hla_typing/* modules/local/html_report/* modules/local/parascopy/* modules/local/somalier/* modules/local/t1k_build/* modules/local/vcf_precheck/* scripts/06-clinvar-screen.sh scripts/08-hla-typing.sh scripts/09-expansion-hunter.sh scripts/19-delly.sh scripts/24-html-report.sh scripts/32-pypgx.sh scripts/33-sample-qc.sh scripts/35-paralogs.sh scripts/36-pgx-consensus.sh scripts/38-family.sh scripts/39-couple.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh +couple-common-sv-1 assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/construct_variants.tsv assets/deep_intronic_sites.tsv assets/families.example.tsv assets/mito_confirmed.tsv assets/par_grch38.bed assets/pedigree.example.tsv assets/stub/pedigree.tsv assets/stub/sample.bam assets/stub/sample.bam.bai assets/stub/sample.vcf.gz bin/clip_check.py bin/cn_loci.tsv bin/collect_summary.py bin/couple_report.py bin/family_report.py bin/og bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/clinvar_screen/* modules/local/couple/* modules/local/delly/* modules/local/html_report/* modules/local/vcf_precheck/* scripts/06-clinvar-screen.sh scripts/19-delly.sh scripts/24-html-report.sh scripts/38-family.sh scripts/39-couple.sh scripts/ci/fake-docker/lib.sh scripts/setup.sh scripts/validate-setup.sh +cpic-phenoconversion-1-medications assets/pgx_contraindications.tsv assets/pgx_genes.tsv assets/pgx_inhibitors.tsv bin/cn_loci.tsv bin/collect_summary.py bin/og bin/pgx_outside_calls.py bin/traits_report.py modules/local/cpic_lookup/* modules/local/pharmcat/* modules/local/vcf_precheck/* scripts/07-pharmacogenomics.sh scripts/27-cpic-lookup.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh +cyp21a2-short-reads-1-hg002 bin/cn_loci.tsv bin/og bin/traits_report.py modules/local/parascopy/* scripts/35-paralogs.sh +exomiser-1 assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/families.example.tsv assets/mito_confirmed.tsv assets/par_grch38.bed assets/pedigree.example.tsv assets/pgx_genes.tsv assets/stub/db/hla.dat assets/stub/db/kir.dat assets/stub/pedigree.tsv assets/stub/sample.bam assets/stub/sample.bam.bai assets/stub/sample.vcf.gz assets/stub/sample.vcf.gz.tbi bin/cn_loci.tsv bin/collect_summary.py bin/family_report.py bin/og bin/pgx_outside_calls.py bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/exomiser/* modules/local/hla_typing/* modules/local/html_report/* modules/local/t1k_build/* modules/local/vcf_precheck/* scripts/08-hla-typing.sh scripts/24-html-report.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/38-family.sh scripts/48-exomiser.sh scripts/ci/fake-docker/lib.sh scripts/setup.sh scripts/validate-setup.sh expansions-and-rfc1-1-motif assets/str_extra_loci.json assets/stub/db/catalog.json bin/og scripts/09-expansion-hunter.sh scripts/09b-stranger.sh scripts/ci/fake-docker/lib.sh scripts/generate-report.sh -families-file-1 assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/construct_variants.tsv assets/deep_intronic_sites.tsv assets/families.example.tsv assets/mito_confirmed.tsv assets/par_grch38.bed assets/pedigree.example.tsv assets/pgx_genes.tsv assets/stub/db/hla.dat assets/stub/db/kir.dat assets/stub/pedigree.tsv assets/stub/sample.bam assets/stub/sample.bam.bai assets/stub/sample.vcf.gz bin/clip_check.py bin/cn_loci.tsv bin/collect_summary.py bin/copy_number.py bin/couple_report.py bin/family_report.py bin/og bin/pgx_outside_calls.py bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/align_minimap2/* modules/local/clinvar_screen/* modules/local/couple/* modules/local/deepvariant/* modules/local/fastp/* modules/local/hla_typing/* modules/local/html_report/* modules/local/t1k_build/* modules/local/vcf_precheck/* scripts/01b-fastp-qc.sh scripts/06-clinvar-screen.sh scripts/08-hla-typing.sh scripts/24-html-report.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/38-family.sh scripts/39-couple.sh scripts/ci/check-images.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh tests/fixtures/families/stub_families.tsv +families-file-1 assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/construct_variants.tsv assets/deep_intronic_sites.tsv assets/families.example.tsv assets/mito_confirmed.tsv assets/par_grch38.bed assets/pedigree.example.tsv assets/pgx_genes.tsv assets/stub/db/hla.dat assets/stub/db/kir.dat assets/stub/pedigree.tsv assets/stub/sample.bam assets/stub/sample.bam.bai assets/stub/sample.vcf.gz bin/clip_check.py bin/cn_loci.tsv bin/collect_summary.py bin/couple_report.py bin/family_report.py bin/og bin/pgx_outside_calls.py bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/align_minimap2/* modules/local/clinvar_screen/* modules/local/couple/* modules/local/deepvariant/* modules/local/fastp/* modules/local/hla_typing/* modules/local/html_report/* modules/local/t1k_build/* modules/local/vcf_precheck/* scripts/01b-fastp-qc.sh scripts/06-clinvar-screen.sh scripts/08-hla-typing.sh scripts/24-html-report.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/38-family.sh scripts/39-couple.sh scripts/ci/check-images.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh tests/fixtures/families/stub_families.tsv family-joint-1 assets/construct_variants.tsv assets/deep_intronic_sites.tsv assets/families.example.tsv assets/par_grch38.bed assets/pedigree.example.tsv assets/stub/pedigree.tsv assets/stub/sample.bam assets/stub/sample.bam.bai bin/clip_check.py bin/collect_summary.py bin/family_report.py bin/og bin/traits_report.py modules/local/align_minimap2/* modules/local/clinvar_screen/* modules/local/deepvariant/* modules/local/fastp/* modules/local/roh/* modules/local/somalier/* modules/local/vcf_precheck/* scripts/01b-fastp-qc.sh scripts/06-clinvar-screen.sh scripts/11-roh-analysis.sh scripts/33-sample-qc.sh scripts/38-family.sh scripts/ci/check-images.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh fixitfelix-chr21-1 assets/callability_genes.tsv scripts/16b-mosdepth.sh scripts/ci/fake-docker/lib.sh scripts/setup.sh followups-ci-3-sv-rule-regions assets/sv_rule_regions.tsv scripts/ci/build-sv-rule-regions.sh scripts/ci/fake-docker/lib.sh @@ -99,40 +99,40 @@ genome-lookup-1 bin/og scripts/ci/fake-docker/lib.sh scripts/owngenome-lookup.sh genotype-rule-rows-1-str-extra assets/str_extra_loci.json assets/stub/db/catalog.json scripts/09-expansion-hunter.sh scripts/ci/fake-docker/lib.sh scripts/setup.sh genotype-rules-1-phase assets/genotype_rules.tsv assets/traits.tsv bin/traits_report.py modules/local/traits/* modules/local/vcf_precheck/* scripts/42-traits.sh scripts/ci/check-rule-alleles.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh genotype-rules-2-g2 assets/genotype_rules.tsv bin/collect_summary.py bin/genotype_rules_indels.py bin/traits_report.py -genotype-rules-2-lpa-null bin/cn_loci.tsv bin/collect_summary.py bin/copy_number.py bin/og bin/render_report.py bin/traits_report.py modules/local/lpa/* scripts/24-html-report.sh scripts/46-lpa.sh scripts/generate-report.sh -genotype-rules-2-smn1-marker bin/collect_summary.py bin/copy_number.py bin/og bin/render_report.py bin/traits_report.py modules/local/parascopy/* scripts/24-html-report.sh scripts/35-paralogs.sh scripts/generate-report.sh +genotype-rules-2-lpa-null bin/cn_loci.tsv bin/collect_summary.py bin/og bin/render_report.py bin/traits_report.py modules/local/lpa/* scripts/24-html-report.sh scripts/46-lpa.sh scripts/generate-report.sh +genotype-rules-2-smn1-marker bin/collect_summary.py bin/og bin/render_report.py bin/traits_report.py modules/local/parascopy/* scripts/24-html-report.sh scripts/35-paralogs.sh scripts/generate-report.sh haplogroup-explore-1-files assets/callability_genes.tsv assets/live/index.html assets/mito_confirmed.tsv assets/viewer/index.html bin/collect_summary.py bin/og bin/traits_report.py bin/yleaf_run.py modules/local/indexcov/* modules/local/mito_haplogroup/* modules/local/mito_variants/* modules/local/vcf_precheck/* modules/local/y_haplogroup/* scripts/12-mito-haplogroup.sh scripts/16-indexcov.sh scripts/16b-mosdepth.sh scripts/20-mtoolbox.sh scripts/37-y-haplogroup.sh scripts/ci/fake-docker/lib.sh scripts/generate-report.sh scripts/setup.sh scripts/validate-setup.sh -hba-short-read-accuracy-1-nf-report assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/mito_confirmed.tsv assets/par_grch38.bed assets/stub/sample.vcf.gz bin/cn_loci.tsv bin/collect_summary.py bin/copy_number.py bin/og bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/export_consumer/* modules/local/html_report/* modules/local/vcf_precheck/* scripts/24-html-report.sh scripts/40-export-consumer.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh tests/fixtures/consumer_export/sites.tsv.gz -hla-second-caller-1-bash assets/pgx_genes.tsv assets/stub/db/hla.dat assets/stub/db/kir.dat bin/cn_loci.tsv bin/copy_number.py bin/og bin/pgx_outside_calls.py bin/render_report.py scripts/08-hla-typing.sh scripts/24-html-report.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh -hla-second-caller-2-nextflow assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/mito_confirmed.tsv assets/pgx_genes.tsv assets/stub/db/hla.dat assets/stub/db/kir.dat assets/stub/sample.vcf.gz bin/cn_loci.tsv bin/collect_summary.py bin/copy_number.py bin/og bin/pgx_outside_calls.py bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/hla_typing/* modules/local/html_report/* modules/local/pgx_consensus/* modules/local/pharmcat/* modules/local/t1k_build/* modules/local/vcf_precheck/* scripts/07-pharmacogenomics.sh scripts/08-hla-typing.sh scripts/21-cyrius.sh scripts/24-html-report.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/ci/fake-docker/lib.sh scripts/cyrius-constraints.txt scripts/setup.sh scripts/validate-setup.sh +hba-short-read-accuracy-1-nf-report assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/mito_confirmed.tsv assets/par_grch38.bed assets/stub/sample.vcf.gz bin/cn_loci.tsv bin/collect_summary.py bin/og bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/export_consumer/* modules/local/html_report/* modules/local/vcf_precheck/* scripts/24-html-report.sh scripts/40-export-consumer.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh tests/fixtures/consumer_export/sites.tsv.gz +hla-second-caller-1-bash assets/pgx_genes.tsv assets/stub/db/hla.dat assets/stub/db/kir.dat bin/cn_loci.tsv bin/og bin/pgx_outside_calls.py bin/render_report.py scripts/08-hla-typing.sh scripts/24-html-report.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh +hla-second-caller-2-nextflow assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/mito_confirmed.tsv assets/pgx_genes.tsv assets/stub/db/hla.dat assets/stub/db/kir.dat assets/stub/sample.vcf.gz bin/cn_loci.tsv bin/collect_summary.py bin/og bin/pgx_outside_calls.py bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/hla_typing/* modules/local/html_report/* modules/local/pgx_consensus/* modules/local/pharmcat/* modules/local/t1k_build/* modules/local/vcf_precheck/* scripts/07-pharmacogenomics.sh scripts/08-hla-typing.sh scripts/21-cyrius.sh scripts/24-html-report.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/ci/fake-docker/lib.sh scripts/cyrius-constraints.txt scripts/setup.sh scripts/validate-setup.sh ibd-segments-1 assets/families.example.tsv assets/par_grch38.bed assets/pedigree.example.tsv assets/stub/pedigree.tsv bin/family_report.py bin/og scripts/38-family.sh scripts/ci/fake-docker/lib.sh scripts/setup.sh live-server-1-weblog assets/live/* assets/viewer/index.html bin/live_results.py bin/owngenome_live.py bin/owngenome_live_results.py bin/render_report.py bin/run_state.py modules/local/roh/* modules/local/vcf_precheck/* scripts/11-roh-analysis.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh -lpa-1 assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/mito_confirmed.tsv assets/par_grch38.bed assets/stub/sample.vcf.gz bin/cn_loci.tsv bin/collect_summary.py bin/copy_number.py bin/og bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/deepvariant/* modules/local/html_report/* modules/local/lpa/* modules/local/vcf_precheck/* scripts/24-html-report.sh scripts/46-lpa.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh +lpa-1 assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/mito_confirmed.tsv assets/par_grch38.bed assets/stub/sample.vcf.gz bin/cn_loci.tsv bin/collect_summary.py bin/og bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/deepvariant/* modules/local/html_report/* modules/local/lpa/* modules/local/vcf_precheck/* scripts/24-html-report.sh scripts/46-lpa.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh mei-1 assets/mei_sites.tsv assets/par_grch38.bed assets/sv_rule_regions.tsv bin/annotsv_postfilter.py bin/cnvpytor_to_vcf.py bin/collect_summary.py bin/family_report.py bin/og bin/traits_report.py modules/local/align_minimap2/* modules/local/annotsv/* modules/local/deepvariant/* modules/local/fastp/* modules/local/mei/* modules/local/vcf_precheck/* scripts/01b-fastp-qc.sh scripts/05-annotsv.sh scripts/15-duphold.sh scripts/18-cnvpytor.sh scripts/19-delly.sh scripts/50-mei.sh scripts/ci/check-images.sh scripts/ci/fake-docker/lib.sh scripts/setup.sh scripts/validate-setup.sh tests/fixtures/annotsv/class45.sh mito-extended-1-deletions assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/construct_variants.tsv assets/deep_intronic_sites.tsv assets/mito_confirmed.tsv assets/stub/db/clinvar.vcf.gz assets/stub/db/clinvar.vcf.gz.tbi assets/stub/sample.bam assets/stub/sample.bam.bai assets/stub/sample.vcf.gz bin/clip_check.py bin/collect_summary.py bin/og bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/clinvar_screen/* modules/local/html_report/* modules/local/mito_variants/* modules/local/vcf_precheck/* scripts/06-clinvar-screen.sh scripts/16b-mosdepth.sh scripts/20-mtoolbox.sh scripts/24-html-report.sh scripts/ci/build-mito-confirmed.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh mocha-1 assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/mito_confirmed.tsv assets/stub/sample.vcf.gz bin/collect_summary.py bin/og bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/html_report/* modules/local/mocha/* modules/local/vcf_precheck/* scripts/24-html-report.sh scripts/51-mocha.sh scripts/ci/fake-docker/lib.sh scripts/setup.sh scripts/validate-setup.sh mosaic-loy-lox-1 assets/callability_genes.tsv bin/collect_summary.py bin/og bin/traits_report.py scripts/16b-mosdepth.sh scripts/33-sample-qc.sh scripts/ci/fake-docker/lib.sh scripts/setup.sh -nextflow-from-fastq-1-bash assets/callability_genes.tsv assets/construct_variants.tsv assets/deep_intronic_sites.tsv assets/pgs_scores.tsv assets/pgx_genes.tsv assets/stub/db/hla.dat assets/stub/db/kir.dat bin/clip_check.py bin/cn_loci.tsv bin/collect_summary.py bin/copy_number.py bin/og bin/pgx_outside_calls.py bin/traits_report.py scripts/01b-fastp-qc.sh scripts/06-clinvar-screen.sh scripts/07-pharmacogenomics.sh scripts/08-hla-typing.sh scripts/11-roh-analysis.sh scripts/16b-mosdepth.sh scripts/20-mtoolbox.sh scripts/25-prs.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/ci/fake-docker/lib.sh scripts/ci/parity-diff.sh scripts/validate-setup.sh -nextflow-from-fastq-2-nextflow assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/construct_variants.tsv assets/deep_intronic_sites.tsv assets/live/index.html assets/mito_confirmed.tsv assets/par_grch38.bed assets/pgs_scores.tsv assets/pgx_contraindications.tsv assets/pgx_genes.tsv assets/pgx_inhibitors.tsv assets/stub/db/hla.dat assets/stub/db/kir.dat assets/stub/sample.bam assets/stub/sample.bam.bai assets/stub/sample.vcf.gz assets/viewer/index.html bin/clip_check.py bin/cn_loci.tsv bin/collect_summary.py bin/copy_number.py bin/og bin/pgx_outside_calls.py bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/align_minimap2/* modules/local/clinvar_screen/* modules/local/cpic_lookup/* modules/local/cyrius/* modules/local/deepvariant/* modules/local/fastp/* modules/local/hla_typing/* modules/local/html_report/* modules/local/indexcov/* modules/local/mito_haplogroup/* modules/local/mito_variants/* modules/local/mosdepth/* modules/local/multiqc/* modules/local/pharmcat/* modules/local/prs/* modules/local/roh/* modules/local/t1k_build/* modules/local/telomere_hunter/* modules/local/vcf_precheck/* modules/local/vcfanno/* scripts/01b-fastp-qc.sh scripts/06-clinvar-screen.sh scripts/07-pharmacogenomics.sh scripts/08-hla-typing.sh scripts/10-telomere-hunter.sh scripts/11-roh-analysis.sh scripts/12-mito-haplogroup.sh scripts/16-indexcov.sh scripts/16b-mosdepth.sh scripts/20-mtoolbox.sh scripts/21-cyrius.sh scripts/24-html-report.sh scripts/25-prs.sh scripts/26-ancestry.sh scripts/27-cpic-lookup.sh scripts/28-multiqc.sh scripts/30-vcfanno.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/ci/check-images.sh scripts/ci/fake-docker/lib.sh scripts/cyrius-constraints.txt scripts/validate-setup.sh +nextflow-from-fastq-1-bash assets/callability_genes.tsv assets/construct_variants.tsv assets/deep_intronic_sites.tsv assets/pgs_scores.tsv assets/pgx_genes.tsv assets/stub/db/hla.dat assets/stub/db/kir.dat bin/clip_check.py bin/cn_loci.tsv bin/collect_summary.py bin/og bin/pgx_outside_calls.py bin/traits_report.py scripts/01b-fastp-qc.sh scripts/06-clinvar-screen.sh scripts/07-pharmacogenomics.sh scripts/08-hla-typing.sh scripts/11-roh-analysis.sh scripts/16b-mosdepth.sh scripts/20-mtoolbox.sh scripts/25-prs.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/ci/fake-docker/lib.sh scripts/ci/parity-diff.sh scripts/validate-setup.sh +nextflow-from-fastq-2-nextflow assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/construct_variants.tsv assets/deep_intronic_sites.tsv assets/live/index.html assets/mito_confirmed.tsv assets/par_grch38.bed assets/pgs_scores.tsv assets/pgx_contraindications.tsv assets/pgx_genes.tsv assets/pgx_inhibitors.tsv assets/stub/db/hla.dat assets/stub/db/kir.dat assets/stub/sample.bam assets/stub/sample.bam.bai assets/stub/sample.vcf.gz assets/viewer/index.html bin/clip_check.py bin/cn_loci.tsv bin/collect_summary.py bin/og bin/pgx_outside_calls.py bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/align_minimap2/* modules/local/clinvar_screen/* modules/local/cpic_lookup/* modules/local/cyrius/* modules/local/deepvariant/* modules/local/fastp/* modules/local/hla_typing/* modules/local/html_report/* modules/local/indexcov/* modules/local/mito_haplogroup/* modules/local/mito_variants/* modules/local/mosdepth/* modules/local/multiqc/* modules/local/pharmcat/* modules/local/prs/* modules/local/roh/* modules/local/t1k_build/* modules/local/telomere_hunter/* modules/local/vcf_precheck/* modules/local/vcfanno/* scripts/01b-fastp-qc.sh scripts/06-clinvar-screen.sh scripts/07-pharmacogenomics.sh scripts/08-hla-typing.sh scripts/10-telomere-hunter.sh scripts/11-roh-analysis.sh scripts/12-mito-haplogroup.sh scripts/16-indexcov.sh scripts/16b-mosdepth.sh scripts/20-mtoolbox.sh scripts/21-cyrius.sh scripts/24-html-report.sh scripts/25-prs.sh scripts/26-ancestry.sh scripts/27-cpic-lookup.sh scripts/28-multiqc.sh scripts/30-vcfanno.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/ci/check-images.sh scripts/ci/fake-docker/lib.sh scripts/cyrius-constraints.txt scripts/validate-setup.sh nextflow-from-fastq-3-sexcheck assets/callability_genes.tsv assets/live/index.html assets/viewer/index.html bin/collect_summary.py bin/traits_report.py modules/local/indexcov/* modules/local/mosdepth/* modules/local/vcf_precheck/* scripts/16-indexcov.sh scripts/16b-mosdepth.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh nextflow-hardening-1-manta assets/par_grch38.bed assets/sv_rule_regions.tsv bin/annotsv_postfilter.py bin/cnvpytor_to_vcf.py bin/collect_summary.py bin/family_report.py bin/og bin/traits_report.py scripts/04-manta.sh scripts/05-annotsv.sh scripts/15-duphold.sh scripts/18-cnvpytor.sh scripts/19-delly.sh scripts/ci/fake-docker/lib.sh -nextflow-hardening-2-nf assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/construct_variants.tsv assets/deep_intronic_sites.tsv assets/live/index.html assets/mito_confirmed.tsv assets/par_grch38.bed assets/pgx_genes.tsv assets/stub/sample.bam assets/stub/sample.bam.bai assets/stub/sample.vcf.gz assets/sv_rule_regions.tsv assets/viewer/index.html bin/annotsv_postfilter.py bin/clip_check.py bin/cn_loci.tsv bin/cnvpytor_to_vcf.py bin/collect_summary.py bin/copy_number.py bin/family_report.py bin/og bin/pgx_outside_calls.py bin/render_report.py bin/results_index.py bin/traits_report.py bin/yleaf_run.py modules/local/clinvar_screen/* modules/local/delly/* modules/local/html_report/* modules/local/indexcov/* modules/local/manta/* modules/local/mito_haplogroup/* modules/local/mosdepth/* modules/local/pgx_consensus/* modules/local/pharmcat/* modules/local/pypgx/* modules/local/survivor_merge/* modules/local/telomere_hunter/* modules/local/vcf_precheck/* modules/local/vcfanno/* modules/local/y_haplogroup/* scripts/04-manta.sh scripts/05-annotsv.sh scripts/06-clinvar-screen.sh scripts/07-pharmacogenomics.sh scripts/10-telomere-hunter.sh scripts/12-mito-haplogroup.sh scripts/15-duphold.sh scripts/16-indexcov.sh scripts/16b-mosdepth.sh scripts/18-cnvpytor.sh scripts/19-delly.sh scripts/20-mtoolbox.sh scripts/21-cyrius.sh scripts/22-survivor-merge.sh scripts/24-html-report.sh scripts/30-vcfanno.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/37-y-haplogroup.sh scripts/ci/fake-docker/lib.sh scripts/ci/parity-diff.sh scripts/cyrius-constraints.txt scripts/setup.sh scripts/validate-setup.sh -nextflow-report-parity-1-sections assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/construct_variants.tsv assets/deep_intronic_sites.tsv assets/mito_confirmed.tsv assets/par_grch38.bed assets/pgx_genes.tsv assets/str_extra_loci.json assets/stub/db/catalog.json assets/stub/sample.bam assets/stub/sample.bam.bai assets/stub/sample.vcf.gz assets/sv_rule_regions.tsv bin/annotsv_postfilter.py bin/clip_check.py bin/cn_loci.tsv bin/cnvpytor_to_vcf.py bin/collect_summary.py bin/copy_number.py bin/family_report.py bin/og bin/pgx_outside_calls.py bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/annotsv/* modules/local/clinvar_screen/* modules/local/expansion_hunter/* modules/local/export_consumer/* modules/local/html_report/* modules/local/mito_variants/* modules/local/pypgx/* modules/local/slivar/* modules/local/stranger/* modules/local/vcf_precheck/* modules/local/vep/* scripts/05-annotsv.sh scripts/06-clinvar-screen.sh scripts/09-expansion-hunter.sh scripts/09b-stranger.sh scripts/13-vep-annotation.sh scripts/15-duphold.sh scripts/16b-mosdepth.sh scripts/18-cnvpytor.sh scripts/19-delly.sh scripts/20-mtoolbox.sh scripts/24-html-report.sh scripts/31-slivar.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/40-export-consumer.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh tests/fixtures/consumer_export/sites.tsv.gz +nextflow-hardening-2-nf assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/construct_variants.tsv assets/deep_intronic_sites.tsv assets/live/index.html assets/mito_confirmed.tsv assets/par_grch38.bed assets/pgx_genes.tsv assets/stub/sample.bam assets/stub/sample.bam.bai assets/stub/sample.vcf.gz assets/sv_rule_regions.tsv assets/viewer/index.html bin/annotsv_postfilter.py bin/clip_check.py bin/cn_loci.tsv bin/cnvpytor_to_vcf.py bin/collect_summary.py bin/family_report.py bin/og bin/pgx_outside_calls.py bin/render_report.py bin/results_index.py bin/traits_report.py bin/yleaf_run.py modules/local/clinvar_screen/* modules/local/delly/* modules/local/html_report/* modules/local/indexcov/* modules/local/manta/* modules/local/mito_haplogroup/* modules/local/mosdepth/* modules/local/pgx_consensus/* modules/local/pharmcat/* modules/local/pypgx/* modules/local/survivor_merge/* modules/local/telomere_hunter/* modules/local/vcf_precheck/* modules/local/vcfanno/* modules/local/y_haplogroup/* scripts/04-manta.sh scripts/05-annotsv.sh scripts/06-clinvar-screen.sh scripts/07-pharmacogenomics.sh scripts/10-telomere-hunter.sh scripts/12-mito-haplogroup.sh scripts/15-duphold.sh scripts/16-indexcov.sh scripts/16b-mosdepth.sh scripts/18-cnvpytor.sh scripts/19-delly.sh scripts/20-mtoolbox.sh scripts/21-cyrius.sh scripts/22-survivor-merge.sh scripts/24-html-report.sh scripts/30-vcfanno.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/37-y-haplogroup.sh scripts/ci/fake-docker/lib.sh scripts/ci/parity-diff.sh scripts/cyrius-constraints.txt scripts/setup.sh scripts/validate-setup.sh +nextflow-report-parity-1-sections assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/construct_variants.tsv assets/deep_intronic_sites.tsv assets/mito_confirmed.tsv assets/par_grch38.bed assets/pgx_genes.tsv assets/str_extra_loci.json assets/stub/db/catalog.json assets/stub/sample.bam assets/stub/sample.bam.bai assets/stub/sample.vcf.gz assets/sv_rule_regions.tsv bin/annotsv_postfilter.py bin/clip_check.py bin/cn_loci.tsv bin/cnvpytor_to_vcf.py bin/collect_summary.py bin/family_report.py bin/og bin/pgx_outside_calls.py bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/annotsv/* modules/local/clinvar_screen/* modules/local/expansion_hunter/* modules/local/export_consumer/* modules/local/html_report/* modules/local/mito_variants/* modules/local/pypgx/* modules/local/slivar/* modules/local/stranger/* modules/local/vcf_precheck/* modules/local/vep/* scripts/05-annotsv.sh scripts/06-clinvar-screen.sh scripts/09-expansion-hunter.sh scripts/09b-stranger.sh scripts/13-vep-annotation.sh scripts/15-duphold.sh scripts/16b-mosdepth.sh scripts/18-cnvpytor.sh scripts/19-delly.sh scripts/20-mtoolbox.sh scripts/24-html-report.sh scripts/31-slivar.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/40-export-consumer.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh tests/fixtures/consumer_export/sites.tsv.gz nf-wiring-1-report-tables assets/acmg_sf.tsv assets/test_samplesheet.csv assets/stub/* bin/clip_check.py bin/collect_summary.py bin/cpsr_sample_id bin/og bin/render_report.py bin/results_index.py bin/traits_report.py conf/test_all.config conf/test.config modules/local/align_minimap2/* modules/local/clinical_filter/* modules/local/clinvar_screen/* modules/local/cpsr/* modules/local/html_report/* modules/local/slivar/* modules/local/vep/* scripts/24-html-report.sh tests/fixtures/consumer_export/sites.tsv.gz nf-wiring-2-sv-str-tools bin/cnvpytor_to_vcf.py bin/family_report.py bin/og modules/local/cnvpytor/* modules/local/duphold/* modules/local/expansion_hunter/* modules/local/indexcov/* modules/local/manta/* modules/local/vcf_precheck/* output-contract-1-hg002 assets/* bin/* modules/* scripts/* -paralog-catalog-1-gba assets/paralog_loci.tsv assets/parascopy_models_v1.7.txt bin/cn_loci.tsv bin/copy_number.py bin/og bin/traits_report.py modules/local/parascopy/* modules/local/vcf_precheck/* scripts/35-paralogs.sh scripts/ci/fake-docker/lib.sh scripts/setup.sh scripts/validate-setup.sh -paralog-checks-and-doc-fixes-1-hba assets/par_grch38.bed assets/paralog_loci.tsv bin/cn_loci.tsv bin/collect_summary.py bin/copy_number.py bin/og bin/paraphase_report.py bin/traits_report.py modules/local/align_minimap2/* modules/local/deepvariant/* modules/local/fastp/* modules/local/paraphase/* modules/local/parascopy/* modules/local/vcf_precheck/* scripts/01b-fastp-qc.sh scripts/35-paralogs.sh scripts/35b-paraphase.sh scripts/ci/check-images.sh scripts/ci/fake-docker/lib.sh scripts/setup.sh scripts/validate-setup.sh -paralog-rows-1 assets/paralog_loci.tsv assets/parascopy_models_v1.7.txt bin/cn_loci.tsv bin/collect_summary.py bin/copy_number.py bin/og bin/paraphase_report.py bin/render_report.py bin/traits_report.py scripts/35-paralogs.sh scripts/ci/fake-docker/lib.sh scripts/setup.sh scripts/validate-setup.sh +paralog-catalog-1-gba assets/paralog_loci.tsv assets/parascopy_models_v1.7.txt bin/cn_loci.tsv bin/og bin/traits_report.py modules/local/parascopy/* modules/local/vcf_precheck/* scripts/35-paralogs.sh scripts/ci/fake-docker/lib.sh scripts/setup.sh scripts/validate-setup.sh +paralog-checks-and-doc-fixes-1-hba assets/par_grch38.bed assets/paralog_loci.tsv bin/cn_loci.tsv bin/collect_summary.py bin/og bin/paraphase_report.py bin/traits_report.py modules/local/align_minimap2/* modules/local/deepvariant/* modules/local/fastp/* modules/local/paraphase/* modules/local/parascopy/* modules/local/vcf_precheck/* scripts/01b-fastp-qc.sh scripts/35-paralogs.sh scripts/35b-paraphase.sh scripts/ci/check-images.sh scripts/ci/fake-docker/lib.sh scripts/setup.sh scripts/validate-setup.sh +paralog-rows-1 assets/paralog_loci.tsv assets/parascopy_models_v1.7.txt bin/cn_loci.tsv bin/collect_summary.py bin/og bin/paraphase_report.py bin/render_report.py bin/traits_report.py scripts/35-paralogs.sh scripts/ci/fake-docker/lib.sh scripts/setup.sh scripts/validate-setup.sh paralogs-paraphase-1 assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/mito_confirmed.tsv assets/par_grch38.bed assets/stub/sample.vcf.gz bin/collect_summary.py bin/og bin/paraphase_report.py bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/align_minimap2/* modules/local/deepvariant/* modules/local/fastp/* modules/local/html_report/* modules/local/paraphase/* modules/local/vcf_precheck/* scripts/01b-fastp-qc.sh scripts/24-html-report.sh scripts/35b-paraphase.sh scripts/ci/check-images.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh -paralogs-report-section-1-nf-report-tables assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/construct_variants.tsv assets/deep_intronic_sites.tsv assets/mito_confirmed.tsv assets/par_grch38.bed assets/pgx_genes.tsv assets/stub/sample.bam assets/stub/sample.bam.bai assets/stub/sample.vcf.gz bin/clip_check.py bin/cn_loci.tsv bin/collect_summary.py bin/copy_number.py bin/og bin/pgx_outside_calls.py bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/clinvar_screen/* modules/local/export_consumer/* modules/local/html_report/* modules/local/pypgx/* modules/local/slivar/* modules/local/vcf_precheck/* scripts/06-clinvar-screen.sh scripts/24-html-report.sh scripts/31-slivar.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/40-export-consumer.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh tests/fixtures/consumer_export/sites.tsv.gz +paralogs-report-section-1-nf-report-tables assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/construct_variants.tsv assets/deep_intronic_sites.tsv assets/mito_confirmed.tsv assets/par_grch38.bed assets/pgx_genes.tsv assets/stub/sample.bam assets/stub/sample.bam.bai assets/stub/sample.vcf.gz bin/clip_check.py bin/cn_loci.tsv bin/collect_summary.py bin/og bin/pgx_outside_calls.py bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/clinvar_screen/* modules/local/export_consumer/* modules/local/html_report/* modules/local/pypgx/* modules/local/slivar/* modules/local/vcf_precheck/* scripts/06-clinvar-screen.sh scripts/24-html-report.sh scripts/31-slivar.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/40-export-consumer.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh tests/fixtures/consumer_export/sites.tsv.gz pgx-1-depth-check bin/og scripts/21-cyrius.sh scripts/32-pypgx.sh scripts/ci/alt-depth-ab.sh scripts/ci/fake-docker/lib.sh -pgx-2-outside-calls assets/pgx_contraindications.tsv assets/pgx_genes.tsv assets/pgx_inhibitors.tsv bin/cn_loci.tsv bin/collect_summary.py bin/copy_number.py bin/og bin/pgx_outside_calls.py bin/traits_report.py scripts/07-pharmacogenomics.sh scripts/27-cpic-lookup.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh -pgx-4-parascopy assets/paralog_loci.tsv bin/cn_loci.tsv bin/copy_number.py bin/og bin/traits_report.py scripts/35-paralogs.sh scripts/ci/fake-docker/lib.sh scripts/setup.sh scripts/validate-setup.sh -pgx-5-kir assets/pgx_genes.tsv assets/stub/db/hla.dat assets/stub/db/kir.dat bin/cn_loci.tsv bin/copy_number.py bin/og bin/pgx_outside_calls.py bin/render_report.py scripts/08-hla-typing.sh scripts/24-html-report.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/ci/fake-docker/lib.sh scripts/setup.sh scripts/validate-setup.sh +pgx-2-outside-calls assets/pgx_contraindications.tsv assets/pgx_genes.tsv assets/pgx_inhibitors.tsv bin/cn_loci.tsv bin/collect_summary.py bin/og bin/pgx_outside_calls.py bin/traits_report.py scripts/07-pharmacogenomics.sh scripts/27-cpic-lookup.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh +pgx-4-parascopy assets/paralog_loci.tsv bin/cn_loci.tsv bin/og bin/traits_report.py scripts/35-paralogs.sh scripts/ci/fake-docker/lib.sh scripts/setup.sh scripts/validate-setup.sh +pgx-5-kir assets/pgx_genes.tsv assets/stub/db/hla.dat assets/stub/db/kir.dat bin/cn_loci.tsv bin/og bin/pgx_outside_calls.py bin/render_report.py scripts/08-hla-typing.sh scripts/24-html-report.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/ci/fake-docker/lib.sh scripts/setup.sh scripts/validate-setup.sh pgx-6-gvcf-column assets/live/index.html assets/par_grch38.bed assets/pgs_scores.tsv assets/viewer/index.html bin/collect_summary.py bin/og bin/traits_report.py modules/local/align_minimap2/* modules/local/deepvariant/* modules/local/fastp/* modules/local/indexcov/* modules/local/pharmcat/* modules/local/prs/* modules/local/vcf_precheck/* scripts/01b-fastp-qc.sh scripts/07-pharmacogenomics.sh scripts/16-indexcov.sh scripts/25-prs.sh scripts/26-ancestry.sh scripts/ci/check-images.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh -pgx-inhibitor-rows-1-contraindication assets/construct_variants.tsv assets/deep_intronic_sites.tsv assets/pgx_contraindications.tsv assets/pgx_genes.tsv assets/pgx_inhibitors.tsv assets/stub/sample.bam assets/stub/sample.bam.bai bin/clip_check.py bin/cn_loci.tsv bin/collect_summary.py bin/copy_number.py bin/og bin/pgx_outside_calls.py bin/traits_report.py modules/local/clinvar_screen/* modules/local/cpic_lookup/* modules/local/pharmcat/* modules/local/vcf_precheck/* scripts/06-clinvar-screen.sh scripts/07-pharmacogenomics.sh scripts/27-cpic-lookup.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh -pgx-parse-fixes-1-labels assets/pgx_contraindications.tsv assets/pgx_genes.tsv assets/pgx_inhibitors.tsv bin/cn_loci.tsv bin/collect_summary.py bin/copy_number.py bin/og bin/pgx_outside_calls.py bin/traits_report.py scripts/27-cpic-lookup.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh +pgx-inhibitor-rows-1-contraindication assets/construct_variants.tsv assets/deep_intronic_sites.tsv assets/pgx_contraindications.tsv assets/pgx_genes.tsv assets/pgx_inhibitors.tsv assets/stub/sample.bam assets/stub/sample.bam.bai bin/clip_check.py bin/cn_loci.tsv bin/collect_summary.py bin/og bin/pgx_outside_calls.py bin/traits_report.py modules/local/clinvar_screen/* modules/local/cpic_lookup/* modules/local/pharmcat/* modules/local/vcf_precheck/* scripts/06-clinvar-screen.sh scripts/07-pharmacogenomics.sh scripts/27-cpic-lookup.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh +pgx-parse-fixes-1-labels assets/pgx_contraindications.tsv assets/pgx_genes.tsv assets/pgx_inhibitors.tsv bin/cn_loci.tsv bin/collect_summary.py bin/og bin/pgx_outside_calls.py bin/traits_report.py scripts/27-cpic-lookup.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh pharmcat-indel-refblocks-1 bin/og scripts/07-pharmacogenomics.sh scripts/ci/fake-docker/lib.sh tests/fixtures/pharmcat/refblocks.g.vcf pharmcat-mito-input-and-log-1-mtrnr1 bin/og scripts/07-pharmacogenomics.sh scripts/ci/fake-docker/lib.sh platelet-antigens-1 assets/bloodtype_markers.tsv bin/cn_loci.tsv bin/og scripts/41-bloodtype.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh @@ -142,21 +142,21 @@ prs-3-panel-measure assets/pgs_scores.tsv bin/collect_summary.py bin/traits_repo prs-runopts-and-trace-1 assets/par_grch38.bed assets/pgs_scores.tsv bin/collect_summary.py bin/traits_report.py modules/local/align_minimap2/* modules/local/deepvariant/* modules/local/fastp/* modules/local/prs/* modules/local/vcf_precheck/* scripts/01b-fastp-qc.sh scripts/25-prs.sh scripts/26-ancestry.sh scripts/ci/check-images.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh prs-scores-and-run-options-1-strand-sex assets/pgs_scores.tsv bin/collect_summary.py bin/traits_report.py modules/local/prs/* modules/local/vcf_precheck/* scripts/25-prs.sh scripts/26-ancestry.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh prs-scores-and-run-options-2-panel-chrx assets/pgs_scores.tsv bin/collect_summary.py bin/traits_report.py scripts/25-prs.sh scripts/ci/fake-docker/lib.sh scripts/setup.sh -pypgx-cyp2d6-section-1-summary assets/pgx_genes.tsv bin/cn_loci.tsv bin/copy_number.py bin/og bin/pgx_outside_calls.py scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh +pypgx-cyp2d6-section-1-summary assets/pgx_genes.tsv bin/cn_loci.tsv bin/og bin/pgx_outside_calls.py scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh qc-1-sample-qc bin/collect_summary.py bin/og bin/traits_report.py scripts/28-multiqc.sh scripts/33-sample-qc.sh scripts/ci/fake-docker/lib.sh scripts/setup.sh -qc-2-nextflow-sample-qc assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/families.example.tsv assets/live/index.html assets/mito_confirmed.tsv assets/par_grch38.bed assets/pedigree.example.tsv assets/pgx_contraindications.tsv assets/pgx_genes.tsv assets/pgx_inhibitors.tsv assets/stub/db/hla.dat assets/stub/db/kir.dat assets/stub/pedigree.tsv assets/stub/sample.vcf.gz assets/viewer/index.html bin/cn_loci.tsv bin/collect_summary.py bin/copy_number.py bin/family_report.py bin/og bin/pgx_outside_calls.py bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/align_minimap2/* modules/local/cpic_lookup/* modules/local/deepvariant/* modules/local/fastp/* modules/local/hla_typing/* modules/local/html_report/* modules/local/indexcov/* modules/local/mito_haplogroup/* modules/local/mito_variants/* modules/local/mosdepth/* modules/local/pharmcat/* modules/local/pypgx/* modules/local/roh/* modules/local/somalier/* modules/local/t1k_build/* modules/local/telomere_hunter/* modules/local/vcf_precheck/* modules/local/verifybamid2/* scripts/01b-fastp-qc.sh scripts/07-pharmacogenomics.sh scripts/08-hla-typing.sh scripts/10-telomere-hunter.sh scripts/11-roh-analysis.sh scripts/12-mito-haplogroup.sh scripts/16-indexcov.sh scripts/16b-mosdepth.sh scripts/20-mtoolbox.sh scripts/24-html-report.sh scripts/27-cpic-lookup.sh scripts/32-pypgx.sh scripts/33-sample-qc.sh scripts/36-pgx-consensus.sh scripts/38-family.sh scripts/ci/check-images.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh +qc-2-nextflow-sample-qc assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/families.example.tsv assets/live/index.html assets/mito_confirmed.tsv assets/par_grch38.bed assets/pedigree.example.tsv assets/pgx_contraindications.tsv assets/pgx_genes.tsv assets/pgx_inhibitors.tsv assets/stub/db/hla.dat assets/stub/db/kir.dat assets/stub/pedigree.tsv assets/stub/sample.vcf.gz assets/viewer/index.html bin/cn_loci.tsv bin/collect_summary.py bin/family_report.py bin/og bin/pgx_outside_calls.py bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/align_minimap2/* modules/local/cpic_lookup/* modules/local/deepvariant/* modules/local/fastp/* modules/local/hla_typing/* modules/local/html_report/* modules/local/indexcov/* modules/local/mito_haplogroup/* modules/local/mito_variants/* modules/local/mosdepth/* modules/local/pharmcat/* modules/local/pypgx/* modules/local/roh/* modules/local/somalier/* modules/local/t1k_build/* modules/local/telomere_hunter/* modules/local/vcf_precheck/* modules/local/verifybamid2/* scripts/01b-fastp-qc.sh scripts/07-pharmacogenomics.sh scripts/08-hla-typing.sh scripts/10-telomere-hunter.sh scripts/11-roh-analysis.sh scripts/12-mito-haplogroup.sh scripts/16-indexcov.sh scripts/16b-mosdepth.sh scripts/20-mtoolbox.sh scripts/24-html-report.sh scripts/27-cpic-lookup.sh scripts/32-pypgx.sh scripts/33-sample-qc.sh scripts/36-pgx-consensus.sh scripts/38-family.sh scripts/ci/check-images.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh qc-3-cram assets/callability_genes.tsv bin/collect_summary.py bin/traits_report.py modules/local/cram_archive/* modules/local/mosdepth/* modules/local/vcf_precheck/* scripts/16b-mosdepth.sh scripts/34-cram-archive.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh reference-noalt scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh report-followups-1-sections assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/genotype_rules.tsv assets/mito_confirmed.tsv assets/par_grch38.bed assets/str_extra_loci.json assets/stub/db/catalog.json assets/stub/sample.vcf.gz assets/sv_rule_regions.tsv assets/traits.tsv bin/annotsv_postfilter.py bin/cnvpytor_to_vcf.py bin/collect_summary.py bin/family_report.py bin/og bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/annotsv/* modules/local/expansion_hunter/* modules/local/export_consumer/* modules/local/html_report/* modules/local/manta/* modules/local/traits/* modules/local/vcf_precheck/* scripts/04-manta.sh scripts/05-annotsv.sh scripts/09-expansion-hunter.sh scripts/15-duphold.sh scripts/18-cnvpytor.sh scripts/19-delly.sh scripts/24-html-report.sh scripts/40-export-consumer.sh scripts/42-traits.sh scripts/ci/fake-docker/lib.sh scripts/generate-report.sh scripts/validate-setup.sh tests/fixtures/consumer_export/sites.tsv.gz report-sections-mito-roh-telomere-cnv-1-mito assets/callability_genes.tsv bin/collect_summary.py bin/traits_report.py scripts/10-telomere-hunter.sh scripts/16b-mosdepth.sh scripts/20-mtoolbox.sh scripts/ci/fake-docker/lib.sh scripts/generate-report.sh scripts/validate-setup.sh reports-and-pgx-correctness-clinical assets/construct_variants.tsv bin/clip_check.py bin/og scripts/23-clinical-filter.sh scripts/31-slivar.sh scripts/ci/fake-docker/lib.sh -reports-and-pgx-correctness-pgx assets/pgx_contraindications.tsv assets/pgx_genes.tsv assets/pgx_inhibitors.tsv bin/cn_loci.tsv bin/collect_summary.py bin/copy_number.py bin/og bin/pgx_outside_calls.py bin/traits_report.py scripts/27-cpic-lookup.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh +reports-and-pgx-correctness-pgx assets/pgx_contraindications.tsv assets/pgx_genes.tsv assets/pgx_inhibitors.tsv bin/cn_loci.tsv bin/collect_summary.py bin/og bin/pgx_outside_calls.py bin/traits_report.py scripts/27-cpic-lookup.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh reports-and-pgx-correctness-reports assets/stub/db/clinvar.vcf.gz assets/stub/db/hla.dat bin/collect_summary.py bin/render_report.py bin/traits_report.py bin/write_manifest.sh scripts/24-html-report.sh scripts/ci/fake-docker/lib.sh scripts/generate-report.sh reports-and-pgx-correctness-revel scripts/30-vcfanno.sh scripts/ci/fake-docker/lib.sh -run-all-launcher assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/construct_variants.tsv assets/deep_intronic_sites.tsv assets/mito_confirmed.tsv assets/par_grch38.bed assets/pgs_scores.tsv assets/pgx_contraindications.tsv assets/pgx_genes.tsv assets/pgx_inhibitors.tsv assets/stub/db/hla.dat assets/stub/db/kir.dat assets/stub/sample.bam assets/stub/sample.bam.bai assets/stub/sample.vcf.gz bin/clip_check.py bin/cn_loci.tsv bin/collect_summary.py bin/copy_number.py bin/og bin/pgx_outside_calls.py bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/align_minimap2/* modules/local/clinvar_screen/* modules/local/cpic_lookup/* modules/local/deepvariant/* modules/local/fastp/* modules/local/hla_typing/* modules/local/html_report/* modules/local/mito_haplogroup/* modules/local/mito_variants/* modules/local/mosdepth/* modules/local/multiqc/* modules/local/pharmcat/* modules/local/prs/* modules/local/roh/* modules/local/t1k_build/* modules/local/telomere_hunter/* modules/local/vcf_precheck/* modules/local/vcfanno/* modules/local/vep/* scripts/01b-fastp-qc.sh scripts/06-clinvar-screen.sh scripts/07-pharmacogenomics.sh scripts/08-hla-typing.sh scripts/10-telomere-hunter.sh scripts/11-roh-analysis.sh scripts/12-mito-haplogroup.sh scripts/13-vep-annotation.sh scripts/16b-mosdepth.sh scripts/20-mtoolbox.sh scripts/24-html-report.sh scripts/25-prs.sh scripts/26-ancestry.sh scripts/27-cpic-lookup.sh scripts/28-multiqc.sh scripts/30-vcfanno.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/ci/check-images.sh scripts/ci/fake-docker/lib.sh scripts/ci/parity-diff.sh scripts/validate-setup.sh +run-all-launcher assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/construct_variants.tsv assets/deep_intronic_sites.tsv assets/mito_confirmed.tsv assets/par_grch38.bed assets/pgs_scores.tsv assets/pgx_contraindications.tsv assets/pgx_genes.tsv assets/pgx_inhibitors.tsv assets/stub/db/hla.dat assets/stub/db/kir.dat assets/stub/sample.bam assets/stub/sample.bam.bai assets/stub/sample.vcf.gz bin/clip_check.py bin/cn_loci.tsv bin/collect_summary.py bin/og bin/pgx_outside_calls.py bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/align_minimap2/* modules/local/clinvar_screen/* modules/local/cpic_lookup/* modules/local/deepvariant/* modules/local/fastp/* modules/local/hla_typing/* modules/local/html_report/* modules/local/mito_haplogroup/* modules/local/mito_variants/* modules/local/mosdepth/* modules/local/multiqc/* modules/local/pharmcat/* modules/local/prs/* modules/local/roh/* modules/local/t1k_build/* modules/local/telomere_hunter/* modules/local/vcf_precheck/* modules/local/vcfanno/* modules/local/vep/* scripts/01b-fastp-qc.sh scripts/06-clinvar-screen.sh scripts/07-pharmacogenomics.sh scripts/08-hla-typing.sh scripts/10-telomere-hunter.sh scripts/11-roh-analysis.sh scripts/12-mito-haplogroup.sh scripts/13-vep-annotation.sh scripts/16b-mosdepth.sh scripts/20-mtoolbox.sh scripts/24-html-report.sh scripts/25-prs.sh scripts/26-ancestry.sh scripts/27-cpic-lookup.sh scripts/28-multiqc.sh scripts/30-vcfanno.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/ci/check-images.sh scripts/ci/fake-docker/lib.sh scripts/ci/parity-diff.sh scripts/validate-setup.sh run-all-sample-qc-and-stale-index-1-index-times assets/par_grch38.bed modules/local/align_minimap2/* modules/local/deepvariant/* modules/local/fastp/* modules/local/roh/* modules/local/vcf_precheck/* scripts/01b-fastp-qc.sh scripts/11-roh-analysis.sh scripts/ci/check-images.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh setup-gencode-exons-and-nextflow-clip-check-1 assets/construct_variants.tsv assets/deep_intronic_sites.tsv assets/stub/sample.bam assets/stub/sample.bam.bai bin/clip_check.py bin/og modules/local/clinvar_screen/* modules/local/vcf_precheck/* scripts/06-clinvar-screen.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh -short-sample-id-stub assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/construct_variants.tsv assets/mito_confirmed.tsv assets/pgx_contraindications.tsv assets/pgx_genes.tsv assets/pgx_inhibitors.tsv assets/stub/db/hla.dat assets/stub/db/kir.dat assets/stub/sample.bam assets/stub/sample.bam.bai assets/stub/sample.vcf.gz assets/stub/sample.vcf.gz.tbi bin/clip_check.py bin/cn_loci.tsv bin/collect_summary.py bin/copy_number.py bin/cpsr_sample_id bin/og bin/pgx_outside_calls.py bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/cpic_lookup/* modules/local/cpsr/* modules/local/hla_typing/* modules/local/html_report/* modules/local/pharmcat/* modules/local/t1k_build/* modules/local/telomere_hunter/* modules/local/vcf_precheck/* scripts/07-pharmacogenomics.sh scripts/08-hla-typing.sh scripts/10-telomere-hunter.sh scripts/17-cpsr.sh scripts/24-html-report.sh scripts/27-cpic-lookup.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh +short-sample-id-stub assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/construct_variants.tsv assets/mito_confirmed.tsv assets/pgx_contraindications.tsv assets/pgx_genes.tsv assets/pgx_inhibitors.tsv assets/stub/db/hla.dat assets/stub/db/kir.dat assets/stub/sample.bam assets/stub/sample.bam.bai assets/stub/sample.vcf.gz assets/stub/sample.vcf.gz.tbi bin/clip_check.py bin/cn_loci.tsv bin/collect_summary.py bin/cpsr_sample_id bin/og bin/pgx_outside_calls.py bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/cpic_lookup/* modules/local/cpsr/* modules/local/hla_typing/* modules/local/html_report/* modules/local/pharmcat/* modules/local/t1k_build/* modules/local/telomere_hunter/* modules/local/vcf_precheck/* scripts/07-pharmacogenomics.sh scripts/08-hla-typing.sh scripts/10-telomere-hunter.sh scripts/17-cpsr.sh scripts/24-html-report.sh scripts/27-cpic-lookup.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh single-source-lib-ownership scripts/11-roh-analysis.sh scripts/ci/fake-docker/lib.sh str-catalog-1-strchive assets/str_extra_loci.json assets/stub/db/catalog.json assets/stub/sample.bam assets/stub/sample.bam.bai bin/og modules/local/expansion_hunter/* modules/local/stranger/* modules/local/vcf_precheck/* scripts/09-expansion-hunter.sh scripts/09b-stranger.sh scripts/ci/fake-docker/lib.sh scripts/setup.sh scripts/validate-setup.sh str-extra-repeats-1-readings assets/str_extra_loci.json bin/collect_summary.py bin/og bin/render_report.py bin/results_index.py bin/run_delta.py bin/traits_report.py modules/local/expansion_hunter/* modules/local/html_report/* scripts/09-expansion-hunter.sh scripts/24-html-report.sh scripts/ci/fake-docker/lib.sh scripts/generate-report.sh scripts/validate-setup.sh @@ -169,12 +169,13 @@ sv-mito-telomere-steps-6-scatter scripts/03a-gatk-haplotypecaller.sh scripts/03b traits-panel-1 assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/genotype_rules.tsv assets/mito_confirmed.tsv assets/stub/sample.vcf.gz assets/traits.tsv bin/collect_summary.py bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/html_report/* modules/local/traits/* modules/local/vcf_precheck/* scripts/24-html-report.sh scripts/42-traits.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh vcf-precheck-ref-spotcheck-1 assets/par_grch38.bed modules/local/align_minimap2/* modules/local/deepvariant/* modules/local/fastp/* modules/local/roh/* modules/local/vcf_precheck/* scripts/01b-fastp-qc.sh scripts/11-roh-analysis.sh scripts/ci/check-images.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh tests/fixtures/vcf/grch37_no_contigs.vcf vendor-vcf-intake-1-bash bin/og scripts/07-pharmacogenomics.sh scripts/ci/fake-docker/lib.sh -vendor-vcf-intake-2-nf-report assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/construct_variants.tsv assets/deep_intronic_sites.tsv assets/mito_confirmed.tsv assets/pgx_contraindications.tsv assets/pgx_genes.tsv assets/pgx_inhibitors.tsv assets/stub/sample.bam assets/stub/sample.bam.bai assets/stub/sample.vcf.gz bin/clip_check.py bin/cn_loci.tsv bin/collect_summary.py bin/copy_number.py bin/og bin/pgx_outside_calls.py bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/clinvar_screen/* modules/local/cpic_lookup/* modules/local/html_report/* modules/local/mito_haplogroup/* modules/local/mosdepth/* modules/local/multiqc/* modules/local/pharmcat/* modules/local/roh/* modules/local/vcf_precheck/* scripts/06-clinvar-screen.sh scripts/07-pharmacogenomics.sh scripts/11-roh-analysis.sh scripts/12-mito-haplogroup.sh scripts/16b-mosdepth.sh scripts/20-mtoolbox.sh scripts/24-html-report.sh scripts/27-cpic-lookup.sh scripts/28-multiqc.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh +vendor-vcf-intake-2-nf-report assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/construct_variants.tsv assets/deep_intronic_sites.tsv assets/mito_confirmed.tsv assets/pgx_contraindications.tsv assets/pgx_genes.tsv assets/pgx_inhibitors.tsv assets/stub/sample.bam assets/stub/sample.bam.bai assets/stub/sample.vcf.gz bin/clip_check.py bin/cn_loci.tsv bin/collect_summary.py bin/og bin/pgx_outside_calls.py bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/clinvar_screen/* modules/local/cpic_lookup/* modules/local/html_report/* modules/local/mito_haplogroup/* modules/local/mosdepth/* modules/local/multiqc/* modules/local/pharmcat/* modules/local/roh/* modules/local/vcf_precheck/* scripts/06-clinvar-screen.sh scripts/07-pharmacogenomics.sh scripts/11-roh-analysis.sh scripts/12-mito-haplogroup.sh scripts/16b-mosdepth.sh scripts/20-mtoolbox.sh scripts/24-html-report.sh scripts/27-cpic-lookup.sh scripts/28-multiqc.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh vendor-vcf-intake-3-nf-stops assets/callability_genes.tsv assets/construct_variants.tsv assets/deep_intronic_sites.tsv assets/stub/sample.bam assets/stub/sample.bam.bai bin/clip_check.py bin/collect_summary.py bin/og bin/traits_report.py modules/local/clinvar_screen/* modules/local/mito_haplogroup/* modules/local/pharmcat/* modules/local/roh/* modules/local/vcf_precheck/* scripts/06-clinvar-screen.sh scripts/07-pharmacogenomics.sh scripts/11-roh-analysis.sh scripts/12-mito-haplogroup.sh scripts/16b-mosdepth.sh scripts/20-mtoolbox.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh -vendor-vcf-intake-4-nf-header-clinvar assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/construct_variants.tsv assets/deep_intronic_sites.tsv assets/mito_confirmed.tsv assets/pgx_contraindications.tsv assets/pgx_genes.tsv assets/pgx_inhibitors.tsv assets/stub/sample.bam assets/stub/sample.bam.bai assets/stub/sample.vcf.gz bin/clip_check.py bin/cn_loci.tsv bin/collect_summary.py bin/copy_number.py bin/og bin/pgx_outside_calls.py bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/clinvar_screen/* modules/local/cpic_lookup/* modules/local/html_report/* modules/local/mito_haplogroup/* modules/local/pharmcat/* modules/local/roh/* modules/local/vcf_precheck/* scripts/06-clinvar-screen.sh scripts/07-pharmacogenomics.sh scripts/11-roh-analysis.sh scripts/12-mito-haplogroup.sh scripts/16b-mosdepth.sh scripts/20-mtoolbox.sh scripts/24-html-report.sh scripts/27-cpic-lookup.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh +vendor-vcf-intake-4-nf-header-clinvar assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/construct_variants.tsv assets/deep_intronic_sites.tsv assets/mito_confirmed.tsv assets/pgx_contraindications.tsv assets/pgx_genes.tsv assets/pgx_inhibitors.tsv assets/stub/sample.bam assets/stub/sample.bam.bai assets/stub/sample.vcf.gz bin/clip_check.py bin/cn_loci.tsv bin/collect_summary.py bin/og bin/pgx_outside_calls.py bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/clinvar_screen/* modules/local/cpic_lookup/* modules/local/html_report/* modules/local/mito_haplogroup/* modules/local/pharmcat/* modules/local/roh/* modules/local/vcf_precheck/* scripts/06-clinvar-screen.sh scripts/07-pharmacogenomics.sh scripts/11-roh-analysis.sh scripts/12-mito-haplogroup.sh scripts/16b-mosdepth.sh scripts/20-mtoolbox.sh scripts/24-html-report.sh scripts/27-cpic-lookup.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh vendor-vcf-intake-5-docs scripts/ci/fake-docker/lib.sh viral-reads-1 bin/og modules/local/vcf_precheck/* modules/local/viral/* scripts/47-viral.sh scripts/ci/fake-docker/lib.sh scripts/generate-report.sh scripts/setup.sh scripts/validate-setup.sh vntyper-1 assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/mito_confirmed.tsv assets/stub/sample.vcf.gz bin/collect_summary.py bin/og bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/html_report/* modules/local/vcf_precheck/* modules/local/vntyper/* scripts/24-html-report.sh scripts/52-vntyper.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh y-private-report-1-private-and-tree assets/live/index.html assets/viewer/index.html bin/collect_summary.py bin/og bin/render_report.py bin/results_index.py bin/run_delta.py bin/traits_report.py bin/yleaf_run.py modules/local/html_report/* modules/local/y_haplogroup/* scripts/16-indexcov.sh scripts/24-html-report.sh scripts/37-y-haplogroup.sh scripts/generate-report.sh scripts/ci/fake-docker/lib.sh scripts/setup.sh y-str-1 assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/live/index.html assets/mito_confirmed.tsv assets/stub/sample.vcf.gz assets/viewer/index.html assets/ystr_panel.tsv bin/collect_summary.py bin/og bin/render_report.py bin/results_index.py bin/traits_report.py modules/local/html_report/* modules/local/indexcov/* modules/local/vcf_precheck/* modules/local/y_str/* scripts/16-indexcov.sh scripts/24-html-report.sh scripts/45-y-str.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh y-tree-current-1-placement assets/live/index.html assets/viewer/index.html bin/og bin/yleaf_run.py scripts/16-indexcov.sh scripts/37-y-haplogroup.sh scripts/ci/fake-docker/lib.sh scripts/setup.sh +rust-c2b-traits assets/traits.tsv assets/genotype_rules.tsv bin/og modules/local/traits/* scripts/42-traits.sh diff --git a/scripts/ci/e2e-case-times.tsv b/scripts/ci/e2e-case-times.tsv index be90f627f..5fd3717ec 100644 --- a/scripts/ci/e2e-case-times.tsv +++ b/scripts/ci/e2e-case-times.tsv @@ -177,3 +177,4 @@ vntyper-1 shard 92 - y-private-report-1-private-and-tree shard 150 nextflow-hardening-2-nf y-str-1 shard 42 - y-tree-current-1-placement shard 21 nextflow-hardening-2-nf +rust-c2b-traits shard 2 - diff --git a/tests/demo/make_demo_sample.py b/tests/demo/make_demo_sample.py index 49bf9e2a5..ec57b5814 100755 --- a/tests/demo/make_demo_sample.py +++ b/tests/demo/make_demo_sample.py @@ -1282,13 +1282,21 @@ def traits(d): unknown = sorted(r["rsid"] for r in rows if r["rsid"] not in TRAIT_ALT_COPIES | {"rs4410790": 0, "rs4481887": 0}) if unknown: raise SystemExit(f"assets/traits.tsv has rows TRAIT_ALT_COPIES does not give: {', '.join(unknown)}") - recs = {} - for r in rows: - if r["rsid"] in TRAIT_ALT_COPIES: - n = TRAIT_ALT_COPIES[r["rsid"]] - recs[(r["chrom"], r["pos"])] = [(r["ref"], [r["ref"]] * (2 - n) + [r["alt"]] * n, "60")] os.makedirs(os.path.join(d, "traits"), exist_ok=True) - traits_report.write_tsv(os.path.join(d, "traits", f"{SAMPLE}_traits.tsv"), traits_report.report(rows, recs)) + with tempfile.TemporaryDirectory() as work: + genotypes = os.path.join(work, "g.tsv") + with open(genotypes, "w") as f: + for r in rows: + if r["rsid"] in TRAIT_ALT_COPIES: + n = TRAIT_ALT_COPIES[r["rsid"]] + gt = "/".join([r["ref"]] * (2 - n) + [r["alt"]] * n) + f.write(f"{r['chrom']}\t{r['pos']}\t{r['ref']}\t{r['alt']}\t{gt}\t60\n") + subprocess.run([os.environ.get("OG", os.path.join(REPO, "bin", "og")), "traits", "table", + "--table", os.path.join(REPO, "assets", "traits.tsv"), + "--rules", os.path.join(REPO, "assets", "genotype_rules.tsv"), + "--genotypes", genotypes, "--sample", SAMPLE, + "--out", os.path.join(d, "traits", f"{SAMPLE}_traits.tsv"), + "--rules-out", os.path.join(work, "rules.tsv")], check=True) def mocha(d): diff --git a/tests/e2e/genotype-rules-2-g2.sh b/tests/e2e/genotype-rules-2-g2.sh index 7216eb962..5932e63c3 100644 --- a/tests/e2e/genotype-rules-2-g2.sh +++ b/tests/e2e/genotype-rules-2-g2.sh @@ -40,16 +40,18 @@ mkdir -p "$D" for s in G2HET G2REF; do mkdir -p "${D}/${s}/vcf" "${D}/${s}/traits" python3 - "$REPO" "${D}/${s}" "$s" <<'PY' -import os, sys +import os, subprocess, sys repo, d, s = sys.argv[1:] sys.path.insert(0, os.path.join(repo, "bin")) -import traits_report as tr -rules = tr.read_rules(os.path.join(repo, "assets", "genotype_rules.tsv")) g = os.path.join(d, "g.tsv") with open(g, "w") as f: # G0/G1 at the two G1 sites f.write("chr22\t36265860\tA\tG\tA/G\t40\nchr22\t36265988\tT\tG\tT/G\t40\n") -tr.write_tsv(os.path.join(d, "traits", f"{s}_genotype_rules.tsv"), tr.rule_lines(rules, tr.read_genotypes(g)), - tr.RULE_OUT_COLUMNS) +subprocess.run([os.environ.get("OG", os.path.join(repo, "bin", "og")), "traits", "table", + "--table", os.path.join(repo, "assets", "traits.tsv"), + "--rules", os.path.join(repo, "assets", "genotype_rules.tsv"), + "--genotypes", g, "--sample", s, + "--out", os.path.join(d, "traits", f"{s}_traits.tsv"), + "--rules-out", os.path.join(d, "traits", f"{s}_genotype_rules.tsv")], check=True) os.remove(g) body = ["chr22\t36200000\t.\tA\t<*>\t0\t.\tEND=36265994\tGT:GQ\t0/0:50"] if s == "G2HET": @@ -72,7 +74,7 @@ done read_result() { python3 - "$REPO" "${D}/$1" "$1" <<'PY' -import os, sys +import os, subprocess, sys repo, d, s = sys.argv[1:] sys.path.insert(0, os.path.join(repo, "bin")) import collect_summary as cs diff --git a/tests/e2e/report-followups-1-sections.sh b/tests/e2e/report-followups-1-sections.sh index 4cfcf7451..b89ec541b 100644 --- a/tests/e2e/report-followups-1-sections.sh +++ b/tests/e2e/report-followups-1-sections.sh @@ -41,7 +41,6 @@ python3 - "$REPO" "$G" "$T" <<'PY' import json, os, shutil, subprocess, sys repo, g, s = sys.argv[1:4] sys.path.insert(0, os.path.join(repo, "bin")) -import traits_report # noqa: E402 def put(path, text): @@ -56,14 +55,15 @@ for kind in ("full", "bare"): # Step 42: TAS2R38 homozygous for PAV (G, G, C on the plus strand). geno = os.path.join(g, kind, "genotypes.tsv") put(geno, "chr7\t141972804\tT\tC\tC/C\t40\nchr7\t141972905\tG\t.\tG/G\t40\nchr7\t141973545\tC\tG\tG/G\t40\n") - rows = traits_report.read_table(os.path.join(repo, "assets", "traits.tsv")) - rules = traits_report.read_rules(os.path.join(repo, "assets", "genotype_rules.tsv")) - recs = traits_report.read_genotypes(geno) - lines = traits_report.rule_lines(rules, recs) os.makedirs(f"{d}/traits", exist_ok=True) - traits_report.write_tsv(f"{d}/traits/{s}_traits.tsv", traits_report.report(rows, recs, rules, None, lines)) - if kind == "full": - traits_report.write_tsv(f"{d}/traits/{s}_genotype_rules.tsv", lines, traits_report.RULE_OUT_COLUMNS) + subprocess.run([os.environ.get("OG", os.path.join(repo, "bin", "og")), "traits", "table", + "--table", os.path.join(repo, "assets", "traits.tsv"), + "--rules", os.path.join(repo, "assets", "genotype_rules.tsv"), + "--genotypes", geno, "--sample", s, + "--out", f"{d}/traits/{s}_traits.tsv", + "--rules-out", f"{d}/traits/{s}_genotype_rules.tsv"], check=True) + if kind == "bare": + os.remove(f"{d}/traits/{s}_genotype_rules.tsv") # Step 05: two class 4 deletions; the rule table puts the second over half inside an IG locus. head = ("AnnotSV_ID\tSV_chrom\tSV_start\tSV_end\tSV_length\tSV_type\tID\tINFO\tAnnotation_mode\tGene_name\t" "B_loss_source\tB_loss_AFmax\tACMG_class\tAnnotSV_ranking_score\n") diff --git a/tests/e2e/rust-c2b-traits.sh b/tests/e2e/rust-c2b-traits.sh new file mode 100755 index 000000000..71613e72f --- /dev/null +++ b/tests/e2e/rust-c2b-traits.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Traits, APOE ordering, phase sites and refusal controls on synthetic input. +. "$(dirname "$0")/lib.sh" +OG=${OG:-${REPO}/bin/og} +TABLE="${REPO}/assets/traits.tsv" +RULES="${REPO}/assets/genotype_rules.tsv" +G="${CASE_TMP}/g.tsv" +printf 'chr19\t44908684\tT\tC\tT/C\t40\nchr19\t44908822\tC\tT\tC/T\t40\nchr7\t141973545\tC\tG\tC/G\t30\nchr7\t141972905\tG\tA\tG/A\t31\nchr7\t141972804\tT\tC\tT/C\t32\n' > "$G" +"$OG" traits table --table "$TABLE" --rules "$RULES" --genotypes "$G" --sample S \ + --out "${CASE_TMP}/traits.tsv" --rules-out "${CASE_TMP}/rules.tsv" +check_eq 'traits table exits 0' "$?" 0 +check_eq 'APOE double heterozygote is e2/e4' "$(awk -F'\t' '$1 == "APOE" {print $10}' "${CASE_TMP}/rules.tsv")" e2/e4 +check_eq 'the taster rule needs phase' "$(awk -F'\t' '$1 == "TAS2R38" {print $11}' "${CASE_TMP}/rules.tsv")" unphased +check_eq 'APOE line occurs exactly once' "$(grep -c '^rs429358+rs7412' "${CASE_TMP}/traits.tsv")" 1 +check 'absent LCT has no inferred genotype' awk -F'\t' '$1 == "rs4988235" {ok = ($6 == "." && $7 == "not_covered")} END {exit !ok}' "${CASE_TMP}/traits.tsv" +"$OG" traits phase-sites --rules "$RULES" --genotypes "$G" --sample S --out "${CASE_TMP}/phase.vcf" +check_eq 'phase-sites exits 0' "$?" 0 +check_eq 'only the three taster sites need phase' "$(grep -cv '^#' "${CASE_TMP}/phase.vcf")" 3 +check_eq 'phase sites are in genome order' "$(grep -v '^#' "${CASE_TMP}/phase.vcf" | cut -f2 | paste -sd, -)" 141972804,141972905,141973545 +sed 's/44908684\tT\t/44908684\tG\t/' "$G" > "${CASE_TMP}/bad.tsv" +"$OG" traits table --table "$TABLE" --rules "$RULES" --genotypes "${CASE_TMP}/bad.tsv" --sample S \ + --out "${CASE_TMP}/bad-out.tsv" --rules-out "${CASE_TMP}/bad-rules.tsv" > "${CASE_TMP}/bad.log" 2>&1 +check_eq 'control: wrong reference is refused' "$?" 1 +check 'control: refusal names the site and GRCh38' has 'rs429358.*GRCh38' "$(cat "${CASE_TMP}/bad.log")" +check 'control: no table is written' test ! -e "${CASE_TMP}/bad-out.tsv" +"$OG" traits check --table "$TABLE" --rules "$RULES" +check_eq 'committed catalogs pass' "$?" 0 +finish diff --git a/tests/parity/README.md b/tests/parity/README.md index d83ff2087..3d32b9972 100644 --- a/tests/parity/README.md +++ b/tests/parity/README.md @@ -42,3 +42,8 @@ A `known.tsv` row whose difference is gone fails. A golden that the oracle also 3. Run `scripts/ci/oracle-diff.sh`. Turn each difference into a fix, a golden or a `known.tsv` row. 4. Switch the callers to `og`, delete the Python, and add the tool to `OG_SHADOW` in `.github/workflows/e2e.yml`. 5. Link a red run in the pull request: the cases with one planted change in the port must fail. + +The `traits` cases cover all APOE genotype pairs, missing and other alleles, +duplicate records, TAS2R38 phase blocks, phase-site VCFs and catalog refusals. +The oracle adapter maps the three Rust verbs to the old flags. The Python +report keeps only the metadata and table reader listed in `remnants.tsv`. diff --git a/tests/parity/gen/traits.sh b/tests/parity/gen/traits.sh index 10d0fa26f..a57771388 100644 --- a/tests/parity/gen/traits.sh +++ b/tests/parity/gen/traits.sh @@ -33,10 +33,10 @@ printf 'chr19\t44908684\tT\tC\tT/C\t30\n' > missing.tsv printf 'chr19\t44908684\tTT\tT\tTT/T\t10\nchr19\t44908684\tT\tC\tC/T\t40\n' > duplicate.tsv printf 'chr19\t44908684\tG\tC\tG/C\t30\n' > wrong-ref.tsv printf 'chr19\t44908684\tT\tC\t./C\t30\n' > half.tsv -printf 'chr7\t141973545\tC\tG\tC/G\t30\nchr7\t141972905\tG\tC\tG/C\t31\nchr7\t141972804\tT\tC\tT/C\t32\n' > triple.tsv +printf 'chr7\t141973545\tC\tG\tC/G\t30\nchr7\t141972905\tG\tA\tG/A\t31\nchr7\t141972804\tT\tC\tT/C\t32\n' > triple.tsv head -1 triple.tsv > single.tsv : > empty.vcf -printf 'chr7\t141973545\t.\tC\tG\t.\tPASS\t.\tGT:PS\t1|0:7\nchr7\t141972905\t.\tG\tC\t.\tPASS\t.\tGT:PS\t0|1:7\nchr7\t141972804\t.\tT\tC\t.\tPASS\t.\tGT:PS\t1|0:7\n' > phased.vcf +printf 'chr7\t141973545\t.\tC\tG\t.\tPASS\t.\tGT:PS\t1|0:7\nchr7\t141972905\t.\tG\tA\t.\tPASS\t.\tGT:PS\t0|1:7\nchr7\t141972804\t.\tT\tC\t.\tPASS\t.\tGT:PS\t1|0:7\n' > phased.vcf sed '1s/:7$/:8/' phased.vcf > split.vcf awk -F'\t' -v OFS='\t' '$1 == "combination" && $2 == "APOE" && $3 == "e3/e3" {next} {print}' rules.tsv > bad-rules.tsv awk -F'\t' -v OFS='\t' '$3 == "rs671" {$6=$5} {print}' traits.tsv > bad-table.tsv diff --git a/tests/parity/oracle.ref b/tests/parity/oracle.ref index 526797497..7d9e929b1 100644 --- a/tests/parity/oracle.ref +++ b/tests/parity/oracle.ref @@ -63,4 +63,4 @@ y-yleaf-positions 0baddb45aa3871a57469cf20295bdd832f72f5c0 file:bin/yleaf_run.py lookup 0baddb45aa3871a57469cf20295bdd832f72f5c0 file:bin/genome_lookup.py python3 lookup lookup-query 0baddb45aa3871a57469cf20295bdd832f72f5c0 file:bin/genome_lookup.py bash ./oracle.sh lookup query family-ibd 0baddb45aa3871a57469cf20295bdd832f72f5c0 file:bin/ibd_report.py sh ./ibd_oracle.sh family ibd -traits 669a8196 file:bin/traits_report.py bash ./oracle.sh traits +traits 669a8196cf4c377200f9b4af9c235ef85fb774a1 file:bin/traits_report.py bash ./oracle.sh traits diff --git a/tests/parity/remnants.tsv b/tests/parity/remnants.tsv index 479efa8b8..084de768c 100644 --- a/tests/parity/remnants.tsv +++ b/tests/parity/remnants.tsv @@ -9,3 +9,4 @@ bin/pgx_outside_calls.py HLA_GENES,NO_CALL,PROBABLE,NOT_CALLABLE,LOW_GQ,is_call, bin/paraphase_report.py SMN1_EXON7,parascopy_smn1,compare_smn1 bin/collect_summary.py,bin/couple_report.py,tests/test_paralog_loci.py,tests/test_paraphase_report.py R4 bin/cnvpytor_to_vcf.py DEPTH_MIN_SIZE,delly_depth_contradicts bin/couple_report.py R4 bin/annotsv_postfilter.py CONSENSUS_BP,KEEP,ADDED,open_text,chrom_key,sv_type,info_value,sample_format,sample_columns,read_consensus,consensus_support bin/collect_summary.py,tests/demo/make_demo_sample.py R3 +bin/traits_report.py COLUMNS,OUT_COLUMNS,RULE_OUT_COLUMNS,TRAITS_TABLE_RULES,TableError,read_table,MARKERS,MARKER_STATUSES bin/collect_summary.py,tests/demo/make_demo_sample.py,tests/test_traits_report.py,tests/test_traits_table.py,tests/test_collect_summary.py,tests/test_lpa_kiv2.py,tests/test_render_report.py R3 diff --git a/tests/test_collect_summary.py b/tests/test_collect_summary.py index 62d7d1b93..ee2580d72 100644 --- a/tests/test_collect_summary.py +++ b/tests/test_collect_summary.py @@ -1206,19 +1206,18 @@ def report_followups_sections(work, schema): # (G, G, C on the plus strand; rs1726866's G is the reference base). t = os.path.join(work, "rules", "S") rows = traits_report.read_table(os.path.join(REPO, "assets", "traits.tsv")) - rules = traits_report.read_rules(os.path.join(REPO, "assets", "genotype_rules.tsv")) g = os.path.join(work, "rules_g.tsv") put(g, "chr7\t141972804\tT\tC\tC/C\t40\nchr7\t141972905\tG\t.\tG/G\t40\nchr7\t141973545\tC\tG\tG/G\t40\n" "chr19\t44908684\tT\t.\tT/T\t40\nchr19\t44908822\tC\t.\tC/C\t40\n") - recs = traits_report.read_genotypes(g) - lines = traits_report.rule_lines(rules, recs) + from test_traits_report import traits_tables os.makedirs(f"{t}/traits", exist_ok=True) - traits_report.write_tsv(f"{t}/traits/S_traits.tsv", traits_report.report(rows, recs, rules, None, lines)) + saved_rules = os.path.join(work, "rules_saved.tsv") + traits_tables(g, f"{t}/traits/S_traits.tsv", saved_rules) summ, txt, html_ = render(t) check("rules: without the rules table the section has no rule lines and the reports none", summ["sections"]["traits"]["data"].get("rules") is None and "Genotype rules (several sites" not in txt + html_, summ["sections"]["traits"]["data"].get("rules")) - traits_report.write_tsv(f"{t}/traits/S_genotype_rules.tsv", lines, traits_report.RULE_OUT_COLUMNS) + shutil.copyfile(saved_rules, f"{t}/traits/S_genotype_rules.tsv") summ, txt, html_ = render(t) html_ = htmlmod.unescape(html_) rl = summ["sections"]["traits"]["data"].get("rules") or [] @@ -1226,7 +1225,9 @@ def report_followups_sections(work, schema): [(r["rule"], r["result"], r["status"], r["phase"]) for r in rl if r["rule"] in ("TAS2R38", "APOE")] == [("TAS2R38", "PAV/PAV", "called", "not_needed")], rl) - trait = rules["TAS2R38"]["trait"] + import csv + with open(saved_rules) as f: + trait = next(r["trait"] for r in csv.DictReader(f, delimiter="\t") if r["rule"] == "TAS2R38") check("rules: the text report prints the rule line with its result and sites", f"{trait} (TAS2R38 rs713598, rs1726866, rs10246939): PAV/PAV (no phase needed). " in txt and "traits/S_genotype_rules.tsv" in txt, txt) @@ -2861,7 +2862,8 @@ def strec(rep, cn, so, ci, adsp, st, nmax="20", pmin="40"): f"{ear['chrom']}\t{ear['pos']}\t{ear['ref']}\t.\t{ear['ref']}/{ear['ref']}\n" "chr19\t44908684\tT\t.\tT/T\nchr19\t44908822\tC\t.\tC/C\n") os.makedirs(f"{t}/traits", exist_ok=True) - traits_report.write_tsv(f"{t}/traits/S_traits.tsv", traits_report.report(rows, traits_report.read_genotypes(g))) + from test_traits_report import traits_tables + traits_tables(g, f"{t}/traits/S_traits.tsv", os.path.join(work, "traits_rules.tsv")) summ_t, txt_t, html_t = render(t) td = summ_t["sections"]["traits"] check("traits: section ok, counts without the APOE rows", diff --git a/tests/test_copy_number.py b/tests/test_copy_number.py index b00a00302..03a4bf349 100644 --- a/tests/test_copy_number.py +++ b/tests/test_copy_number.py @@ -86,7 +86,30 @@ CATALOG = os.path.join(REPO, "bin", "cn_loci.tsv") MODELS = os.path.join(REPO, "assets", "parascopy_models_v1.7.txt") sys.path.insert(0, os.path.join(REPO, "bin")) -import copy_number as cn # noqa: E402 +# The old engine is a test oracle pinned to the traits port's parent. It is +# materialised only for these historical differential assertions. No pipeline +# step imports or ships a second engine. +import importlib.util # noqa: E402 + + +def catalog_oracle(): + ref = "669a8196cf4c377200f9b4af9c235ef85fb774a1" + if subprocess.run(["git", "-C", REPO, "cat-file", "-e", ref + "^{commit}"], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode: + subprocess.run(["git", "-C", REPO, "fetch", "--depth=1", "origin", ref], check=True) + source = subprocess.check_output(["git", "-C", REPO, "show", ref + ":bin/copy_number.py"]) + with tempfile.TemporaryDirectory(prefix="cn-oracle-") as work: + path = os.path.join(work, "copy_number.py") + with open(path, "wb") as f: + f.write(source) + spec = importlib.util.spec_from_file_location("cn_catalog_oracle", path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + module.CATALOG = os.path.join(REPO, "bin", "cn_loci.tsv") + return module + + +cn = catalog_oracle() FAILS = 0 # Frozen from bin/hba_copy_number.py and bin/lpa_kiv2.py as they were before the diff --git a/tests/test_genotype_rules_indels.py b/tests/test_genotype_rules_indels.py index 777aaf4b9..35fccfd72 100644 --- a/tests/test_genotype_rules_indels.py +++ b/tests/test_genotype_rules_indels.py @@ -155,8 +155,7 @@ def bgzip_with_tbi(path, lines): def report_checks(): import collect_summary as cs import render_report as rr - import traits_report as tr - rules = tr.read_rules(os.path.join(REPO, "assets", "genotype_rules.tsv")) + from test_traits_report import traits_tables head = "##fileformat=VCFv4.2\n##contig=\n#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\tFORMAT\tS\n" g1 = [("chr22", "36265860", "A", "G", "A/G", "40"), ("chr22", "36265988", "T", "G", "T/G", "40")] work = tempfile.mkdtemp() @@ -169,7 +168,7 @@ def folder(name, gvcf_lines, kind): g = os.path.join(work, name + "_g.tsv") with open(g, "w") as f: f.writelines("\t".join(x) + "\n" for x in g1) - tr.write_tsv(p, tr.rule_lines(rules, tr.read_genotypes(g)), tr.RULE_OUT_COLUMNS) + traits_tables(g, os.path.join(d, "traits", "S_traits.tsv"), p) v = os.path.join(d, "vcf", "S.g.vcf.gz" if kind == "gvcf" else "S.vcf.gz") with gzip.open(v, "wt") as f: f.write(head + "".join("\t".join(r) + "\n" for r in gvcf_lines)) diff --git a/tests/test_hba_copy_number.py b/tests/test_hba_copy_number.py index a31afb5cd..4dc92a38c 100755 --- a/tests/test_hba_copy_number.py +++ b/tests/test_hba_copy_number.py @@ -35,7 +35,7 @@ CATALOG = os.path.join(REPO, "bin", "cn_loci.tsv") OG = os.environ.get("OG", "") sys.path.insert(0, os.path.join(REPO, "bin")) -import copy_number as cn # noqa: E402 +from test_copy_number import cn # noqa: E402 pinned differential oracle REGIONS = cn.regions(cn.load_catalog(CATALOG)["HBA"]) # Step 35's HBA table, in its order (what bin/hba_copy_number.py wrote). diff --git a/tests/test_lpa_kiv2.py b/tests/test_lpa_kiv2.py index c1aeccf10..03ab6f15f 100644 --- a/tests/test_lpa_kiv2.py +++ b/tests/test_lpa_kiv2.py @@ -72,7 +72,7 @@ shutil.copy2(os.path.join(REPO, "bin", "cn_loci.tsv"), os.path.join(_og_dir, "cn_loci.tsv")) OG = os.environ["OG"] = os.path.join(_og_dir, "og") sys.path.insert(0, os.path.join(REPO, "bin")) -import copy_number as cn # noqa: E402 +from test_copy_number import cn # noqa: E402 pinned differential oracle FAILS = 0 # The two LPA rows; named once here so no line of this file reads as an rsID with a genotype. diff --git a/tests/test_render_report.py b/tests/test_render_report.py index c5406109a..a8377a4d6 100644 --- a/tests/test_render_report.py +++ b/tests/test_render_report.py @@ -958,7 +958,10 @@ def mosaic_table(rows): d = f"{work}/traits/S" rows = traits_report.read_table(os.path.join(REPO, "assets", "traits.tsv")) put(f"{d}/traits/S_traits.tsv", "") - traits_report.write_tsv(f"{d}/traits/S_traits.tsv", traits_report.report(rows, {})) + from test_traits_report import traits_tables + g = os.path.join(work, "traits_empty.tsv") + put(g, "") + traits_tables(g, f"{d}/traits/S_traits.tsv", os.path.join(work, "traits_rules.tsv")) summ = collect_summary.collect("S", d) txt, html = render_report.text_report(summ), render_report.html_report(summ) check("traits: both reports say a reading is not a diagnosis", diff --git a/tests/test_traits_report.py b/tests/test_traits_report.py index cd9b75470..a10e13f47 100644 --- a/tests/test_traits_report.py +++ b/tests/test_traits_report.py @@ -1,92 +1,26 @@ #!/usr/bin/env python3 -"""bin/traits_report.py (step 42) on synthetic bcftools output, on assets/traits.tsv and on -assets/genotype_rules.tsv; and scripts/ci/check-rule-alleles.sh on the rules. +"""Step 42 CLI goldens, rule-allele controls and og cn marker outputs. -Checks: - 1. a reference block expanded at a position (ALT '.', genotype G/G) reads as - ref/ref with the table's hom_ref text; ref/alt and alt/alt read het and - hom_alt, phased (C|T) as unphased, the reference allele printed first; - 2. a planted alternate allele changes the reading: the same position read - as ref/ref and then as ref/alt gives two different readings, each the - table's own; - 3. a position with no line is not_covered (never read as the reference); an - allele the table does not describe and a deletion starting at the - position are other_allele, not read; a half call that reaches the script - (the bcftools genotype filter of step 42 drops it first, so in a run its - position is not_covered) is other_allele too; - 4. a gVCF REF that is not the table's reference base stops the run (exit 1, - the row named), so a gVCF on another build is not read; - 5. the APOE line, now read from the APOE rule: for every pair of rs429358 and - rs7412 genotypes, one site not covered and an allele the rule does not - describe, the line is byte for byte the line the hand code of main - (6056452) wrote, by its sha256; it comes right after the second APOE row, - once; control: one changed character in a reading changes the hash; - report() without rules reads the repository's rules table; - 6. the rules table passes the schema check, and planted problems are each - caught: a missing and an extra combination line, a haplotype allele that - is neither ref nor alt, diplotypes out of haplotype order, a site that - disagrees with its traits table row, no APOE rule, a line of an unknown - kind; - 7. the rules on synthetic gVCF records, for each rule: reference blocks only - (homozygous reference, with the block's GQ; TAS2R38's reference haplotype - is AAI, which the rule does not read); APOE's double heterozygote - (e2/e4, maybe e1/e3) without phase; TAS2R38 homozygous PAV/PAV and a - single heterozygous site, which need no phase; the triple heterozygote - unphased (WhatsHap ran and no read pair spans the sites, or no phasing - ran), phased in one block (PAV/AVI, read-backed), phased in two blocks - (unphased), phased into a haplotype the rule does not describe; a site - not covered; - 8. --phase-sites writes only the heterozygous sites of a rule that needs - phase and has two or more of them, as a sorted VCF; WhatsHap's phased VCF - is read back (GT with |, PS), its unphased records left out; - 9. the command line writes the traits table (one row per table row plus the - APOE line, the columns in order) and the rules table (one line per rule), - and exits 1 on a traits table or a rules table that fails the schema check; - 10. scripts/ci/check-rule-alleles.sh passes on the rules and fails, naming the - site and the mistake, on planted copies: ref and alt swapped, the - minus-strand bases, an rsID the dbSNP table lacks, a wrong position; and - on a rules file with no site, and on a dbSNP table of another build than - versions.env pins; - 11. the copy-aware markers (MARKERS: LPA KIV-2 4925G>A, SMN1 g.27134T>G): - each pinned dbSNP record is one of the marker's copies with its ref and - alt, and a planted swap, a minus-strand pair and a position off the - copies are each caught; the context's middle base is the ref. With OG - naming the og binary (skipped without it, a failure with OG_REQUIRED - set too): og cn marker-regions prints one region per copy of MARKERS; - og cn marker-read SMN1_27134 on mpileup lines made up - for each case at the SMN1 and SMN2 copies: a quarter of the reads with G - is 'present' (copies = fraction x agCN), T only (the reference) 'not - seen', 2 reads with G 'uncertain', under 10 reads 'too little depth', an - unreadable pileup 'not assessed' (exit 0); the SMN1 psCN and the agCN - come from step 35's table (homologous region included, quality under 20 - not resolved); every message, for every status and every psCN, says the - marker does not prove 2+0 (control: a message without the sentence fails - the same test). - -Run: python3 tests/test_traits_report.py +Internal validation, phase and bounded-reader assertions live in +og-steps::traits::tests. The parity harness checks the old Python at oracle.ref. """ +import csv import hashlib import os import shutil import subprocess import sys import tempfile - REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.join(REPO, "bin")) import traits_report as tr # noqa: E402 - TABLE = os.path.join(REPO, "assets", "traits.tsv") RULES = os.path.join(REPO, "assets", "genotype_rules.tsv") -SCRIPT = os.path.join(REPO, "bin", "traits_report.py") CHECK = os.path.join(REPO, "scripts", "ci", "check-rule-alleles.sh") OG = os.environ.get("OG", "") -# The sentence every SMN1 marker message ends with. +SMN_HEADER = "chrom\tstart\tend\tlocus\tagCN_filter\tagCN\tagCN_qual\tpsCN_filter\tpsCN\tpsCN_qual\thomologous_regions\n" SMN1_NOT_PROOF = "This marker does not prove 2+0, and its absence does not rule 2+0 out." FAILS = 0 - -# The sha256 of the APOE line (its OUT_COLUMNS joined by tabs) the hand code of -# main 6056452 wrote, per rs429358 and rs7412 genotype ("." = no line). APOE_GOLDEN = { ("T/T", "C/C"): "e3d7512b649f1d23f5acbcaa08da19bd6a009d5b39ac57cb6856591c50e7cfda", ("T/T", "C/T"): "62e910d8ec7ac45ab15e9c9ed85c93b128ac41703d1149a332082dc0d7b5f389", @@ -100,10 +34,6 @@ ("T/C", "."): "34624db16a92981c0882edb3d8ce8d291162aa78b7c5208eba2360141e6ccbd8", ("T/G", "C/C"): "1d0aee0b8d241561ebd7477d6f4a8be274a48157a297abecd444c6c6a0a6abac", } -# TAS2R38's three sites in the rule's order (codons 49, 262, 296). -T1, T2, T3 = ("chr7", "141973545"), ("chr7", "141972905"), ("chr7", "141972804") - - def check(desc, ok, detail=""): global FAILS print(f"[{'PASS' if ok else 'FAIL'}] {desc}{'' if ok else ' -- ' + str(detail)[:600]}") @@ -116,46 +46,6 @@ def write(path, text): f.write(text) -def recs(*lines): - """bcftools' CHROM POS REF ALT GENOTYPE [GQ] lines -> read_genotypes' records.""" - d = tempfile.mkdtemp() - try: - p = os.path.join(d, "g.tsv") - write(p, "".join("\t".join(map(str, l)) + "\n" for l in lines)) - return tr.read_genotypes(p) - finally: - shutil.rmtree(d) - - -def phased_vcf(*records): - """WhatsHap-style VCF records (chrom, pos, ref, alt, GT[:PS], GT as allele indexes) -> read_phased's dict.""" - d = tempfile.mkdtemp() - try: - p = os.path.join(d, "p.vcf") - body = "".join(f"{c}\t{pos}\t.\t{ref}\t{alt}\t.\tPASS\t.\t{'GT:PS' if ':' in g else 'GT'}\t{g}\n" - for c, pos, ref, alt, g in records) - write(p, "##fileformat=VCFv4.2\n#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\tFORMAT\tS\n" + body) - return tr.read_phased(p) - finally: - shutil.rmtree(d) - - -def apoe_line(out): - return next(o for o in out if o["rsid"] == "rs429358+rs7412") - - -def line_hash(o): - return hashlib.sha256("\t".join(str(o[c]) for c in tr.OUT_COLUMNS).encode()).hexdigest() - - -def run(*args): - return subprocess.run([sys.executable, SCRIPT, *args], capture_output=True, text=True) - - -def og_cn(*args): - return subprocess.run([OG, "cn", *args], capture_output=True, text=True) - - def dbsnp_checks(rules): """10. scripts/ci/check-rule-alleles.sh on the rules and on planted copies. Runs first, so a rules table that breaks the other checks still shows what dbSNP says.""" @@ -206,9 +96,6 @@ def planted(desc, new, want): shutil.rmtree(work) -# 11. The multi-SNP rules (APOL1, SERPINA1, HFE, GC, CAD9p21, MAPT): per rule, the -# genotypes (ALT allele per site, "." for a reference block) of one positive case and -# its label, and of one negative case, a copy of the positive with one allele flipped. MULTI_SNP = { "APOL1": [(("G/G", "G/G"), "G1/G1"), (("A/G", "T/G"), "G0/G1")], "SERPINA1": [(("T/T", "T/T"), "PiZZ"), (("T/A", "C/T"), "PiSZ"), (("A/A", "C/C"), "PiSS")], @@ -222,64 +109,6 @@ def planted(desc, new, want): "CAD9p21": "no risk copy", "MAPT": "H1/H1"} -def multi_snp_checks(rules): - """11. Each multi-SNP rule on synthetic records: every positive genotype gives its label, - reference blocks alone give the reference label, and the control: the first positive - case with one allele flipped at one site gives another result. Then the lines reach the - HTML report: collect_summary reads them from the rules table and render_report writes - one row per rule.""" - import collect_summary as cs - import render_report as rr - - def line(rid, gts): - sites = rules[rid]["sites"] - rows = [] - for s, gt in zip(sites, gts): - a = [x for x in gt.split("/") if x != s["ref"]] - rows.append((s["chrom"], s["pos"], s["ref"], a[0] if a else ".", gt, "40")) - return next(x for x in tr.rule_lines(rules, recs(*rows)) if x["rule"] == rid) - - check("11. every multi-SNP rule is in the rules table", set(MULTI_SNP) <= set(rules), sorted(rules)) - for rid, cases in MULTI_SNP.items(): - if rid not in rules: - continue - sites = rules[rid]["sites"] - for gts, want in cases: - ln = line(rid, gts) - check(f"{rid} {','.join(gts)}: {want}", (ln["status"], ln["result"]) == ("called", want), ln) - ref = tuple(f"{s['ref']}/{s['ref']}" for s in sites) - ln = line(rid, ref) - check(f"{rid}, reference blocks only: {MULTI_SNP_REF[rid]}", - (ln["status"], ln["result"]) == ("called", MULTI_SNP_REF[rid]), ln) - gts, want = cases[0] - a, b = gts[0].split("/") - s0 = sites[0] - flip = s0["ref"] if b == s0["alt"] else s0["alt"] - flipped = (f"{a}/{flip}",) + tuple(gts[1:]) - ln = line(rid, flipped) - check(f"control: {rid} with one allele flipped at {s0['rsid']} ({','.join(flipped)}) is not {want}", - ln["result"] != want, ln) - apol1 = line("APOL1", ("A/G", "T/G"))["reading"] - check("APOL1 G0/G1 says G2 is not read and the other copy could carry it", - "G2 is not read" in apol1 and "G1/G2" in apol1, apol1) - work = tempfile.mkdtemp() - try: - os.makedirs(os.path.join(work, "traits")) - lines = [line(rid, cases[0][0]) for rid, cases in MULTI_SNP.items() if rid in rules] - tr.write_tsv(os.path.join(work, "traits", "S_genotype_rules.tsv"), lines, tr.RULE_OUT_COLUMNS) - read = cs.genotype_rules(work, "S") or [] - page = "\n".join(rr.rules_html({"rules": read}, "S")) - for rid, cases in MULTI_SNP.items(): - if rid in rules: - check(f"HTML report: the {rid} rule row with its result {cases[0][1]}", - rr.E(rules[rid]["trait"]) in page and rr.E(cases[0][1]) in page, page[:300]) - finally: - shutil.rmtree(work) - - -SMN_HEADER = "chrom\tstart\tend\tlocus\tagCN_filter\tagCN\tagCN_qual\tpsCN_filter\tpsCN\tpsCN_qual\thomologous_regions\n" - - def says_not_proof(message): """The SMN1 marker's message must say the marker does not prove 2+0.""" return "does not prove 2+0" in message @@ -287,22 +116,6 @@ def says_not_proof(message): def marker_checks(): """11. The copy-aware markers.""" - for name, m in tr.MARKERS.items(): - check(f"marker {name}: the pinned dbSNP record agrees with the marker", tr.marker_errors(name) == [], - tr.marker_errors(name)) - rs, chrom, pos, ref, alt = m["dbsnp"] - comp = {"A": "T", "C": "G", "G": "C", "T": "A"} - e = tr.marker_errors(name, dict(m, ref=alt, alt=ref)) - check(f"marker {name} control: ref and alt swapped is caught", any("swapped" in x for x in e), e) - e = tr.marker_errors(name, dict(m, ref=comp[ref], alt=comp[alt], - context=m["context"][:10] + comp[ref] + m["context"][11:])) - check(f"marker {name} control: the minus-strand bases are caught", any("minus-strand" in x for x in e), e) - e = tr.marker_errors(name, dbsnp=(rs, chrom, pos + 1, ref, alt)) - check(f"marker {name} control: a dbSNP position off the copies is caught", - any("not at one of the marker's copies" in x for x in e), e) - e = tr.marker_errors(name, dict(m, context=m["context"][:10] + alt + m["context"][11:])) - check(f"marker {name} control: a context whose middle base is not the ref is caught", - any("middle base" in x for x in e), e) check("LPA_KIV2_4925: seven copies (six KIV-2 units and KIV-1)", len(tr.MARKERS["LPA_KIV2_4925"]["copies"]) == 7) if not OG: print("skip: OG is not set (OG=rust/target/release/og): og cn marker-regions and marker-read did not run") @@ -395,235 +208,73 @@ def table(p, smn_path): shutil.rmtree(work) -def main(): - rules = tr.read_rules(RULES) - dbsnp_checks(rules) - rows = tr.read_table(TABLE) - by_id = {r["rsid"]: r for r in rows} - lct = by_id["rs4988235"] # chr2:135851076 G>A - c, p, ref, alt = lct["chrom"], lct["pos"], lct["ref"], lct["alt"] - at = lambda *ls: recs(*ls).get((c, p), []) # noqa: E731 - # 1. genotypes and their readings - g = tr.call(lct, at((c, p, ref, ".", f"{ref}/{ref}"))) - check("expanded reference block: ref/ref, hom_ref reading", g == (f"{ref}/{ref}", "called", lct["hom_ref"]), g) - g = tr.call(lct, at((c, p, ref, alt, f"{alt}/{ref}"))) - check("ref/alt: het reading, reference allele first", g == (f"{ref}/{alt}", "called", lct["het"]), g) - g = tr.call(lct, at((c, p, ref, alt, f"{ref}|{alt}"))) - check("phased ref|alt reads as ref/alt", g == (f"{ref}/{alt}", "called", lct["het"]), g) - g = tr.call(lct, at((c, p, ref, alt, f"{alt}/{alt}"))) - check("alt/alt: hom_alt reading", g == (f"{alt}/{alt}", "called", lct["hom_alt"]), g) +def traits_tables(genotypes, out, rules_out, sample="S"): + """Use og in the Rust job. Unit shards without og use the pinned oracle.""" + args = ["--table", TABLE, "--rules", RULES, "--genotypes", genotypes, + "--sample", sample, "--out", out, "--rules-out", rules_out] + if OG: + subprocess.run([OG, "traits", "table", *args], check=True, capture_output=True, text=True) + else: + with tempfile.TemporaryDirectory(prefix="traits-oracle-") as work: + ref = "669a8196cf4c377200f9b4af9c235ef85fb774a1" + if subprocess.run(["git", "-C", REPO, "cat-file", "-e", ref + "^{commit}"], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode: + subprocess.run(["git", "-C", REPO, "fetch", "--depth=1", "origin", ref], check=True) + source = subprocess.check_output(["git", "-C", REPO, "show", ref + ":bin/traits_report.py"]) + path = os.path.join(work, "traits_report.py") + with open(path, "wb") as f: + f.write(source) + subprocess.run([sys.executable, path, *args], check=True, capture_output=True, text=True) - # 2. a planted alternate allele changes the reading - before = tr.call(lct, at((c, p, ref, ".", f"{ref}/{ref}")))[2] - after = tr.call(lct, at((c, p, ref, alt, f"{ref}/{alt}")))[2] - check("planted alt: the reading changes from hom_ref to het", before == lct["hom_ref"] and after == lct["het"] - and before != after, (before, after)) - # 3. not covered and other alleles - check("no line: not_covered", tr.call(lct, [])[1] == "not_covered") - other = next(b for b in "ACGT" if b not in (ref, alt)) - g = tr.call(lct, at((c, p, ref, other, f"{ref}/{other}"))) - check("an allele the table does not describe: other_allele, not read", g[1] == "other_allele" - and g[2] == tr.OTHER_ALLELE, g) - g = tr.call(lct, at((c, p, ref, alt, f"./{alt}"))) - check("a half call: other_allele", g[1] == "other_allele", g) - g = tr.call(lct, at((c, p, ref + "T", ref, f"{ref}T/{ref}"))) - check("a deletion starting at the position: other_allele", g[1] == "other_allele", g) - g = tr.call(lct, at((c, p, ref + "T", ref, f"{ref}T/{ref}"), (c, p, ref, alt, f"{ref}/{alt}"))) - check("a SNV record beside an indel at the position is the one read", g == (f"{ref}/{alt}", "called", lct["het"]), g) - - # 4. a REF that is not the GRCh38 base stops - try: - tr.call(lct, at((c, p, other, alt, f"{other}/{alt}"))) - check("a gVCF REF that differs from the table stops the run", False, "no error") - except tr.TableError as e: - check("a gVCF REF that differs from the table stops the run, naming the row", "rs4988235" in str(e), e) - - # 5. APOE, byte for byte the hand code's line - r4, r2 = by_id["rs429358"], by_id["rs7412"] - for (g4, g2), want in APOE_GOLDEN.items(): - lines = [(r4["chrom"], r4["pos"], "T", g4.split("/")[1], g4)] - if g2 != ".": - lines.append((r2["chrom"], r2["pos"], "C", "T", g2)) - out = tr.report(rows, recs(*lines), rules) - line = apoe_line(out) - check(f"APOE line byte for byte as before: rs429358 {g4}, rs7412 {g2} -> {line['genotype']} {line['status']}", - line_hash(line) == want, line) - out = tr.report(rows, recs((r4["chrom"], r4["pos"], "T", "C", "T/C"), (r2["chrom"], r2["pos"], "C", "T", "C/T")), - rules) - check("APOE e2/e4: says it could also be the rare e1/e3", "e1/e3" in apoe_line(out)["reading"], apoe_line(out)) - ids = [o["rsid"] for o in out] - check("APOE: the line comes right after the second APOE row", - ids.index("rs429358+rs7412") == max(ids.index("rs429358"), ids.index("rs7412")) + 1, ids) - check("APOE: one line only", ids.count("rs429358+rs7412") == 1, ids) - changed = dict(rules["APOE"], combinations=[dict(x, reading=x["reading"].replace("e3/e3.", "e3/e3!")) - for x in rules["APOE"]["combinations"]]) - out = tr.report(rows, recs((r4["chrom"], r4["pos"], "T", "C", "T/C"), (r2["chrom"], r2["pos"], "C", "T", "C/C")), - dict(rules, APOE=changed)) - check("control: one changed character in an APOE reading changes the hash", - line_hash(apoe_line(out)) != APOE_GOLDEN[("T/C", "C/C")], apoe_line(out)) - e4 = apoe_line(tr.report(rows, recs((r4["chrom"], r4["pos"], "T", "C", "C/C"), - (r2["chrom"], r2["pos"], "C", "T", "C/C")), rules)) - check("APOE e4/e4: a risk factor, not a diagnosis", "not a diagnosis" in e4["reading"], e4) - check("report() without rules reads assets/genotype_rules.tsv (other tests call it so)", - line_hash(apoe_line(tr.report(rows, {}))) == line_hash(apoe_line(tr.report(rows, {}, rules))), rows[:0]) - - # 6. the rules table's schema - errs = tr.check_rules(rules, rows) - check(f"assets/genotype_rules.tsv passes the schema check ({len(rules)} rules)", not errs, errs) - check("the APOE and TAS2R38 rules are there", {"APOE", "TAS2R38"} <= set(rules), list(rules)) - - def broken(desc, mutate, want, table=rows): - rs = {k: dict(v, sites=[dict(s) for s in v["sites"]], haplotypes=[dict(h) for h in v["haplotypes"]], - combinations=[dict(x) for x in v["combinations"]]) for k, v in rules.items()} - mutate(rs) - e = tr.check_rules(rs, table) - check(f"control: {desc} is caught", any(want in x for x in e), e) - - broken("a missing combination line", lambda rs: rs["TAS2R38"]["combinations"].pop(), "no combination line for AVI/AVI") - broken("an extra combination line", lambda rs: rs["TAS2R38"]["combinations"].append( - dict(rs["TAS2R38"]["combinations"][0], diplotypes="PAV/PAV|AVI/AVI")), "can never be read") - broken("a haplotype allele that is neither ref nor alt", - lambda rs: rs["TAS2R38"]["haplotypes"][0].update(alleles="C,C,C"), "neither its ref") - broken("diplotypes out of haplotype order", - lambda rs: rs["TAS2R38"]["combinations"][1].update(diplotypes="AVI/PAV"), "write PAV/AVI") - broken("a rule site that disagrees with its traits table row", - lambda rs: rs["TAS2R38"]["sites"][1].update(ref="A", alt="G"), "the traits table has chr7:141972905 G>A") - broken("no APOE rule", lambda rs: rs.pop("APOE"), "rule APOE is missing") - broken("a needs_phase that is not yes or no", lambda rs: rs["APOE"].update(needs_phase="maybe"), "needs_phase") - work = tempfile.mkdtemp() - try: - bad = os.path.join(work, "r.tsv") - write(bad, open(RULES).read() + "allele\tAPOE\tx\n") - try: - tr.read_rules(bad) - check("control: a line of an unknown kind is caught", False, "read without error") - except tr.TableError as e: - check("control: a line of an unknown kind is caught", "kind 'allele'" in str(e), e) - finally: - shutil.rmtree(work) - - # 7. the rules on synthetic gVCF records - def rule(rid, *lines, phased=None): - return next(x for x in tr.rule_lines(rules, recs(*lines), phased) if x["rule"] == rid) - - a4, a2 = (r4["chrom"], r4["pos"]), (r2["chrom"], r2["pos"]) - l = rule("APOE", (*a4, "T", ".", "T/T", "37"), (*a2, "C", ".", "C/C", "41")) - check("APOE, reference blocks only: e3/e3 called, with each block's GQ", - (l["result"], l["status"], l["genotypes"], l["gq"], l["phase"]) == ("e3/e3", "called", "T/T,C/C", "37,41", - "not_used"), l) - l = rule("APOE", (*a4, "T", "C", "T/C", "30"), (*a2, "C", "T", "C/T", "30")) - check("APOE, double heterozygote without phase: e2/e4 (the rule needs no phase)", - (l["result"], l["status"]) == ("e2/e4", "called"), l) - ref_blocks = [(*T1, "C", ".", "C/C", "50"), (*T2, "G", ".", "G/G", "50"), (*T3, "T", ".", "T/T", "48")] - l = rule("TAS2R38", *ref_blocks) - check("TAS2R38, reference blocks only: homozygous reference with the GQ, AAI (C,G,T) not read", - (l["status"], l["genotypes"], l["gq"], l["result"]) == ("other_haplotype", "C/C,G/G,T/T", "50,50,48", ".") - and "C,G,T" in l["reading"], l) - pav = [(*T1, "C", "G", "G/G", "40"), (*T2, "G", ".", "G/G", "40"), (*T3, "T", "C", "C/C", "40")] - l = rule("TAS2R38", *pav) - check("TAS2R38 PAV/PAV: called, no phase needed", - (l["result"], l["status"], l["phase"]) == ("PAV/PAV", "called", "not_needed"), l) - one_het = [(*T1, "C", "G", "C/G", "40"), (*T2, "G", "A", "A/A", "40"), (*T3, "T", ".", "T/T", "40")] - l = rule("TAS2R38", *one_het) - check("TAS2R38, one heterozygous site: AVI with a G,A,T copy, which the rule does not read (no phase needed)", - l["status"] == "other_haplotype" and l["phase"] == "not_needed" and "G,A,T" in l["reading"], l) - tri = [(*T1, "C", "G", "C/G", "35"), (*T2, "G", "A", "G/A", "35"), (*T3, "T", "C", "T/C", "35")] - l = rule("TAS2R38", *tri, phased={}) - check("TAS2R38 triple heterozygote, no read pair spans the sites: unphased, not read", - (l["status"], l["phase"], l["result"]) == ("unphased", "unphased", ".") - and "no read or read pair" in l["reading"], l) - l = rule("TAS2R38", *tri) - check("TAS2R38 triple heterozygote, no phasing ran: unphased, says why", - l["status"] == "unphased" and "no phasing ran" in l["reading"], l) - one_block = phased_vcf(("chr7", T3[1], "T", "C", "1|0:141972804"), ("chr7", T2[1], "G", "A", "0|1:141972804"), - ("chr7", T1[1], "C", "G", "1|0:141972804")) - l = rule("TAS2R38", *tri, phased=one_block) - check("TAS2R38 triple heterozygote phased in one block: PAV/AVI, read-backed", - (l["result"], l["status"], l["phase"]) == ("PAV/AVI", "called", "read_backed"), l) - two_blocks = phased_vcf(("chr7", T3[1], "T", "C", "1|0:141972804"), ("chr7", T2[1], "G", "A", "0|1:141972804"), - ("chr7", T1[1], "C", "G", "1|0:141973545")) - l = rule("TAS2R38", *tri, phased=two_blocks) - check("TAS2R38 triple heterozygote phased in two blocks: unphased", l["status"] == "unphased", l) - odd = phased_vcf(("chr7", T3[1], "T", "C", "0|1:141972804"), ("chr7", T2[1], "G", "A", "0|1:141972804"), - ("chr7", T1[1], "C", "G", "1|0:141972804")) - l = rule("TAS2R38", *tri, phased=odd) - check("TAS2R38 phased into G,G,T and C,A,C: a haplotype the rule does not describe", - l["status"] == "other_haplotype" and "G,G,T and C,A,C" in l["reading"], l) - l = rule("TAS2R38", *tri[:2]) - check("TAS2R38, a site not covered: not_covered, names the three sites", - l["status"] == "not_covered" and "every one of rs713598, rs1726866 and rs10246939" in l["reading"], l) - - # 8. the sites to phase, and WhatsHap's VCF read back - s = tr.phase_sites(rules, recs(*tri, (*a4, "T", "C", "T/C"), (*a2, "C", "T", "C/T"))) - check("--phase-sites: the three TAS2R38 sites, not APOE's (needs_phase no)", - sorted(x[2] for x in s) == ["rs10246939", "rs1726866", "rs713598"], s) - check("--phase-sites: nothing for one heterozygous site", tr.phase_sites(rules, recs(*one_het)) == []) - check("--phase-sites: nothing when a site is not covered", tr.phase_sites(rules, recs(*tri[:2])) == []) - check("WhatsHap's VCF: unphased records are left out", - phased_vcf(("chr7", "1", "C", "G", "0/1"), ("chr7", "2", "G", "A", "0|1:2")) == {("chr7", "2"): ("G", "A", "2")}) - work = tempfile.mkdtemp() - try: - g = os.path.join(work, "g.tsv") - write(g, "".join("\t".join(x) + "\n" for x in tri)) - vcf = os.path.join(work, "in.vcf") - res = run("--rules", RULES, "--genotypes", g, "--sample", "S", "--phase-sites", vcf) - body = [x.split("\t") for x in open(vcf).read().splitlines() if not x.startswith("#")] if res.returncode == 0 else [] - check("--phase-sites CLI: a sorted one-sample VCF of the three 0/1 sites", - [(x[1], x[2], x[9]) for x in body] == [(T3[1], "rs10246939", "0/1"), (T2[1], "rs1726866", "0/1"), - (T1[1], "rs713598", "0/1")] - and "#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\tFORMAT\tS" in open(vcf).read(), (res.stderr, body)) - - # 9. the command line - write(g, f"{c}\t{p}\t{ref}\t{alt}\t{ref}/{alt}\t30\n" + "".join("\t".join(x) + "\n" for x in tri)) - out, rout = os.path.join(work, "S_traits.tsv"), os.path.join(work, "S_genotype_rules.tsv") - ph = os.path.join(work, "phased.vcf") - write(ph, "##fileformat=VCFv4.2\n#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\tFORMAT\tS\n" - f"chr7\t{T3[1]}\t.\tT\tC\t.\tPASS\t.\tGT:PS\t1|0:9\nchr7\t{T2[1]}\t.\tG\tA\t.\tPASS\t.\tGT:PS\t0|1:9\n" - f"chr7\t{T1[1]}\t.\tC\tG\t.\tPASS\t.\tGT:PS\t1|0:9\n") - res = run("--table", TABLE, "--rules", RULES, "--genotypes", g, "--phased", ph, "--sample", "S", - "--out", out, "--rules-out", rout) - check("CLI: exits 0", res.returncode == 0, res.stderr) - lines = open(out).read().splitlines() if res.returncode == 0 else [""] - check("CLI: the header is the output columns", lines[0].split("\t") == tr.OUT_COLUMNS, lines[0]) - check("CLI: one row per table row plus the APOE line", len(lines) - 1 == len(rows) + 1, len(lines)) - row = next((l.split("\t") for l in lines[1:] if l.startswith("rs4988235\t")), []) - check("CLI: the planted het reads het", row[5:8] == [f"{ref}/{alt}", "called", lct["het"]], row) - check("CLI: a row with no line is not_covered", - next((l for l in lines if l.startswith("rs671\t")), "").split("\t")[6:7] == ["not_covered"]) - rl = open(rout).read().splitlines() if res.returncode == 0 else [""] - check("CLI: the rules table has its header and one line per rule", - rl[0].split("\t") == tr.RULE_OUT_COLUMNS and len(rl) - 1 == len(rules), rl[:1]) - t = next((x.split("\t") for x in rl if x.startswith("TAS2R38\t")), []) - check("CLI: TAS2R38 read from the phased VCF: PAV/AVI, read-backed, with the GQ", - t[6:11] == ["C/G,G/A,T/C", "35,35,35", "read_backed", "PAV/AVI", "called"], t) - bad = os.path.join(work, "bad.tsv") - write(bad, open(TABLE).read().replace("\trs671\tALDH2\tG\tA\t", "\trs671\tALDH2\tG\tG\t")) - res = run("--table", bad, "--rules", RULES, "--genotypes", g, "--sample", "S", - "--out", os.path.join(work, "x.tsv"), "--rules-out", os.path.join(work, "y.tsv")) - check("CLI: a traits table that fails the schema check exits 1, naming the row", - res.returncode == 1 and "rs671" in res.stderr and not os.path.exists(os.path.join(work, "x.tsv")), - (res.returncode, res.stderr)) - badr = os.path.join(work, "badr.tsv") - write(badr, open(RULES).read().replace("haplotype\tTAS2R38\tAVI\tC,A,T", "haplotype\tTAS2R38\tAVI\tC,A,A")) - res = run("--table", TABLE, "--rules", badr, "--genotypes", g, "--sample", "S", - "--out", os.path.join(work, "x.tsv"), "--rules-out", os.path.join(work, "y.tsv")) - check("CLI: a rules table that fails the schema check exits 1, naming the haplotype", - res.returncode == 1 and "AVI" in res.stderr and not os.path.exists(os.path.join(work, "y.tsv")), - (res.returncode, res.stderr)) - res = run("--check-table", TABLE, "--rules", RULES) - check("CLI --check-table with --rules: schema OK", res.returncode == 0 and "schema OK" in res.stdout, - (res.returncode, res.stderr)) +def og_cn(*args): + return subprocess.run([OG, "cn", *args], capture_output=True, text=True) - finally: - shutil.rmtree(work) - multi_snp_checks(rules) +def main(): + rules = {} + with open(RULES) as f: + for cells in csv.reader((line for line in f if not line.startswith("#")), delimiter="\t"): + if cells and cells[0] == "site": + rules.setdefault(cells[1], {"sites": []})["sites"].append(cells[2:]) + dbsnp_checks(rules) + if not OG: + check("OG_REQUIRED needs the og binary", not os.environ.get("OG_REQUIRED")) + print("skip: OG is not set: traits CLI and cn marker checks did not run") + return int(bool(FAILS)) + with tempfile.TemporaryDirectory() as work: + def table(records): + g = os.path.join(work, "g.tsv") + write(g, "".join("\t".join(rec) + "\n" for rec in records)) + out, rout = os.path.join(work, "traits.tsv"), os.path.join(work, "rules.tsv") + res = subprocess.run([OG, "traits", "table", "--table", TABLE, "--rules", RULES, + "--genotypes", g, "--sample", "S", "--out", out, + "--rules-out", rout], capture_output=True, text=True) + check("traits table exits 0", res.returncode == 0, res.stderr) + if res.returncode: + return [], [] + with open(out) as f: + traits = list(csv.DictReader(f, delimiter="\t")) + with open(rout) as f: + lines = list(csv.DictReader(f, delimiter="\t")) + return traits, lines + for (a, b), want in APOE_GOLDEN.items(): + records = [("chr19", pos, ref, alt, gt) for pos, ref, alt, gt in + [("44908684", "T", "C", a), ("44908822", "C", "T", b)] if gt != "."] + traits, lines = table(records) + apoe = next(r for r in traits if "+" in r["rsid"]) + got = hashlib.sha256("\t".join(apoe[k] for k in tr.OUT_COLUMNS).encode()).hexdigest() + check(f"APOE golden {a}, {b}", got == want, got) + for rid, cases in MULTI_SNP.items(): + for gts, label in cases: + records = [(s[1], s[2], s[3], s[4], gt) for s, gt in zip(rules[rid]["sites"], gts)] + _, lines = table(records) + got = next(r for r in lines if r["rule"] == rid) + check(f"{rid} {gts}: {label}", got["status"] == "called" and got["result"] == label, got) marker_checks() - - print("\nRESULT:", "ALL PASS" if FAILS == 0 else f"{FAILS} FAILED") - return 1 if FAILS or (not OG and os.environ.get("OG_REQUIRED")) else 0 + print("RESULT:", "ALL PASS" if not FAILS else f"{FAILS} FAILED") + return int(bool(FAILS)) if __name__ == "__main__": diff --git a/tests/test_traits_table.py b/tests/test_traits_table.py index 0996c911a..84cf60203 100644 --- a/tests/test_traits_table.py +++ b/tests/test_traits_table.py @@ -145,39 +145,19 @@ def online_errors(rows): return errs +def schema(path): + return subprocess.run([os.environ.get("OG", os.path.join(REPO, "bin", "og")), + "traits", "check", "--table", path], capture_output=True, text=True) + + def schema_controls(): good = tr.read_table(TABLE) - errs = tr.check_rows(good) - check(f"assets/traits.tsv passes the schema check ({len(good)} rows)", not errs, errs) check("the table has at least 30 rows", len(good) >= 30, len(good)) - - def broken(desc, mutate, want): - rows = [dict(r) for r in good] - mutate(rows) - e = tr.check_rows(rows) - check(f"control: {desc} is caught", any(want in x for x in e), e) - - broken("ref = alt", lambda rs: rs[0].update(alt=rs[0]["ref"]), "same base") - broken("a ref that is not a base", lambda rs: rs[0].update(ref="N"), "is not one of A, C, G, T") - broken("a duplicate rsID", lambda rs: rs[1].update(rsid=rs[0]["rsid"]), "appears twice") - broken("a PMID that is not a number", lambda rs: rs[0].update(pmids="PMC123"), "PubMed ids") - broken("a contig outside chr1 to chr22", lambda rs: rs[0].update(chrom="chrX"), "is not chr1 to chr22") - broken("a missing APOE row", lambda rs: rs.remove(next(r for r in rs if r["rsid"] == "rs7412")), "rs7412 is missing") - broken("an empty reading", lambda rs: rs[0].update(het=" "), "het is empty") - broken("SNPedia text", lambda rs: rs[0].update(note="From SNPedia."), "SNPedia") work = tempfile.mkdtemp() try: - bad = os.path.join(work, "t.tsv") - with open(TABLE) as f: - text = f.read() - with open(bad, "w") as f: - f.write(text.replace("\trs671\t", "\trs671\textra\t", 1)) - try: - tr.read_table(bad) - check("control: a row with an extra column is caught", False, "read without error") - except tr.TableError as e: - check("control: a row with an extra column is caught", "columns" in str(e), e) - + # Schema controls are Rust unit tests and traits parity cases. + res = schema(TABLE) + check("assets/traits.tsv passes og traits check", res.returncode == 0, res.stderr) # FASTA check on a synthetic FASTA: chr2 holds the lactase row's base. lct = next(r for r in good if r["rsid"] == "rs4988235") pos = int(lct["pos"]) @@ -212,6 +192,9 @@ def main(argv=None): ap.add_argument("--online", action="store_true", help="look the PMIDs up in PubMed and the rsIDs in dbSNP") a = ap.parse_args(argv) if a.table == TABLE and not a.fasta and not a.online: + if not os.environ.get("OG") and not os.environ.get("OG_REQUIRED"): + print("skip: OG is not set: traits schema controls run in the Rust job") + return 0 schema_controls() else: try: @@ -219,8 +202,8 @@ def main(argv=None): except tr.TableError as e: check("the table can be read", False, e) rows = [] - errs = tr.check_rows(rows) - check(f"{a.table} passes the schema check", not errs, errs) + res = schema(a.table) + check(f"{a.table} passes the schema check", res.returncode == 0, res.stderr) if a.fasta and rows: e, n = fasta_errors(a.fasta, rows) print(f" ref checked against {a.fasta}: {n} rows (the others are on contigs it lacks)") From 339b64733676314d8b5d75bc98553d9e9aea5254 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 15:11:07 +0100 Subject: [PATCH 24/45] test(traits): plant diplotype, reference and phase controls Bead: pgp-bs9u.25 --- rust/crates/og-steps/src/traits.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rust/crates/og-steps/src/traits.rs b/rust/crates/og-steps/src/traits.rs index 40c3db950..1c4d6169f 100644 --- a/rust/crates/og-steps/src/traits.rs +++ b/rust/crates/og-steps/src/traits.rs @@ -339,6 +339,7 @@ fn allowed(rule: &Rule, gt: &[Vec]) -> Vec { } } } + out.reverse(); // planted control: diplotype key order out } fn check_rules(rules: &[Rule], rows: Option<&[Site]>) -> Vec { @@ -562,7 +563,7 @@ fn read_genotypes(path: &str, sites: &[&Site]) -> Result { }; let site = sites[0]; for s in sites { - if c[2].chars().next() != s.row["ref"].chars().next() { + if c[2].is_empty() { // planted control: REF check removed return Err(format!( "{} at {}:{}: the gVCF has REF {}, the table {} (the GRCh38 base). Is the gVCF on GRCh38?", s.row["rsid"], c[0], c[1], c[2], s.row["ref"] @@ -861,6 +862,7 @@ fn phase_vcf(path: &str, sample: &str, rules: &[Rule], gt: &Genotypes) -> Result } } } + sites.clear(); // planted control: missing phase sites let mut out = AtomicFile::create(path).map_err(|e| e.to_string())?; let write = |out: &mut AtomicFile| -> std::io::Result<()> { writeln!(out, "##fileformat=VCFv4.2")?; From ab5088dc7e0ec80665fba0b074a4d756c7b37b65 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 15:14:48 +0100 Subject: [PATCH 25/45] test(traits): restore evaluation after planted controls Reverts 339b64733676314d8b5d75bc98553d9e9aea5254. Bead: pgp-bs9u.25 --- rust/crates/og-steps/src/traits.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/rust/crates/og-steps/src/traits.rs b/rust/crates/og-steps/src/traits.rs index 1c4d6169f..40c3db950 100644 --- a/rust/crates/og-steps/src/traits.rs +++ b/rust/crates/og-steps/src/traits.rs @@ -339,7 +339,6 @@ fn allowed(rule: &Rule, gt: &[Vec]) -> Vec { } } } - out.reverse(); // planted control: diplotype key order out } fn check_rules(rules: &[Rule], rows: Option<&[Site]>) -> Vec { @@ -563,7 +562,7 @@ fn read_genotypes(path: &str, sites: &[&Site]) -> Result { }; let site = sites[0]; for s in sites { - if c[2].is_empty() { // planted control: REF check removed + if c[2].chars().next() != s.row["ref"].chars().next() { return Err(format!( "{} at {}:{}: the gVCF has REF {}, the table {} (the GRCh38 base). Is the gVCF on GRCh38?", s.row["rsid"], c[0], c[1], c[2], s.row["ref"] @@ -862,7 +861,6 @@ fn phase_vcf(path: &str, sample: &str, rules: &[Rule], gt: &Genotypes) -> Result } } } - sites.clear(); // planted control: missing phase sites let mut out = AtomicFile::create(path).map_err(|e| e.to_string())?; let write = |out: &mut AtomicFile| -> std::io::Result<()> { writeln!(out, "##fileformat=VCFv4.2")?; From 0e0ba11bd114bb1c417feb8a099ab8d2ff9682d3 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 15:33:10 +0100 Subject: [PATCH 26/45] fix(traits): keep shared sites independent and stage both outputs Exercise the three named fault controls in Guard before testing the restored source. Retain the local red log as reviewable evidence. Bead: pgp-bs9u.25 --- .github/workflows/guard.yml | 23 +++ CHANGELOG.md | 2 +- rust/crates/og-steps/src/traits.rs | 155 +++++++++++++++++--- tests/e2e/rust-c2b-traits.sh | 1 + tests/parity/cases.tsv | 1 + tests/parity/evidence/rust-c2b-controls.txt | 69 +++++++++ tests/parity/gen/traits.sh | 16 +- tests/parity/remnants.tsv | 2 +- tests/test_copy_number.py | 3 +- tests/test_render_report.py | 2 - tests/test_traits_report.py | 3 +- tests/test_traits_table.py | 35 +---- 12 files changed, 252 insertions(+), 60 deletions(-) create mode 100644 tests/parity/evidence/rust-c2b-controls.txt diff --git a/.github/workflows/guard.yml b/.github/workflows/guard.yml index 7ee58a0dc..66800c3a5 100644 --- a/.github/workflows/guard.yml +++ b/.github/workflows/guard.yml @@ -279,6 +279,29 @@ jobs: grep -E 'planted_failure.*FAILED' "$log" || { cat "$log"; echo "ERROR: the run failed, but not on the planted test"; exit 1; } echo "Control: the planted test failed, as it must." + - name: Traits controls (diplotype order, REF check and missing phase rows) + env: + CARGO_BUILD_JOBS: 3 + run: | + src=crates/og-steps/src/traits.rs + saved="${RUNNER_TEMP}/traits.rs" + log="${RUNNER_TEMP}/traits-controls.log" + cp -f "$src" "$saved" + trap 'cp -f "$saved" "$src"' EXIT + perl -0pi -e 's/(fn allowed\b.*?)(\n out\n)/$1\n out.reverse();$2/s; s/if c\[2\]\.chars\(\)\.next\(\) != s\.row\["ref"\]\.chars\(\)\.next\(\)/if c[2].is_empty()/; s/(fn phase_vcf\b.*?)( let mut out = AtomicFile::create\(path\))/$1 sites.clear();\n$2/s' "$src" + cmp -s "$saved" "$src" && { echo "ERROR: no traits control was planted"; exit 1; } + if cargo test --locked -p og-steps traits::tests --no-fail-fast > "$log" 2>&1; then + cat "$log" + echo "ERROR: traits accepted the planted faults" + exit 1 + fi + cat "$log" + for test in apoe_grid_keeps_haplotype_order_and_missing_calls stream_prefers_first_snv_but_checks_every_ref_and_bounds_memory phase_requires_one_block_and_only_multiple_hets_are_exported; do + grep -E "traits::tests::${test} .*FAILED" "$log" || { echo "ERROR: $test did not catch its fault"; exit 1; } + done + cp -f "$saved" "$src" + trap - EXIT + - name: cargo test run: cargo test --workspace --locked --no-fail-fast diff --git a/CHANGELOG.md b/CHANGELOG.md index 8400d72da..b6245e959 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ Each release's notes. From 1.0.0 on, a major or minor release has hand-written n ## Unreleased -- Step 42 reads traits and genotype rules with `og traits table`, exports WhatsHap sites with `og traits phase-sites`, and checks the catalogs with `og traits check`. The replaced Python evaluator is removed. Report columns and readings stay the same. Genotypes outside the catalog are streamed past, and write errors now report a controlled error instead of a traceback. +- Step 42 reads traits and genotype rules with `og traits table`, exports WhatsHap sites with `og traits phase-sites`, and checks the catalogs with `og traits check`. The replaced Python evaluator is removed. Report columns and readings stay the same. Genotypes outside the catalog are streamed past, and write errors now report a controlled error with the path instead of a traceback. Both tables are staged before either is published, so an unwritable second output leaves the previous traits table intact. - The unused Python copy-number engine is removed. Its historical catalog tests materialise a pinned test oracle and keep testing the Rust commands. diff --git a/rust/crates/og-steps/src/traits.rs b/rust/crates/og-steps/src/traits.rs index 40c3db950..d537e672a 100644 --- a/rust/crates/og-steps/src/traits.rs +++ b/rust/crates/og-steps/src/traits.rs @@ -112,7 +112,10 @@ fn row(keys: &[&str], values: &[String]) -> Row { } fn records(path: &str) -> Result)>> { // Parse even the header as a CSV record. Comments do not enter the parser. - let mut lines = Lines::new(og_io::open(path).map_err(|e| e.to_string())?, Utf8::Strict); + let mut lines = Lines::new( + og_io::open(path).map_err(|e| og_filters::os_error(&e, path))?, + Utf8::Strict, + ); let mut text = String::new(); let mut number = 0; let mut out = Vec::new(); @@ -534,13 +537,48 @@ fn check_rules(rules: &[Rule], rows: Option<&[Site]>) -> Vec { e } +// A locus may be reused by rules with different alternate alleles. The +// selected record is shared, but each site's allele vocabulary is its own. +fn genotype(site: &Site, records: &Genotypes) -> SiteGt { + let mut gt = records.get(&site.key()).cloned().unwrap_or_default(); + if gt.status == Status::NotCovered { + return gt; + } + gt.alleles.sort_by_key(|a| { + ( + if a == &site.row["ref"] { + 0 + } else if a == &site.row["alt"] { + 1 + } else { + 2 + }, + a.clone(), + ) + }); + gt.status = if gt.alleles.len() == 2 + && gt + .alleles + .iter() + .all(|a| a == &site.row["ref"] || a == &site.row["alt"]) + { + Status::Called + } else { + Status::OtherAllele + }; + gt +} + fn read_genotypes(path: &str, sites: &[&Site]) -> Result { let mut wanted: HashMap> = HashMap::new(); for s in sites { wanted.entry(s.key()).or_default().push(s); } let mut result = HashMap::new(); - let mut lines = Lines::new(og_io::open(path).map_err(|e| e.to_string())?, Utf8::Strict); + let mut lines = Lines::new( + og_io::open(path).map_err(|e| og_filters::os_error(&e, path))?, + Utf8::Strict, + ); let mut line = String::new(); let mut n = 0; while lines.read_line(&mut line).map_err(|e| e.to_string())? { @@ -613,7 +651,10 @@ fn read_genotypes(path: &str, sites: &[&Site]) -> Result { Ok(result) } fn read_phased(path: &str, wanted: &HashSet) -> Result { - let mut lines = Lines::new(og_io::open(path).map_err(|e| e.to_string())?, Utf8::Strict); + let mut lines = Lines::new( + og_io::open(path).map_err(|e| og_filters::os_error(&e, path))?, + Utf8::Strict, + ); let mut text = String::new(); let mut out = HashMap::new(); while lines.read_line(&mut text).map_err(|e| e.to_string())? { @@ -657,11 +698,7 @@ fn read_phased(path: &str, wanted: &HashSet) -> Result { Ok(out) } fn rule_line(r: &Rule, gt: &Genotypes, phased: Option<&Phased>) -> Row { - let g: Vec<_> = r - .sites - .iter() - .map(|s| gt.get(&s.key()).cloned().unwrap_or_default()) - .collect(); + let g: Vec<_> = r.sites.iter().map(|s| genotype(s, gt)).collect(); let rs: Vec<_> = r.sites.iter().map(|s| s.row["rsid"].as_str()).collect(); let mut out = r.meta.clone(); for (k, v) in [ @@ -818,20 +855,22 @@ fn rule_line(r: &Rule, gt: &Genotypes, phased: Option<&Phased>) -> Row { ), } } -fn write_tsv(path: &str, columns: &[&str], rows: &[Row]) -> Result<()> { - let mut file = AtomicFile::create(path).map_err(|e| e.to_string())?; +fn stage_tsv(path: &str, columns: &[&str], rows: &[Row]) -> Result { + let mut file = AtomicFile::create(path).map_err(|e| format!("cannot write {path}: {e}"))?; { let mut writer = Writer::new(&mut file, '\t'); writer .write_row(columns.iter().copied()) - .map_err(|e| e.to_string())?; + .map_err(|e| format!("cannot write {path}: {e}"))?; for row in rows { writer .write_row(columns.iter().map(|k| row[*k].as_str())) - .map_err(|e| e.to_string())?; + .map_err(|e| format!("cannot write {path}: {e}"))?; } } - file.commit().map_err(|e| e.to_string()) + file.flush() + .map_err(|e| format!("cannot write {path}: {e}"))?; + Ok(file) } fn phase_vcf(path: &str, sample: &str, rules: &[Rule], gt: &Genotypes) -> Result { let mut sites = BTreeMap::new(); @@ -839,7 +878,10 @@ fn phase_vcf(path: &str, sample: &str, rules: &[Rule], gt: &Genotypes) -> Result let calls: Option> = r .sites .iter() - .map(|s| gt.get(&s.key()).filter(|g| g.status == Status::Called)) + .map(|s| { + let g = genotype(s, gt); + (g.status == Status::Called).then_some(g) + }) .collect(); if let Some(calls) = calls { let het: Vec<_> = r @@ -861,7 +903,7 @@ fn phase_vcf(path: &str, sample: &str, rules: &[Rule], gt: &Genotypes) -> Result } } } - let mut out = AtomicFile::create(path).map_err(|e| e.to_string())?; + let mut out = AtomicFile::create(path).map_err(|e| format!("cannot write {path}: {e}"))?; let write = |out: &mut AtomicFile| -> std::io::Result<()> { writeln!(out, "##fileformat=VCFv4.2")?; for chrom in sites.keys().map(|(c, _)| c).collect::>() { @@ -884,8 +926,9 @@ fn phase_vcf(path: &str, sample: &str, rules: &[Rule], gt: &Genotypes) -> Result } Ok(()) }; - write(&mut out).map_err(|e| e.to_string())?; - out.commit().map_err(|e| e.to_string())?; + write(&mut out).map_err(|e| format!("cannot write {path}: {e}"))?; + out.commit() + .map_err(|e| format!("cannot write {path}: {e}"))?; Ok(sites.len()) } fn run(verb: &str, opts: &HashMap) -> Result { @@ -968,7 +1011,7 @@ fn run(verb: &str, opts: &HashMap) -> Result { let mut out = vec![]; let mut counts = BTreeMap::::new(); for (i, s) in rows.iter().enumerate() { - let g = gt.get(&s.key()).cloned().unwrap_or_default(); + let g = genotype(s, >); let mut r = s.row.clone(); r.insert( "genotype".into(), @@ -1003,8 +1046,16 @@ fn run(verb: &str, opts: &HashMap) -> Result { } } // Compute both tables before opening either output. Each rename is atomic. - write_tsv(required("out")?, OUT, &out)?; - write_tsv(required("rules-out")?, RULE_OUT, &lines)?; + let out_path = required("out")?; + let rules_path = required("rules-out")?; + let traits_file = stage_tsv(out_path, OUT, &out)?; + let rules_file = stage_tsv(rules_path, RULE_OUT, &lines)?; + traits_file + .commit() + .map_err(|e| format!("cannot write {out_path}: {e}"))?; + rules_file + .commit() + .map_err(|e| format!("cannot write {rules_path}: {e}"))?; for r in &out { *counts.entry(r["status"].clone()).or_default() += 1; } @@ -1284,6 +1335,29 @@ mod tests { let path = scratch.put("many", &"chr99\t1\tN\t.\tN/N\n".repeat(10000)); assert!(read_genotypes(&path, &[s]).unwrap().is_empty()); } + #[test] + fn shared_position_keeps_each_rules_allele_vocabulary() { + let (rows, _) = assets(); + let first = &rows[0]; + let mut second = first.clone(); + let alt = ["A", "C", "G", "T"] + .into_iter() + .find(|a| *a != first.row["ref"] && *a != first.row["alt"]) + .unwrap(); + second.row.insert("alt".into(), alt.into()); + let scratch = Scratch::new(); + let path = scratch.put( + "g", + &format!( + "{}\t{}\t{}\t{}\t{}/{}\n", + first.row["chrom"], first.row["pos"], first.row["ref"], alt, first.row["ref"], alt + ), + ); + let gt = read_genotypes(&path, &[first, &second]).unwrap(); + assert_eq!(genotype(first, >).status, Status::OtherAllele); + assert_eq!(genotype(&second, >).status, Status::Called); + } + #[test] fn phase_requires_one_block_and_only_multiple_hets_are_exported() { let (_, rules) = assets(); @@ -1322,6 +1396,45 @@ mod tests { let gt = genotypes(r, &[0, 0, 0]); assert_eq!(rule_line(r, >, None)["phase"], "not_needed"); } + #[test] + fn unwritable_rules_output_keeps_existing_traits_table() { + let scratch = Scratch::new(); + let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../../assets"); + let target = scratch.put("out", "previous complete table\n"); + let gt = scratch.put("g", ""); + let opts = [ + ( + "table", + root.join("traits.tsv").to_string_lossy().into_owned(), + ), + ( + "rules", + root.join("genotype_rules.tsv") + .to_string_lossy() + .into_owned(), + ), + ("genotypes", gt), + ("sample", "S".into()), + ("out", target.clone()), + ( + "rules-out", + scratch + .0 + .join("missing/rules.tsv") + .to_string_lossy() + .into_owned(), + ), + ] + .into_iter() + .map(|(k, v)| (k.into(), v)) + .collect(); + assert!(run("table", &opts).unwrap_err().contains("cannot write")); + assert_eq!( + std::fs::read_to_string(target).unwrap(), + "previous complete table\n" + ); + } + #[test] fn csv_header_unicode_rules_and_atomic_write_errors() { let scratch = Scratch::new(); @@ -1335,7 +1448,7 @@ mod tests { assert!(read_table(&path).unwrap().is_empty()); let path = scratch.put("r", "rule\té\tG\tT\tno\tyes\tN\t1\n"); assert!(!check_rules(&read_rules(&path).unwrap(), None).is_empty()); - assert!(write_tsv(scratch.0.join("absent/x").to_str().unwrap(), OUT, &[]).is_err()); + assert!(stage_tsv(scratch.0.join("absent/x").to_str().unwrap(), OUT, &[]).is_err()); assert_eq!(cli(&["table".into()]), 2); assert_eq!( cli(&[ diff --git a/tests/e2e/rust-c2b-traits.sh b/tests/e2e/rust-c2b-traits.sh index 71613e72f..e5e2e3852 100755 --- a/tests/e2e/rust-c2b-traits.sh +++ b/tests/e2e/rust-c2b-traits.sh @@ -12,6 +12,7 @@ check_eq 'traits table exits 0' "$?" 0 check_eq 'APOE double heterozygote is e2/e4' "$(awk -F'\t' '$1 == "APOE" {print $10}' "${CASE_TMP}/rules.tsv")" e2/e4 check_eq 'the taster rule needs phase' "$(awk -F'\t' '$1 == "TAS2R38" {print $11}' "${CASE_TMP}/rules.tsv")" unphased check_eq 'APOE line occurs exactly once' "$(grep -c '^rs429358+rs7412' "${CASE_TMP}/traits.tsv")" 1 +# shellcheck disable=SC2016 # awk field references check 'absent LCT has no inferred genotype' awk -F'\t' '$1 == "rs4988235" {ok = ($6 == "." && $7 == "not_covered")} END {exit !ok}' "${CASE_TMP}/traits.tsv" "$OG" traits phase-sites --rules "$RULES" --genotypes "$G" --sample S --out "${CASE_TMP}/phase.vcf" check_eq 'phase-sites exits 0' "$?" 0 diff --git a/tests/parity/cases.tsv b/tests/parity/cases.tsv index 2a2fe1e34..70faade6e 100644 --- a/tests/parity/cases.tsv +++ b/tests/parity/cases.tsv @@ -977,3 +977,4 @@ traits sites-single gen:traits.sh phase-sites --rules rules.tsv --genotypes sing traits check gen:traits.sh check --table traits.tsv --rules rules.tsv - bytes PYTHON_IMAGE traits bad-table gen:traits.sh check --table bad-table.tsv --rules rules.tsv - bytes PYTHON_IMAGE traits bad-rules gen:traits.sh check --table traits.tsv --rules bad-rules.tsv - bytes PYTHON_IMAGE +traits shared-vocabulary gen:traits.sh table --table traits.tsv --rules shared-rules.tsv --genotypes shared.tsv --sample S --out traits-out.tsv --rules-out rules-out.tsv traits-out.tsv,rules-out.tsv tsv PYTHON_IMAGE diff --git a/tests/parity/evidence/rust-c2b-controls.txt b/tests/parity/evidence/rust-c2b-controls.txt new file mode 100644 index 000000000..bed580b72 --- /dev/null +++ b/tests/parity/evidence/rust-c2b-controls.txt @@ -0,0 +1,69 @@ +Local execution of the Guard step "Traits controls", Rust 1.93.0. +Command: cargo test --locked -p og-steps traits::tests --no-fail-fast +Faults: reverse the diplotype key list; remove the genotype REF guard; +clear the phase-site list before writing the VCF. +The step requires each of the three named regression tests to fail, then +restores the source. A compiler failure alone does not satisfy the control. + +running 8 tests +test traits::tests::csv_header_unicode_rules_and_atomic_write_errors ... ok +test traits::tests::apoe_grid_keeps_haplotype_order_and_missing_calls ... FAILED +test traits::tests::stream_prefers_first_snv_but_checks_every_ref_and_bounds_memory ... FAILED +test traits::tests::shared_position_keeps_each_rules_allele_vocabulary ... ok +test traits::tests::phase_requires_one_block_and_only_multiple_hets_are_exported ... FAILED +test traits::tests::unwritable_rules_output_keeps_existing_traits_table ... FAILED +test traits::tests::committed_catalogs_and_every_rule_genotype ... FAILED +test traits::tests::schema_faults_are_refused_before_enumeration ... ok + +failures: + +---- traits::tests::apoe_grid_keeps_haplotype_order_and_missing_calls stdout ---- + +thread 'traits::tests::apoe_grid_keeps_haplotype_order_and_missing_calls' (4288523) panicked at crates/og-steps/src/traits.rs:1242:17: +assertion `left == right` failed + left: "other_haplotype" + right: "called" +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +---- traits::tests::stream_prefers_first_snv_but_checks_every_ref_and_bounds_memory stdout ---- + +thread 'traits::tests::stream_prefers_first_snv_but_checks_every_ref_and_bounds_memory' (4288529) panicked at crates/og-steps/src/traits.rs:1336:45: +called `Result::unwrap_err()` on an `Ok` value: {("chr2", "135851076"): SiteGt { alleles: ["G", "A"], status: Called, gq: "42", snv: true }} + +---- traits::tests::phase_requires_one_block_and_only_multiple_hets_are_exported stdout ---- + +thread 'traits::tests::phase_requires_one_block_and_only_multiple_hets_are_exported' (4288526) panicked at crates/og-steps/src/traits.rs:1390:9: +assertion `left == right` failed + left: 0 + right: 3 + +---- traits::tests::unwritable_rules_output_keeps_existing_traits_table stdout ---- +ERROR: /Volumes/Data/repos/personal/.worktrees/pgp-rust-c2b/rust/crates/og-steps/../../../assets/genotype_rules.tsv: rule APOE (line 19): no combination line for e2/e4|e1/e3 +ERROR: /Volumes/Data/repos/personal/.worktrees/pgp-rust-c2b/rust/crates/og-steps/../../../assets/genotype_rules.tsv: rule APOE (line 19): the combination line e1/e3|e2/e4 can never be read (a rule that needs phase reads one diplotype) + +thread 'traits::tests::unwritable_rules_output_keeps_existing_traits_table' (4288530) panicked at crates/og-steps/src/traits.rs:1433:37: +called `Result::unwrap_err()` on an `Ok` value: 1 + +---- traits::tests::committed_catalogs_and_every_rule_genotype stdout ---- + +thread 'traits::tests::committed_catalogs_and_every_rule_genotype' (4288524) panicked at crates/og-steps/src/traits.rs:1216:9: +assertion `left == right` failed + left: ["rule APOE (line 19): no combination line for e2/e4|e1/e3", "rule APOE (line 19): the combination line e1/e3|e2/e4 can never be read (a rule that needs phase reads one diplotype)"] + right: [] + + +failures: + traits::tests::apoe_grid_keeps_haplotype_order_and_missing_calls + traits::tests::committed_catalogs_and_every_rule_genotype + traits::tests::phase_requires_one_block_and_only_multiple_hets_are_exported + traits::tests::stream_prefers_first_snv_but_checks_every_ref_and_bounds_memory + traits::tests::unwritable_rules_output_keeps_existing_traits_table + +test result: FAILED. 3 passed; 5 failed; 0 ignored; 0 measured; 91 filtered out; finished in 0.01s + +error: test failed, to rerun pass `-p og-steps --lib` +error: 1 target failed: + `-p og-steps --lib` +test traits::tests::apoe_grid_keeps_haplotype_order_and_missing_calls ... FAILED +test traits::tests::stream_prefers_first_snv_but_checks_every_ref_and_bounds_memory ... FAILED +test traits::tests::phase_requires_one_block_and_only_multiple_hets_are_exported ... FAILED diff --git a/tests/parity/gen/traits.sh b/tests/parity/gen/traits.sh index a57771388..227660390 100644 --- a/tests/parity/gen/traits.sh +++ b/tests/parity/gen/traits.sh @@ -2,8 +2,8 @@ # Public catalogs and synthetic genotype grids for all three traits verbs. set -euo pipefail root=$(cd "$(dirname "$0")/../../.." && pwd) -cp "$root/assets/traits.tsv" traits.tsv -cp "$root/assets/genotype_rules.tsv" rules.tsv +cp -f "$root/assets/traits.tsv" traits.tsv +cp -f "$root/assets/genotype_rules.tsv" rules.tsv cat > oracle.sh <<'EOF' prog=$1; shift mode=$1; shift @@ -40,3 +40,15 @@ printf 'chr7\t141973545\t.\tC\tG\t.\tPASS\t.\tGT:PS\t1|0:7\nchr7\t141972905\t.\t sed '1s/:7$/:8/' phased.vcf > split.vcf awk -F'\t' -v OFS='\t' '$1 == "combination" && $2 == "APOE" && $3 == "e3/e3" {next} {print}' rules.tsv > bad-rules.tsv awk -F'\t' -v OFS='\t' '$3 == "rs671" {$6=$5} {print}' traits.tsv > bad-table.tsv +cp -f rules.tsv shared-rules.tsv +cat >> shared-rules.tsv <<'TSV' +rule SHARE SHARE Shared position test no no Synthetic alternate allele vocabulary. 1 +site SHARE rs999999991 chr19 44908684 T G +site SHARE rs999999992 chr19 44908822 C T +haplotype SHARE h0 T,C +haplotype SHARE h1 G,T +combination SHARE h0/h0 normal Reference pair. +combination SHARE h0/h1 carrier One alternate copy. +combination SHARE h1/h1 alternate Two alternate copies. +TSV +printf 'chr19\t44908684\tT\tG\tT/G\t40\nchr19\t44908822\tC\tT\tC/T\t40\n' > shared.tsv diff --git a/tests/parity/remnants.tsv b/tests/parity/remnants.tsv index 084de768c..268a9ce99 100644 --- a/tests/parity/remnants.tsv +++ b/tests/parity/remnants.tsv @@ -9,4 +9,4 @@ bin/pgx_outside_calls.py HLA_GENES,NO_CALL,PROBABLE,NOT_CALLABLE,LOW_GQ,is_call, bin/paraphase_report.py SMN1_EXON7,parascopy_smn1,compare_smn1 bin/collect_summary.py,bin/couple_report.py,tests/test_paralog_loci.py,tests/test_paraphase_report.py R4 bin/cnvpytor_to_vcf.py DEPTH_MIN_SIZE,delly_depth_contradicts bin/couple_report.py R4 bin/annotsv_postfilter.py CONSENSUS_BP,KEEP,ADDED,open_text,chrom_key,sv_type,info_value,sample_format,sample_columns,read_consensus,consensus_support bin/collect_summary.py,tests/demo/make_demo_sample.py R3 -bin/traits_report.py COLUMNS,OUT_COLUMNS,RULE_OUT_COLUMNS,TRAITS_TABLE_RULES,TableError,read_table,MARKERS,MARKER_STATUSES bin/collect_summary.py,tests/demo/make_demo_sample.py,tests/test_traits_report.py,tests/test_traits_table.py,tests/test_collect_summary.py,tests/test_lpa_kiv2.py,tests/test_render_report.py R3 +bin/traits_report.py COLUMNS,OUT_COLUMNS,RULE_OUT_COLUMNS,TRAITS_TABLE_RULES,TableError,read_table,MARKERS,MARKER_STATUSES bin/collect_summary.py,tests/demo/make_demo_sample.py,tests/test_traits_report.py,tests/test_traits_table.py,tests/test_collect_summary.py,tests/test_lpa_kiv2.py R3 diff --git a/tests/test_copy_number.py b/tests/test_copy_number.py index 03a4bf349..3cb0eb2da 100644 --- a/tests/test_copy_number.py +++ b/tests/test_copy_number.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 """og cn and bin/cn_loci.tsv: one depth-ratio engine for every copy-number -locus, and what bin/copy_number.py keeps (the catalog reader and assess(), -which step 41's report imports). +locus, compared with the pinned historical catalog reader and assess() oracle. Without og (OG unset) the checks of the catalog and of assess() run, and the wiring; with OG naming the og binary (OG=rust/target/release/og, or bin/og) diff --git a/tests/test_render_report.py b/tests/test_render_report.py index a8377a4d6..beb0497c0 100644 --- a/tests/test_render_report.py +++ b/tests/test_render_report.py @@ -954,9 +954,7 @@ def mosaic_table(rows): # 11. traits card import time - import traits_report d = f"{work}/traits/S" - rows = traits_report.read_table(os.path.join(REPO, "assets", "traits.tsv")) put(f"{d}/traits/S_traits.tsv", "") from test_traits_report import traits_tables g = os.path.join(work, "traits_empty.tsv") diff --git a/tests/test_traits_report.py b/tests/test_traits_report.py index a10e13f47..2414935bc 100644 --- a/tests/test_traits_report.py +++ b/tests/test_traits_report.py @@ -105,8 +105,7 @@ def planted(desc, new, want): "MAPT": [(("G/G", "G/G"), "H2/H2"), (("A/G", "A/G"), "H1/H2")], } # The label of the reference blocks alone (every site homozygous for the GRCh38 base). -MULTI_SNP_REF = {"APOL1": "G0/G0", "SERPINA1": "PiMM", "HFE": "wt/wt", "GC": "Gc1F/Gc1F", - "CAD9p21": "no risk copy", "MAPT": "H1/H1"} + def says_not_proof(message): diff --git a/tests/test_traits_table.py b/tests/test_traits_table.py index 84cf60203..3d2566b27 100644 --- a/tests/test_traits_table.py +++ b/tests/test_traits_table.py @@ -1,33 +1,10 @@ #!/usr/bin/env python3 -"""assets/traits.tsv, the curated table of step 42: its schema, its reference -bases and its sources. - - python3 tests/test_traits_table.py schema (the unit-tests job) - python3 tests/test_traits_table.py --fasta REF.fasta also: each row's ref is the - FASTA's base, on every contig - the FASTA has (needs REF.fasta.fai) - python3 tests/test_traits_table.py --online also: every PMID resolves in - PubMed, and every rsID's GRCh38 - position, reference base and - alt allele are dbSNP's (curl) - --table FILE checks another table (the e2e case plants a broken copy). - -The e2e case traits-panel-1 runs it with --fasta (the fixture's GRCh38 -reference) and --online, and on a planted copy with one wrong reference base -and one PubMed id that does not exist, which must fail. - -Checks without options: - 1. the table passes bin/traits_report.py's schema check (columns, chr1 to - chr22, positions, one base each for ref and alt, readings, ancestry - yes/no, PubMed ids, the two APOE rows, no SNPedia text); - 2. planted problems are each caught: a wrong column count, ref = alt, a - non-base allele, a duplicate rsID, a PMID that is not a number, a - missing APOE row; - 3. the FASTA check on a three-base synthetic FASTA: the right base passes, - a planted wrong base fails, a contig the FASTA lacks is counted as not - checked. - -Run: python3 tests/test_traits_table.py +"""Validate the curated traits catalog and its GRCh38 reference bases. + +Schema checks use og traits check. Rust unit tests plant invalid catalog +rows and rules. The optional --fasta and --online checks also verify the +FASTA bases, PubMed sources and dbSNP records. A synthetic FASTA provides +positive, wrong-base and missing-contig controls without network access. """ import argparse import json From 853dda63ca5dbd14eb8ab2e86ee5b63cdfd8146f Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 15:39:11 +0100 Subject: [PATCH 27/45] docs(traits): describe the removed copy-number test oracle Bead: pgp-bs9u.25 --- docs/cn-loci.md | 2 +- docs/rust.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/cn-loci.md b/docs/cn-loci.md index eb6f0a44e..247ffc02c 100644 --- a/docs/cn-loci.md +++ b/docs/cn-loci.md @@ -1,6 +1,6 @@ # Copy Number from Depth (the locus catalog) -Several steps count copies of a gene from short-read depth: HBA1 and HBA2 in step 35, the RHD deletion in step 41 and the LPA KIV-2 repeat in step 46. Step 35 also counts three gene-dosage loci that no pinned caller models (HP, LCE3B/LCE3C and CCL3L1), CYP21A2 from depth and its pseudogene variant sites, and the interval rows: the recurrent CNV regions ClinGen curates (22q11.2, 1q21.1, 15q11.2, 16p11.2, 17p12 and the rest), five carrier deletion alleles and the Y chromosome AZF regions. They all use one table, `bin/cn_loci.tsv`, with one row per locus, and one engine, `og cn` (the Rust crate `rust/crates/og-cn`, run through `bin/og`; [docs/rust.md](rust.md)). Step 41 uses the same engine: `og cn bed` gives it the RHD regions, and `og bloodtype report` makes the RhD call with og cn's rules on the RHD row. `bin/copy_number.py` stays only for the catalog tests. A new locus is a new row, not a new script. +Several steps count copies of a gene from short-read depth: HBA1 and HBA2 in step 35, the RHD deletion in step 41 and the LPA KIV-2 repeat in step 46. Step 35 also counts three gene-dosage loci that no pinned caller models (HP, LCE3B/LCE3C and CCL3L1), CYP21A2 from depth and its pseudogene variant sites, and the interval rows: the recurrent CNV regions ClinGen curates (22q11.2, 1q21.1, 15q11.2, 16p11.2, 17p12 and the rest), five carrier deletion alleles and the Y chromosome AZF regions. They all use one table, `bin/cn_loci.tsv`, with one row per locus, and one engine, `og cn` (the Rust crate `rust/crates/og-cn`, run through `bin/og`; [docs/rust.md](rust.md)). Step 41 uses the same engine: `og cn bed` gives it the RHD regions, and `og bloodtype report` makes the RhD call with og cn's rules on the RHD row. The catalog tests materialise the historical Python engine from its pinned Git commit as a differential oracle. A new locus is a new row, not a new script. The table sits in `bin/` because `bin/` is the one repository folder that every container mounts: the bash steps mount it as `/pgp-bin`, and Nextflow puts it on every task's PATH. diff --git a/docs/rust.md b/docs/rust.md index 99637ea75..728a2abd2 100644 --- a/docs/rust.md +++ b/docs/rust.md @@ -37,7 +37,7 @@ bin/og the shim tasks call; runs bin/.og/og-linux- (og-da tests/rust/ the differential test against CPython, the image test ``` -Each ported tool becomes a subcommand named after its file in `bin/` without `.py`, in kebab-case. Today `og` has `og qc sex-loss` (step 33), `og version`, `og --tree-hash`, `og map`, `og setup mask-fasta`, `og run state|tasks|eta|headroom|delta`, `og dev parity-compare`, `og pgx cyp2d6-bed|cyp2d6-measure|cyp2d6-check`, `og str motif|catalog`, `og cdna check|scan` (were `clip_check.py` and `cdna_scan.py`; `clip_check.py` keeps only the names `collect_summary.py` imports), `og comphet phase` (was `comphet_phase.py`), `og bam contigs`, `og setup unzip|genetic-map` and two hidden ones the tests drive: `og pycompat-probe` and `og bam-fetch-probe`. The filters of steps 04, 05 and 07 are grouped by step: `og sv svlen-fix` and `og sv inv-filter` (were `sv_svlen_fix.py` and `manta_inv_filter.py`), `og pgx refblock-fill` and `og pgx mito-merge` (were `pgx_refblock_fill.py` and `pgx_mito_merge.py`). Step 05 builds AnnotSV's input with `og sv add-callers` and checks its class 4 and 5 calls with `og sv postfilter` (were `cnvpytor_to_vcf.py` and `annotsv_postfilter.py`); both apply one read-depth rule for long Delly calls (`og-steps::sv`). `cnvpytor_to_vcf.py` keeps only the Python copy of that rule, which step 39's couple screen still imports, and `annotsv_postfilter.py` only the column names and the consensus join the reports import. `og str catalog` (was `strchive_convert.py`) builds the STRchive catalog of steps 09 and 9b; `og setup unzip` and `og setup genetic-map` replace two small Python programs inside `setup.sh`. Step 25 has `og prs format` (a port of `collect_summary.py prs-format`, named by what it works on) and `og prs table` (was `prs-table`). Step 33 has `og qc sample` (was `sample-qc`), and step 20 has `og mito annotate|deletions|copies` (were `mito-annotate`, `mito-deletions` and `mito-copies`). Steps 23 and 31 use `og vcf csq-rank` and `og table constraint-join` (ports of the awk programs `bin/csq_rank.awk` and `bin/constraint_join.awk`, named by what they work on). Step 27 uses `og pgx cpic-report` and `og pgx step36-outside` (were the two commands of `pgx_parse.py`). Step 49 runs `og chip prepare`, `og chip report` and `og chip check-table` (the commands of `chip_report.py`, which keeps only the detection limit and the column names the reports import). Steps 32, 35, 41 and 46 run `og cn bed`, `og cn call`, `og cn intervals`, `og cn check-bam`, `og cn marker-regions` and `og cn marker-read` ([Copy number from depth](cn-loci.md)); they replace `hba_copy_number.py`, `lpa_kiv2.py`, the commands of `copy_number.py` (which keeps the catalog reader the catalog tests import) and the marker reader of `traits_report.py` (which keeps `MARKERS` for the summary). Step 41 runs `og bloodtype report` (was `bloodtype_report.py`), which reads the marker sites through the VCF's index. Step 26b's six commands are `og ancestry-fine panel-map|check-map|order|sample-bed|chroms|report`; `bin/ancestry_fine_report.py` keeps only the names `collect_summary.py` and the docs demo import. Step 40 runs `og consumer build-sites`, `og consumer export` and `og consumer check` (were the three commands of `export_consumer.py`). Step 38 and the Nextflow family workflow run `og family plan`, `prepare`, `check`, `report`, `segments` and `ibd` (were `family_report.py`, `family_segments.py` and `ibd_report.py`). `og family report` reads the joint VCF once, compresses each child's inheritance table on its own thread, and stops with exit 2 on a joint VCF that is not sorted. The opt-in steps read their tool's output with `og y str-bed|str-report` (45), `og viral report` (47), `og exomiser prepare|report` (48), `og mei report|annotsv-input` (50), `og mocha report` (51), `og vntyper report` (52) and `og locityper report` (53); they replaced `ystr_report.py`, `viral_report.py`, `exomiser_report.py`, `mei_report.py`, `mocha_report.py`, `vntyper_report.py` and `locityper_report.py`. Step 37 places the sample on the current YFull YTree with `og y tree-positions` and `og y tree-place` (were the two commands of `y_tree_place.py`), lists its private SNPs with `og y private-snps` (was `y_private_snps.py`) and writes Yleaf's marker positions with `og y yleaf-positions` (was `yleaf_run.py positions`; `yleaf_run.py predict` stays Python, as glue inside Yleaf's own code). Steps 08, 32 and 36 run `og pgx hla-concordance`, `og pgx pypgx-summary` and `og pgx outside-calls` (were the three commands of `pgx_outside_calls.py`, which keeps the CYP2D6 and HLA rules the reports import), and step 35b runs `og paraphase check-bam|table` (were `paraphase_report.py`, which keeps the SMN1 rule beside Parascopy). Both write their two output files together or not at all. `scripts/owngenome-lookup.sh` runs `og lookup query` ([genotype lookup](lookup.md)), which answers every query of the command line in one process; `og lookup resolve` and `og lookup report` are its two halves, with the command lines of `genome_lookup.py`, which keeps only the query reading and the names rule that `results_index.py` imports. `og recheck` replaced `owngenome_recheck.py` ([Newer databases since a run](recheck.md)); the file stays one release as a shim that runs it, for crontabs. `og dev freshness` replaced `scripts/ci/freshness.py`, the monthly freshness issue of `freshness.yml`. Both ask the network through the host's `curl` (the config on stdin, so no token reaches the process list), which keeps a TLS stack out of the static binary and its cross-builds. +Each ported tool becomes a subcommand named after its file in `bin/` without `.py`, in kebab-case. Today `og` has `og qc sex-loss` (step 33), `og version`, `og --tree-hash`, `og map`, `og setup mask-fasta`, `og run state|tasks|eta|headroom|delta`, `og dev parity-compare`, `og pgx cyp2d6-bed|cyp2d6-measure|cyp2d6-check`, `og str motif|catalog`, `og cdna check|scan` (were `clip_check.py` and `cdna_scan.py`; `clip_check.py` keeps only the names `collect_summary.py` imports), `og comphet phase` (was `comphet_phase.py`), `og bam contigs`, `og setup unzip|genetic-map` and two hidden ones the tests drive: `og pycompat-probe` and `og bam-fetch-probe`. The filters of steps 04, 05 and 07 are grouped by step: `og sv svlen-fix` and `og sv inv-filter` (were `sv_svlen_fix.py` and `manta_inv_filter.py`), `og pgx refblock-fill` and `og pgx mito-merge` (were `pgx_refblock_fill.py` and `pgx_mito_merge.py`). Step 05 builds AnnotSV's input with `og sv add-callers` and checks its class 4 and 5 calls with `og sv postfilter` (were `cnvpytor_to_vcf.py` and `annotsv_postfilter.py`); both apply one read-depth rule for long Delly calls (`og-steps::sv`). `cnvpytor_to_vcf.py` keeps only the Python copy of that rule, which step 39's couple screen still imports, and `annotsv_postfilter.py` only the column names and the consensus join the reports import. `og str catalog` (was `strchive_convert.py`) builds the STRchive catalog of steps 09 and 9b; `og setup unzip` and `og setup genetic-map` replace two small Python programs inside `setup.sh`. Step 25 has `og prs format` (a port of `collect_summary.py prs-format`, named by what it works on) and `og prs table` (was `prs-table`). Step 33 has `og qc sample` (was `sample-qc`), and step 20 has `og mito annotate|deletions|copies` (were `mito-annotate`, `mito-deletions` and `mito-copies`). Steps 23 and 31 use `og vcf csq-rank` and `og table constraint-join` (ports of the awk programs `bin/csq_rank.awk` and `bin/constraint_join.awk`, named by what they work on). Step 27 uses `og pgx cpic-report` and `og pgx step36-outside` (were the two commands of `pgx_parse.py`). Step 49 runs `og chip prepare`, `og chip report` and `og chip check-table` (the commands of `chip_report.py`, which keeps only the detection limit and the column names the reports import). Steps 32, 35, 41 and 46 run `og cn bed`, `og cn call`, `og cn intervals`, `og cn check-bam`, `og cn marker-regions` and `og cn marker-read` ([Copy number from depth](cn-loci.md)); they replace `hba_copy_number.py`, `lpa_kiv2.py`, `copy_number.py` (the catalog tests materialise its pinned historical version as a differential oracle) and the marker reader of `traits_report.py` (which keeps `MARKERS` for the summary). Step 41 runs `og bloodtype report` (was `bloodtype_report.py`), which reads the marker sites through the VCF's index. Step 26b's six commands are `og ancestry-fine panel-map|check-map|order|sample-bed|chroms|report`; `bin/ancestry_fine_report.py` keeps only the names `collect_summary.py` and the docs demo import. Step 40 runs `og consumer build-sites`, `og consumer export` and `og consumer check` (were the three commands of `export_consumer.py`). Step 38 and the Nextflow family workflow run `og family plan`, `prepare`, `check`, `report`, `segments` and `ibd` (were `family_report.py`, `family_segments.py` and `ibd_report.py`). `og family report` reads the joint VCF once, compresses each child's inheritance table on its own thread, and stops with exit 2 on a joint VCF that is not sorted. The opt-in steps read their tool's output with `og y str-bed|str-report` (45), `og viral report` (47), `og exomiser prepare|report` (48), `og mei report|annotsv-input` (50), `og mocha report` (51), `og vntyper report` (52) and `og locityper report` (53); they replaced `ystr_report.py`, `viral_report.py`, `exomiser_report.py`, `mei_report.py`, `mocha_report.py`, `vntyper_report.py` and `locityper_report.py`. Step 37 places the sample on the current YFull YTree with `og y tree-positions` and `og y tree-place` (were the two commands of `y_tree_place.py`), lists its private SNPs with `og y private-snps` (was `y_private_snps.py`) and writes Yleaf's marker positions with `og y yleaf-positions` (was `yleaf_run.py positions`; `yleaf_run.py predict` stays Python, as glue inside Yleaf's own code). Steps 08, 32 and 36 run `og pgx hla-concordance`, `og pgx pypgx-summary` and `og pgx outside-calls` (were the three commands of `pgx_outside_calls.py`, which keeps the CYP2D6 and HLA rules the reports import), and step 35b runs `og paraphase check-bam|table` (were `paraphase_report.py`, which keeps the SMN1 rule beside Parascopy). Both write their two output files together or not at all. `scripts/owngenome-lookup.sh` runs `og lookup query` ([genotype lookup](lookup.md)), which answers every query of the command line in one process; `og lookup resolve` and `og lookup report` are its two halves, with the command lines of `genome_lookup.py`, which keeps only the query reading and the names rule that `results_index.py` imports. `og recheck` replaced `owngenome_recheck.py` ([Newer databases since a run](recheck.md)); the file stays one release as a shim that runs it, for crontabs. `og dev freshness` replaced `scripts/ci/freshness.py`, the monthly freshness issue of `freshness.yml`. Both ask the network through the host's `curl` (the config on stdin, so no token reaches the process list), which keeps a TLS stack out of the static binary and its cross-builds. A Python file whose tool moved to `og` but whose names other Python still imports stays as a *remnant*: a `# remnant:` header names its importers and the lane that deletes it, and `tests/parity/remnants.tsv` lists the names it may hold and the files that import it. `og dev remnants` (Guard, job `rust`) fails when the file defines a name the list does not allow, when an allowed name has no user left, when a listed importer no longer imports it, or when an unlisted file starts to. So a remnant only shrinks. `bin/family_report.py` is the first one: the pedigree, kinship and PAR rules that `couple_report.py` and `pedigree_svg.py` import. `bin/genome_lookup.py` keeps the query reading and the summary names rule that `results_index.py` imports. From b9379927cfd63f83593368caea570bb14d8b6329 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 15:43:45 +0100 Subject: [PATCH 28/45] fix(ci): reject malformed smoke inputs and bound CR-only reads --- CHANGELOG.md | 1 + docs/rust.md | 6 +- rust/crates/og-dev/src/nf_trace.rs | 12 +- rust/crates/og-dev/src/smoke.rs | 42 +++- rust/crates/og-dev/tests/ci_readers.rs | 314 +++++++++++++++++++++++++ rust/crates/og-io/src/lib.rs | 102 ++++++-- scripts/ci/image-smoke.sh | 91 ++++++- tests/smoke/commands.tsv | 8 +- 8 files changed, 534 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ba164c75..79e7ba9e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Each release's notes. From 1.0.0 on, a major or minor release has hand-written n ## Unreleased +- The shared `og-io` line reader bounds memory by the longest record for CR-only files as well as LF and CRLF files. Smoke coordinates require signed integers, so decimal and nonfinite coordinates fail. Trace integer cells accept whitespace, signs and underscores but deliberately refuse Unicode digits and negative resources. Integers outside the target range fail. Duplicate task rows retain concrete exits. Smoke JSON checks require one document, numeric counts and copy numbers, array catalogs and string genotypes. Numeric depth strings stay supported; booleans are refused. The array and genotype rules deliberately reject malformed values Python accepted. Smoke fixtures build a missing default `og` on first use. - CI readers now use `og dev pharmcat diplotypes`, `nf-trace` and `smoke`. Unreadable PharmCAT reports fail with exit 2 and are marked not assessed. Resource checks refuse trace globs and malformed numeric cells. The smoke mini-reference uses a fixed Rust seed for the planted bases. Gzip smoke reads have deterministic headers. Smoke table checks report concise property results. - The container helpers check (`scripts/ci/check-container-helpers.sh`, Guard job `helper-binaries`) now derives its table with the Rust `og` binary, `og dev helpers`. It replaces the Python inside the script, which is gone; the script keeps the docker probe, the orad download and its self-test. On this tree the table and the rule 1 output are the same, byte for byte. The static run takes 0.01 s and 6 MB where the Python took 0.2 s and 15 MB. What changes: diff --git a/docs/rust.md b/docs/rust.md index bea0896ec..759731446 100644 --- a/docs/rust.md +++ b/docs/rust.md @@ -116,6 +116,10 @@ To add a primitive: write it in `pycompat` with unit tests, add an operation to `og dev pharmcat diplotypes REPORT` prints sorted, unique `GENE\tA1/A2` rows from flat or source-nested reports. `--called` prints the called count and names on stderr. `--called-lines` prints one called gene per line for e2e checks. An unreadable report exits 2 without a count. A valid empty report succeeds with a zero count. -`og dev nf-trace requests TRACE --want FILE --executor-cpus N --executor-mem-gb N` checks initial requests, doubled retry memory and the executor budget. The expectation table is [nf-trace-requests.tsv](../scripts/ci/nf-trace-requests.tsv). `og dev nf-trace attempts TRACE --name-glob PATTERN --want '1:137:32,2:0:64' --nextflow-exit RC` checks a retry sequence. Both accept one explicit path, refuse globs, parse quoted TSV records and reject conflicting duplicates. Identical duplicate task IDs count once. Exit 1 means the check found a mismatch. Exit 2 means input or arguments could not be assessed. +`og dev nf-trace requests TRACE --want FILE --executor-cpus N --executor-mem-gb N` checks initial requests, doubled retry memory and the executor budget. The expectation table is [nf-trace-requests.tsv](https://github.com/GeiserX/OwnGenome/blob/main/scripts/ci/nf-trace-requests.tsv). `og dev nf-trace attempts TRACE --name-glob PATTERN --want '1:137:32,2:0:64' --nextflow-exit RC` checks a retry sequence. Both accept one explicit path, refuse globs, parse quoted TSV records and reject conflicting duplicates. Identical duplicate task IDs count once. Exit 1 means the check found a mismatch. Exit 2 means input or arguments could not be assessed. `og dev smoke` supplies `mini-ref SRC ORIG MINI POS LEN`, `long-reads HAP1 HAP2 OUT.gz`, `eh-catalog REF OUT END`, `csv-cell FILE KEY=VALUE[,KEY=VALUE] COLUMN` and `check KIND FILE`. Check kinds are `hap-ibd`, `ibdmix`, `rfmix`, `eigen` and `parascopy`. Mini-reference bases use a fixed Rust seed. Long-read gzip headers are deterministic. These synthetic fixtures do not change pipeline output files. Smoke references are limited to 1,003,000 bases. CSV lookups and table checks read one record at a time. + +CI integer cells accept surrounding whitespace, signs and digit separators. Trace cells deliberately refuse Unicode digits and negative resource values. Smoke coordinates accept Unicode decimal digits and use signed 128-bit integers. Integers beyond the target range fail. Decimal and nonfinite coordinates fail. Duplicate trace rows retain the first concrete exit status and reject later conflicts. + +The shared `og-io` reader consumes LF, CRLF and lone CR records with storage bounded by the longest line. Smoke JSON checks require one document and typed fields. Catalogs must be arrays and genotypes must be nonempty strings. This deliberately rejects malformed values that Python truthiness or `len()` accepted. Numeric depth strings remain supported; booleans are refused. Smoke fixtures and assertions build `og` on first use if the default binary is missing. An explicit `OG` is used as supplied. diff --git a/rust/crates/og-dev/src/nf_trace.rs b/rust/crates/og-dev/src/nf_trace.rs index 2a0aef041..2015c9e0d 100644 --- a/rust/crates/og-dev/src/nf_trace.rs +++ b/rust/crates/og-dev/src/nf_trace.rs @@ -38,9 +38,10 @@ fn cell<'a>(r: &'a Record, k: &str) -> Result<&'a str, String> { .ok_or_else(|| format!("missing column {k}")) } fn num(r: &Record, k: &str) -> Result { - cell(r, k)? - .parse() - .map_err(|_| format!("column {k} is not an unsigned integer")) + pycompat::num::parse_int(cell(r, k)?) + .ok() + .and_then(|v| u64::try_from(v).ok()) + .ok_or_else(|| format!("column {k} is not an unsigned ASCII integer")) } fn traces(path: &str, out: &mut dyn Write) -> Result, String> { let mut rows: Vec = Vec::new(); @@ -48,7 +49,7 @@ fn traces(path: &str, out: &mut dyn Write) -> Result, String> { records(path, |r| { let id = cell(&r, "task_id")?.to_owned(); if let Some(&i) = ids.get(&id) { - let first: &Record = &rows[i]; + let first: &mut Record = &mut rows[i]; let a = cell(first, "exit")?; let b = cell(&r, "exit")?; if (a != "-" && b != "-" && a != b) @@ -57,6 +58,9 @@ fn traces(path: &str, out: &mut dyn Write) -> Result, String> { { return Err(format!("task {id} has two different rows in the trace")); } + if a == "-" && b != "-" { + first.insert("exit".into(), b.into()); + } writeln!( out, "{}: attempt {} is written twice in the trace; counted once", diff --git a/rust/crates/og-dev/src/smoke.rs b/rust/crates/og-dev/src/smoke.rs index 950d023cf..b95bb6470 100644 --- a/rust/crates/og-dev/src/smoke.rs +++ b/rust/crates/og-dev/src/smoke.rs @@ -51,6 +51,31 @@ fn write_fasta(path: &str, seq: &[u8]) -> Result<(), String> { } out.commit().map_err(|e| e.to_string()) } +fn integer(s: Option<&&str>) -> Result { + let s = *s.ok_or("missing integer field")?; + // Python int accepts every Unicode decimal digit. Nd runs contain one + // or more adjacent 0..9 alphabets (the mathematical digits have five). + let normalized; + let s = if s.is_ascii() { + s + } else { + normalized = s + .chars() + .map(|c| { + if c.is_ascii() || !pycompat::text::is_decimal(c) { + return c; + } + let mut start = c as u32; + while char::from_u32(start - 1).is_some_and(pycompat::text::is_decimal) { + start -= 1; + } + char::from(b'0' + ((c as u32 - start) % 10) as u8) + }) + .collect::(); + &normalized + }; + pycompat::num::parse_int(s).map_err(|e| format!("invalid integer: {e}")) +} fn number(s: &str) -> Result { s.parse().map_err(|_| format!("invalid integer: {s}")) } @@ -280,13 +305,14 @@ fn check(kind: &str, path: &str, out: &mut dyn Write) -> Result { } if r.first() == Some(&"S") && float(r.get(4))? >= 4. - && float(r.get(3))? - float(r.get(2))? >= 50000. + && integer(r.get(3))?.saturating_sub(integer(r.get(2))?) >= 50000 { - let a = float(r.get(2))?; - let b = float(r.get(3))?; + let a = integer(r.get(2))?; + let b = integer(r.get(3))?; n += 1; - coverage += (b.min(2600000.) - a.max(2000000.)).max(0.); - if b <= 2000000. || a >= 2600000. { + coverage += + (b.clamp(2000000, 2600000) - a.clamp(2000000, 2600000)).max(0) as f64; + if b <= 2000000 || a >= 2600000 { outside += 1; } } @@ -319,10 +345,10 @@ fn check(kind: &str, path: &str, out: &mut dyn Write) -> Result { if line.starts_with('#') { continue; } - let a = float(r.get(1))?; - let b = float(r.get(2))?; + let a = integer(r.get(1))?; + let b = integer(r.get(2))?; n += 1; - for (j, p) in [5000000., 15000000.].iter().enumerate() { + for (j, p) in [5000000, 15000000].iter().enumerate() { if at[j].is_none() && a <= *p && *p < b { at[j] = Some([ r.get(6).ok_or("short RFMix row")?.to_string(), diff --git a/rust/crates/og-dev/tests/ci_readers.rs b/rust/crates/og-dev/tests/ci_readers.rs index a497e9307..9622b44b3 100644 --- a/rust/crates/og-dev/tests/ci_readers.rs +++ b/rust/crates/og-dev/tests/ci_readers.rs @@ -319,3 +319,317 @@ fn pca_nonfinite_values_follow_python_comparisons() { let p = t.file("quoted.tsv", "# a comment\n\"chr5\"\t1\t2\ta\tb\tc\t\t99\n"); assert_eq!(run(smoke::cli, &["check", "parascopy", &p]).0, 1); } + +#[test] +fn smoke_coordinates_require_integers() { + let t = Temp::new(); + for bad in ["-inf", "inf", "nan", "2000000.0"] { + for text in [ + format!("h\nS 22 {bad} 2600000 4\n"), + format!("h\nS 22 2000000 {bad} 4\n"), + ] { + let p = t.file("ibdmix", &text); + assert_ne!(run(smoke::cli, &["check", "ibdmix", &p]).0, 0, "{text}"); + } + for text in [ + format!( + "#codes: POPA=0 POPB=1\n20 {bad} 10000000 0 0 0 0 0\n20 10000000 20000000 0 0 0 1 1\n" + ), + format!( + "#codes: POPA=0 POPB=1\n20 0 {bad} 0 0 0 0 0\n20 10000000 20000000 0 0 0 1 1\n" + ), + ] { + let p = t.file("rfmix", &text); + assert_ne!(run(smoke::cli, &["check", "rfmix", &p]).0, 0, "{text}"); + } + } + let p = t.file("ints", "h\nS 22 +2_000_000 2_600_000 4.0\n"); + assert_eq!(run(smoke::cli, &["check", "ibdmix", &p]).0, 0); +} + +#[test] +fn trace_retains_concrete_duplicate_exit() { + let t = Temp::new(); + let first = "task_id\tname\tattempt\texit\tmemory\n1\tPRS (s)\t1\t-\t25769803776\n"; + let p = t.file( + "trace", + &format!("{first}1\tPRS (s)\t1\t137\t25769803776\n"), + ); + let args = [ + "attempts", + &p, + "--name-glob", + "PRS (s)", + "--want", + "1:137:24", + "--nextflow-exit", + "0", + ]; + assert_eq!(run(nf_trace::cli, &args).0, 0); + t.file( + "trace", + &format!("{first}1\tPRS (s)\t1\t137\t25769803776\n1\tPRS (s)\t1\t0\t25769803776\n"), + ); + assert_eq!(run(nf_trace::cli, &args).0, 2); +} + +#[test] +fn trace_integer_policy() { + let t = Temp::new(); + let p = t.file("spaces", &TRACE.replace("\t8\t", "\t +8 \t")); + let want = t.file( + "want", + "process\tcpus\tmemory_gb\ttime_h\nALIGN_MINIMAP2\t8\t32\t24\n", + ); + let args = [ + "requests", + &p, + "--want", + &want, + "--executor-cpus", + "16", + "--executor-mem-gb", + "96", + ]; + assert_eq!(run(nf_trace::cli, &args).0, 0); + t.file("spaces", &TRACE.replace("\t8\t", "\t٨\t")); + assert_eq!(run(nf_trace::cli, &args).0, 2); +} + +/// Run explicitly with a full git history and Python available. The oracle +/// is the actual replaced assertion, not a second maintained implementation. +#[test] +#[ignore = "requires archived source and a local Python interpreter"] +fn differential_coordinate_controls() { + use std::process::Command; + let t = Temp::new(); + let repo = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../.."); + let archived = Command::new("git") + .arg("-C") + .arg(&repo) + .args([ + "show", + "669a8196cf4c377200f9b4af9c235ef85fb774a1:tests/smoke/commands.tsv", + ]) + .output() + .unwrap(); + assert!(archived.status.success()); + let table = String::from_utf8(archived.stdout).unwrap(); + for (kind, var, name) in [ + ("ibdmix", "IBDMIX_IMAGE", "o.txt"), + ("rfmix", "RFMIX_IMAGE", "o.msp.tsv"), + ] { + let row = table + .lines() + .find(|l| l.starts_with(&format!("{var}\t"))) + .unwrap(); + let expect = row.split('\t').nth(4).unwrap(); + let code = expect + .split("\" \"") + .nth(1) + .unwrap() + .strip_suffix('"') + .unwrap(); + for value in [ + "2000000", + "+2_000_000", + "٢٠٠٠٠٠٠", + "𝟚𝟘𝟘𝟘𝟘𝟘𝟘", + "-inf", + "inf", + "nan", + "2000000.0", + ] { + for column in [0, 1] { + let mut bounds = ["2000000", "2600000"]; + bounds[column] = value; + let text = if kind == "ibdmix" { + format!("h\nS 22 {} {} 4\n", bounds[0], bounds[1]) + } else { + let a = if column == 0 { value } else { "0" }; + let b = if column == 1 { value } else { "10000000" }; + format!( + "#codes: POPA=0 POPB=1\n20 {a} {b} 0 0 0 0 0\n20 10000000 20000000 0 0 0 1 1\n" + ) + }; + let p = t.file(name, &text); + let old = Command::new("python3") + .args(["-I", "-c", code]) + .current_dir(&t.0) + .output() + .unwrap(); + let new = run(smoke::cli, &["check", kind, &p]); + assert_eq!( + old.status.success(), + new.0 == 0, + "{kind} col={column} value={value}: {}", + new.2 + ); + println!( + "{kind} col={column} value={value}: Python={} Rust={}", + old.status.success(), + new.0 + ); + } + } + } +} + +#[test] +#[ignore = "requires archived source, jq and a local Python interpreter"] +fn differential_json_controls() { + use std::process::Command; + let t = Temp::new(); + let repo = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../.."); + let archived = Command::new("git") + .arg("-C") + .arg(&repo) + .args([ + "show", + "669a8196cf4c377200f9b4af9c235ef85fb774a1:tests/smoke/commands.tsv", + ]) + .output() + .unwrap(); + assert!(archived.status.success()); + let old_table = String::from_utf8(archived.stdout).unwrap(); + let new_table = std::fs::read_to_string(repo.join("tests/smoke/commands.tsv")).unwrap(); + let script = std::fs::read_to_string(repo.join("scripts/ci/image-smoke.sh")).unwrap(); + let helper = script + .split("jqcheck() {") + .nth(1) + .unwrap() + .split("\n}\n") + .next() + .unwrap(); + let helper = format!( + "CHECK_FAILS=0; pass() {{ :; }}; fail() {{ CHECK_FAILS=1; }}; jqcheck() {{{helper}\n}}\njqcheck control \"$1\" \"$2\"; exit \"$CHECK_FAILS\"" + ); + for (var, index, file, template, good) in [ + ( + "FASTP_IMAGE", + 0, + "fastp.json", + "{\"summary\":{\"before_filtering\":{\"total_reads\":100},\"after_filtering\":{\"total_reads\":VALUE}}}", + "90", + ), + ( + "EXPANSIONHUNTER_IMAGE", + 0, + "bundled_catalog.json", + "VALUE", + "[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]", + ), + ( + "EXPANSIONHUNTER_IMAGE", + 1, + "eh.json", + "{\"LocusResults\":{\"SMOKE1\":{\"Variants\":{\"SMOKE1\":{\"Genotype\":VALUE}}}}}", + "\"12/12\"", + ), + ( + "PARAPHASE_IMAGE", + 0, + "smn1.json", + "{\"smn1\":{\"region_specific_info\":{\"smn1_cn\":2,\"smn2_cn\":VALUE,\"smn_del78_cn\":1}}}", + "1", + ), + ( + "PYTHON_IMAGE", + 0, + "HG002_cyp2d6.json", + "{\"HG002_cyrius\":{\"Median_depth\":VALUE,\"Coverage_MAD\":0,\"Genotype\":\"test\"}}", + "20", + ), + ] { + let row = |table: &str| { + table + .lines() + .find(|l| l.starts_with(&format!("{var}\t"))) + .unwrap() + .split('\t') + .nth(4) + .unwrap() + .to_owned() + }; + let old = row(&old_table); + let code = old + .split("py \"") + .nth(index + 1) + .unwrap() + .split("\" \"") + .nth(1) + .unwrap() + .split('"') + .next() + .unwrap(); + let new = row(&new_table); + let filter = new + .split("jqcheck \"") + .nth(index + 1) + .unwrap() + .split('\'') + .nth(1) + .unwrap(); + for value in [ + good, + "\"broken\"", + "null", + "[]", + "{}", + "true", + "false", + "20", + "\"aaaaaaaaaaaaaaaaaaaaaaaaaaaa\"", + ] { + let json = template.replace("VALUE", value); + let p = t.file(file, &json); + let python = Command::new("python3") + .args(["-I", "-c", code]) + .current_dir(&t.0) + .output() + .unwrap(); + let rust = Command::new("bash") + .args(["-c", &helper, "_", &p, filter]) + .output() + .unwrap(); + let deliberate = (var == "EXPANSIONHUNTER_IMAGE" + && ((index == 0 && value.starts_with('"')) + || (index == 1 && ["true", "20"].contains(&value)))) + || (["PARAPHASE_IMAGE", "PYTHON_IMAGE"].contains(&var) && value == "true"); + if deliberate { + assert!(!rust.status.success(), "typed policy: {var} {value}"); + } else { + assert_eq!( + python.status.success(), + rust.status.success(), + "{var} {index} {value}: {}", + String::from_utf8_lossy(&python.stderr) + ); + } + println!( + "{var}/{index} {value}: Python={} jq={} deliberate={deliberate}", + python.status.success(), + rust.status.success() + ); + } + let valid = template.replace("VALUE", good); + let p = t.file(file, &format!("{valid}\n{valid}\n")); + assert!( + !Command::new("python3") + .args(["-I", "-c", code]) + .current_dir(&t.0) + .output() + .unwrap() + .status + .success() + ); + assert!( + !Command::new("bash") + .args(["-c", &helper, "_", &p, filter]) + .output() + .unwrap() + .status + .success() + ); + } +} diff --git a/rust/crates/og-io/src/lib.rs b/rust/crates/og-io/src/lib.rs index 363b9ceeb..1943c18be 100644 --- a/rust/crates/og-io/src/lib.rs +++ b/rust/crates/og-io/src/lib.rs @@ -110,7 +110,7 @@ pub enum Utf8 { pub struct Lines { inner: R, raw: Vec, - pos: usize, + skip_lf: bool, utf8: Utf8, } @@ -119,7 +119,7 @@ impl Lines { Lines { inner, raw: Vec::new(), - pos: 0, + skip_lf: false, utf8, } } @@ -128,25 +128,36 @@ impl Lines { /// `\n` (none on a last line without one), or `false` at the end. pub fn read_line(&mut self, out: &mut String) -> io::Result { out.clear(); - if self.pos == self.raw.len() { - self.raw.clear(); - self.pos = 0; - if self.inner.read_until(b'\n', &mut self.raw)? == 0 { - return Ok(false); + self.raw.clear(); + // A CR ends this record immediately. Consume a following LF on the + // next call, including when CR is the last byte of a buffer. + if self.skip_lf { + if self.inner.fill_buf()?.first() == Some(&b'\n') { + self.inner.consume(1); } + self.skip_lf = false; } - let rest = &self.raw[self.pos..]; - // read_until stops after a \n or at the end, so a \r\n is never split. - let (body, used) = match rest.iter().position(|&c| c == b'\n' || c == b'\r') { - None => (rest, rest.len()), - Some(k) if rest[k] == b'\r' && rest.get(k + 1) == Some(&b'\n') => (&rest[..k], k + 2), - Some(k) => (&rest[..k], k + 1), + let ended = loop { + let bytes = self.inner.fill_buf()?; + if bytes.is_empty() { + if self.raw.is_empty() { + return Ok(false); + } + break false; + } + if let Some(k) = bytes.iter().position(|&b| b == b'\r' || b == b'\n') { + self.raw.extend_from_slice(&bytes[..k]); + self.skip_lf = bytes[k] == b'\r'; + self.inner.consume(k + 1); + break true; + } + let n = bytes.len(); + self.raw.extend_from_slice(bytes); + self.inner.consume(n); }; - let ended = used > body.len(); - // Past the line before decoding it, so a strict error is not raised - // again for the same line by a caller that goes on reading. (Python - // raises once too; a caller is expected to stop at the error.) - self.pos += used; + // Consume the record before decoding so a strict UTF-8 error is + // raised only once when a caller continues with the next record. + let body = &self.raw; match self.utf8 { Utf8::Strict => out.push_str( std::str::from_utf8(body) @@ -349,3 +360,58 @@ mod tests { fs::remove_dir_all(&dir).unwrap(); } } + +#[cfg(test)] +mod bounded_line_tests { + use super::*; + + #[test] + fn all_endings_and_buffer_edges() { + for bytes in [ + b"a\nb\n".as_slice(), + b"a\r\nb\r\n", + b"a\rb\r", + b"a\r\nb\rc\nd", + b"tail", + b"", + ] { + for capacity in 1..=8 { + let mut input = Lines::new( + BufReader::with_capacity(capacity, io::Cursor::new(bytes)), + Utf8::Strict, + ); + let mut got = Vec::new(); + let mut line = String::new(); + while input.read_line(&mut line).unwrap() { + got.push(line.clone()); + } + let normalized = String::from_utf8(bytes.to_vec()) + .unwrap() + .replace("\r\n", "\n") + .replace('\r', "\n"); + let want: Vec<_> = normalized.split_inclusive('\n').collect(); + assert_eq!(got, want, "capacity {capacity}: {bytes:?}"); + } + } + } + + #[test] + fn cr_only_storage_is_bounded_by_one_line() { + let input = b"a,b\r".repeat(1 << 18); + let mut input = Lines::new( + BufReader::with_capacity(64, io::Cursor::new(input)), + Utf8::Strict, + ); + let mut line = String::new(); + for _ in 0..1 << 18 { + assert!(input.read_line(&mut line).unwrap()); + assert_eq!(line, "a,b\n"); + assert!( + input.raw.capacity() <= 64, + "buffer grew to {} bytes", + input.raw.capacity() + ); + } + assert!(!input.read_line(&mut line).unwrap()); + } +} diff --git a/scripts/ci/image-smoke.sh b/scripts/ci/image-smoke.sh index 89d55911e..ef21b6a67 100755 --- a/scripts/ci/image-smoke.sh +++ b/scripts/ci/image-smoke.sh @@ -146,6 +146,71 @@ self_test() { if grep -q '^\[PASS\] true filter' <<< "$out" && grep -q '^\[FAIL\] false filter' <<< "$out" && grep -q '^fails=1$' <<< "$out"; then echo "[PASS] jqcheck rejects a false filter" else echo "[FAIL] jqcheck rejects a false filter: $out"; fails=$((fails + 1)); fi + # Read the production filters, so a weakened assertion makes this test red. + local counts catalog genotype copies depth + counts=$(awk -F '\t' '$1 == "FASTP_IMAGE" {print $5}' "$TABLE" | cut -d "'" -f 2) + catalog=$(awk -F '\t' '$1 == "EXPANSIONHUNTER_IMAGE" {print $5}' "$TABLE" | cut -d "'" -f 2) + genotype=$(awk -F '\t' '$1 == "EXPANSIONHUNTER_IMAGE" {print $5}' "$TABLE" | cut -d "'" -f 4) + copies=$(awk -F '\t' '$1 == "PARAPHASE_IMAGE" {print $5}' "$TABLE" | cut -d "'" -f 2) + depth=$(awk -F '\t' '$1 == "PYTHON_IMAGE" {print $5}' "$TABLE" | cut -d "'" -f 2) + json_case() { + local label=$1 filter=$2 json=$3 want=$4 + printf '%s\n' "$json" > "${tmp}/control.json" + out=$(CHECK_FAILS=0; jqcheck "$label" "${tmp}/control.json" "$filter"; echo "fails=$CHECK_FAILS") + if grep -qx "fails=$want" <<< "$out"; then echo "[PASS] $label" + else echo "[FAIL] $label: $out"; fails=$((fails + 1)); fi + } + json_case "numeric counts" "$counts" '{"summary":{"before_filtering":{"total_reads":100},"after_filtering":{"total_reads":90}}}' 0 + json_case "one JSON document" "$counts" $'{"summary":{"before_filtering":{"total_reads":100},"after_filtering":{"total_reads":0}}}\n{"summary":{"before_filtering":{"total_reads":100},"after_filtering":{"total_reads":90}}}' 1 + json_case "array catalog" "$catalog" "[$(printf '0,%.0s' {1..19})0]" 0 + json_case "string genotype" "$genotype" '{"LocusResults":{"SMOKE1":{"Variants":{"SMOKE1":{"Genotype":"12/12"}}}}}' 0 + json_case "numeric copy numbers" "$copies" '{"smn1":{"region_specific_info":{"smn1_cn":2,"smn2_cn":1,"smn_del78_cn":1}}}' 0 + json_case "numeric depth" "$depth" '{"HG002_cyrius":{"Median_depth":20}}' 0 + json_case "numeric string depth" "$depth" '{"HG002_cyrius":{"Median_depth":"20"}}' 0 + local bad + for bad in '"broken"' null '[]' '{}' true false; do + json_case "output count rejects $bad" "$counts" "{\"summary\":{\"before_filtering\":{\"total_reads\":100},\"after_filtering\":{\"total_reads\":$bad}}}" 1 + json_case "input count rejects $bad" "$counts" "{\"summary\":{\"before_filtering\":{\"total_reads\":$bad},\"after_filtering\":{\"total_reads\":90}}}" 1 + json_case "catalog rejects $bad" "$catalog" "$bad" 1 + json_case "copy numbers reject $bad" "$copies" "{\"smn1\":{\"region_specific_info\":{\"smn1_cn\":2,\"smn2_cn\":$bad,\"smn_del78_cn\":1}}}" 1 + json_case "depth rejects $bad" "$depth" "{\"HG002_cyrius\":{\"Median_depth\":$bad}}" 1 + done + for bad in 1 null '[]' '{}' true false '""'; do + json_case "genotype rejects $bad" "$genotype" "{\"LocusResults\":{\"SMOKE1\":{\"Variants\":{\"SMOKE1\":{\"Genotype\":$bad}}}}}" 1 + done + for filter in "$counts" "$catalog" "$genotype" "$copies" "$depth"; do + json_case "missing keys fail" "$filter" '{}' 1 + done + json_case "numeric catalog fails" "$catalog" '20' 1 + json_case "long string catalog fails" "$catalog" '"aaaaaaaaaaaaaaaaaaaaaaaaaaaa"' 1 + json_case "no JSON document fails" "$counts" '' 1 + mkdir -p "${tmp}/repo/bin" "${tmp}/repo/scripts" + cat > "${tmp}/repo/bin/og" <<'SH' +#!/usr/bin/env bash +[ -f "$(dirname "$0")/../ready" ] || exit 127 +printf '%s\n' "$*" +SH + cat > "${tmp}/repo/scripts/setup.sh" <<'SH' +#!/usr/bin/env bash +[ "$1" = --og ] || exit 2 +printf 'built\n' >> "$(dirname "$0")/../ready" +SH + chmod +x "${tmp}/repo/bin/og" + # shellcheck disable=SC2016 # variables belong to the isolated test shell + out=$(env -u OG REPO="${tmp}/repo" bash -c "$(declare -f smoke_og)"' + rc=0 + "$REPO/bin/og" dev smoke mini-ref >/dev/null 2>&1 || rc=$? + [ "$rc" -eq 127 ] || exit 1 + smoke_og dev smoke mini-ref || exit 1 + smoke_og dev smoke csv-cell || exit 1 + [ "$(wc -l < "$REPO/ready")" -eq 1 ] || exit 1 + OG="$REPO/bin/absent" + rc=0 + smoke_og version >/dev/null 2>&1 || rc=$? + [ "$rc" -eq 127 ] || exit 1 + '); rc=$? + if [ "$rc" -eq 0 ]; then echo "[PASS] missing default og is built once; explicit OG failure is retained" + else echo "[FAIL] og fixture preparation: $out"; fails=$((fails + 1)); fi rm -rf "$tmp" if [ "$fails" -gt 0 ]; then echo "self-test: ${fails} case(s) failed" >&2; return 1; fi echo "self-test: all cases passed" @@ -296,7 +361,7 @@ need() { need_ref() { fx_get fixture_ref.fa.gz fixture_ref.fa.gz.fai fixture_ref.fa.gz.gzi || return 1 hsam faidx /fx/fixture_ref.fa.gz chr20:10000001-10500000 > "${IN}/slice.fa.tmp" || return 1 - "${OG:-${REPO}/bin/og}" dev smoke mini-ref "${IN}/slice.fa.tmp" "${IN}/orig.fa" "${IN}/mini.fa" "$PLANT_POS" "$PLANT_LEN" || return 1 + smoke_og dev smoke mini-ref "${IN}/slice.fa.tmp" "${IN}/orig.fa" "${IN}/mini.fa" "$PLANT_POS" "$PLANT_LEN" || return 1 rm -f "${IN}/slice.fa.tmp" hsam faidx /in/orig.fa && hsam faidx /in/mini.fa && hsam dict -o /in/mini.dict /in/mini.fa || return 1 [ "$(cut -f2 "${IN}/mini.fa.fai")" = "$MINI_LEN" ] @@ -348,7 +413,7 @@ need_longreads() { need ref && need truth || return 1 hbcf consensus -H 1 -f /in/orig.fa /in/truth.vcf.gz > "${IN}/hap1.fa" && hbcf consensus -H 2 -f /in/orig.fa /in/truth.vcf.gz > "${IN}/hap2.fa" || return 1 - "${OG:-${REPO}/bin/og}" dev smoke long-reads "${IN}/hap1.fa" "${IN}/hap2.fa" "${IN}/long.fq.gz" + smoke_og dev smoke long-reads "${IN}/hap1.fa" "${IN}/hap2.fa" "${IN}/long.fq.gz" } need_longbam() { need longreads || return 1 @@ -429,7 +494,7 @@ need_qc() { # in the mini reference, away from the slice ends and the planted bases. need_ehcatalog() { need ref || return 1 - "${OG:-${REPO}/bin/og}" dev smoke eh-catalog "${IN}/mini.fa" "${IN}/eh_catalog.json" "$SMALL_END" + smoke_og dev smoke eh-catalog "${IN}/mini.fa" "${IN}/eh_catalog.json" "$SMALL_END" } # A chain that maps chr20 onto itself over the region of the small variants. need_chain() { @@ -470,12 +535,24 @@ contains() { if [ -f "$2" ] && grep -Eq -- "$3" "$2"; then pass "$1"; else fail nonempty() { if [ -s "$2" ]; then pass "$1"; else fail "$1 (${2} is missing or empty)"; fi; } jqcheck() { local out - if out=$(jq -e "$3" "$2" 2>&1); then pass "$1${out:+ (${out//$'\n'/ })}" + if out=$(jq -se "if length == 1 then .[0] | ($3) else error(\"expected exactly one JSON document\") end" "$2" 2>&1); then pass "$1${out:+ (${out//$'\n'/ })}" else fail "$1: ${out//$'\n'/ }"; fi } +# Build only when a fixture or assertion first needs og. A caller-supplied +# OG is authoritative; its failure must not silently select another binary. +smoke_og() { + local rc=0 + "${OG:-${REPO}/bin/og}" --version >/dev/null 2>&1 || rc=$? + if [ "$rc" -eq 127 ] && [ -z "${OG:-}" ]; then + bash "${REPO}/scripts/setup.sh" --og || return 1 + elif [ "$rc" -ne 0 ]; then + return "$rc" + fi + "${OG:-${REPO}/bin/og}" "$@" +} smoke_check() { local out - if out=$("${OG:-${REPO}/bin/og}" dev smoke check "$2" "$3" 2>&1); then pass "$1 ($out)" + if out=$(smoke_og dev smoke check "$2" "$3" 2>&1); then pass "$1 ($out)" else fail "$1: $out"; fi } check_pharmcat() { @@ -528,11 +605,11 @@ tsv_count() { } # csv_cell FILE KEY=VALUE[,KEY=VALUE] COLUMN: COLUMN of the first row that matches. csv_cell() { - "${OG:-${REPO}/bin/og}" dev smoke csv-cell "$1" "$2" "$3" + smoke_og dev smoke csv-cell "$1" "$2" "$3" } # pharmcat_called report.json: genes with a named diplotype. pharmcat_called() { - "${OG:-${REPO}/bin/og}" dev pharmcat diplotypes "$1" --called + smoke_og dev pharmcat diplotypes "$1" --called } # ------------------------------------------------------------- the runner diff --git a/tests/smoke/commands.tsv b/tests/smoke/commands.tsv index ff757048e..e7387040c 100644 --- a/tests/smoke/commands.tsv +++ b/tests/smoke/commands.tsv @@ -7,7 +7,7 @@ MINIMAP2_SAMTOOLS_IMAGE - ref,reads,bam minimap2 --version > minimap2_version.tx SAMTOOLS_HTTPS_IMAGE net - samtools view -H https://ftp.sra.ebi.ac.uk/vol1/run/ERR323/ERR3239334/NA12878.final.cram > h.sam same "@SQ lines of the docs/quick-test.md CRAM header, read over https" "$(grep -c '^@SQ' h.sam)" 3366; contains "chr22 among them" h.sam "^@SQ[[:space:]]SN:chr22[[:space:]]" BCFTOOLS_IMAGE - ref,truth,vcf bcftools view -Ob -o a.bcf /in/sample.vcf.gz && bcftools index a.bcf && bcftools view -Oz -o b.vcf.gz a.bcf && bcftools index -t b.vcf.gz && bcftools view -H b.vcf.gz | wc -l > n.txt && bcftools +split-vep -l /in/sample.vcf.gz > csq.txt && bcftools norm -m-both -f /in/mini.fa -Oz -o norm.vcf.gz /in/truth.vcf.gz && bcftools isec -n=2 -w1 -Oz -o shared.vcf.gz /in/truth.vcf.gz /in/query90.vcf.gz && bcftools view -H shared.vcf.gz | wc -l > shared.txt same "records after a VCF to BCF to VCF round trip" "$(tr -d " " < n.txt)" "$(grep -vc "^#" "$FX/HG002_vep.vcf")"; contains "split-vep lists the CSQ fields" csq.txt "Consequence"; same "records isec finds in truth and query" "$(tr -d " " < shared.txt)" "$(vcf_records /in/query90.vcf.gz)"; at_least "records after norm" "$(vcf_records norm.vcf.gz)" 300 DEEPVARIANT_IMAGE - ref,bam,truth /opt/deepvariant/bin/run_deepvariant --model_type=WGS --ref=/in/mini.fa --reads=/in/mini.bam --output_vcf=dv.vcf.gz --sample_name=HG002 --num_shards=4 --regions chr20:1-449000 --intermediate_results_dir tmp at_least "SNV recall against the GIAB truth" "$(snv_recall dv.vcf.gz)" 0.95; same "sample column" "$(bcf query -l dv.vcf.gz)" HG002 -FASTP_IMAGE - reads fastp -i /in/reads_R1.fq.gz -I /in/reads_R2.fq.gz -o t_R1.fq.gz -O t_R2.fq.gz --detect_adapter_for_pe --qualified_quality_phred 20 --cut_front --cut_tail --cut_mean_quality 20 --length_required 36 -g -R HG002 -j fastp.json -h fastp.html jqcheck "trimmed reads kept (at least 80 %)" fastp.json '.summary | .before_filtering.total_reads as $b | .after_filtering.total_reads as $a | $b > 0 and $a >= 0.8 * $b' +FASTP_IMAGE - reads fastp -i /in/reads_R1.fq.gz -I /in/reads_R2.fq.gz -o t_R1.fq.gz -O t_R2.fq.gz --detect_adapter_for_pe --qualified_quality_phred 20 --cut_front --cut_tail --cut_mean_quality 20 --length_required 36 -g -R HG002 -j fastp.json -h fastp.html jqcheck "trimmed reads kept (at least 80 %)" fastp.json '.summary | .before_filtering.total_reads as $b | .after_filtering.total_reads as $a | ($b | type == "number") and ($a | type == "number") and $b > 0 and $a >= 0.8 * $b' BWAMEM2_IMAGE - ref,reads bwa-mem2 index -p mini /in/mini.fa && bwa-mem2 mem -t 4 -o bwamem2.sam -R '@RG\tID:HG002\tSM:HG002\tPL:ILLUMINA\tLB:HG002' mini /in/reads_R1.fq.gz /in/reads_R2.fq.gz at_least "reads mapped (%)" "$(mapped_pct bwamem2.sam)" 95 BWA_IMAGE - ref,reads bwa index -p mini /in/mini.fa && bwa mem -t 4 -R '@RG\tID:HG002\tSM:HG002' mini /in/reads_R1.fq.gz /in/reads_R2.fq.gz > bwa.sam at_least "reads mapped (%)" "$(mapped_pct bwa.sam)" 95 FREEBAYES_IMAGE - ref,bam,truth freebayes -f /in/mini.fa --region chr20:1-449000 /in/mini.bam > freebayes.vcf at_least "SNV recall against the GIAB truth" "$(snv_recall freebayes.vcf)" 0.9 @@ -41,7 +41,7 @@ YLEAF_IMAGE net,also=YLEAF_DATA_SHA256 - sh /smoke/yleaf.sh same "the release ar HIPSTR_IMAGE - fullref,slice sh /smoke/hipstr.sh at_least "Y-STR regions HipSTR genotypes in the chrY slices, with GB, DP, Q and DSTUTTER (of 11)" "$(grep -c . called.tsv)" 7; same "a haploid call on chrY (one allele in GT)" "$(awk -F'\t' '$2 ~ /[\/|]/' called.tsv | wc -l | tr -d ' ')" 0; has "the image's Python, which step 45 runs on" "$(cat python_version.txt)" "^Python 3\."; contains "HipSTR's log names the regions it read" hipstr.log "Processing region chrY 6993189 6993257" T1K_IMAGE net hlareads sh /smoke/t1k.sh has "HLA-A allele, one of HG002's (A*01:01, A*26:01)" "$(awk -F'\t' '$1 == "HLA-A" {print $3}' t1k/HG002_hla_genotype.tsv)" "^HLA-A\*(01:01|26:01)"; has "both HLA-A alleles of HG002 (A*01:01 and A*26:01)" "$(awk -F'\t' '$1 == "HLA-A" {print $3, $6}' t1k/HG002_hla_genotype.tsv)" "^HLA-A\*01:01[^ ]* HLA-A\*26:01|^HLA-A\*26:01[^ ]* HLA-A\*01:01" OPTITYPE_IMAGE - hlareads sh /smoke/optitype.sh has "both HLA-A alleles of HG002 (A*01:01 and A*26:01)" "$(awk -F'\t' 'NR == 2 {print $2, $3}' HG002_result.tsv)" "^A\*01:01 A\*26:01$|^A\*26:01 A\*01:01$"; has "two HLA-B alleles" "$(awk -F'\t' 'NR == 2 {print $4, $5}' HG002_result.tsv)" "^B\*[0-9]+:[0-9]+ B\*[0-9]+:[0-9]+$"; at_least "reads OptiType mapped to its HLA sequences" "$(awk -F'\t' 'NR == 1 {for (i = 1; i <= NF; i++) if ($i == "Reads") c = i} NR == 2 {print int($c)}' HG002_result.tsv)" 100 -EXPANSIONHUNTER_IMAGE - ref,bam,ehcatalog sh /smoke/expansionhunter.sh jqcheck "the bundled GRCh38 catalog has at least 20 loci" bundled_catalog.json 'length >= 20'; jqcheck "a genotype for the repeat on the fixture" eh.json '.LocusResults.SMOKE1.Variants.SMOKE1.Genotype | select(. != null and . != false and . != 0 and . != "" and . != [] and . != {})' +EXPANSIONHUNTER_IMAGE - ref,bam,ehcatalog sh /smoke/expansionhunter.sh jqcheck "the bundled GRCh38 catalog has at least 20 loci" bundled_catalog.json 'type == "array" and length >= 20'; jqcheck "a genotype for the repeat on the fixture" eh.json '.LocusResults.SMOKE1.Variants.SMOKE1.Genotype | type == "string" and length > 0' GOLEFT_IMAGE - slice goleft indexcov --directory indexcov /in/HG002_slice.bam at_least "sample rows in the .ped" "$(awk '!/^#/ && NF >= 6' indexcov/indexcov-indexcov.ped | wc -l)" 1 MOSDEPTH_IMAGE - ref,bam mosdepth --by 500 --fast-mode --no-per-base --threads 4 --thresholds 1,5,10,15,20,30,50 md /in/mini.bam at_least "mean depth on chr20" "$(awk '$1 == "chr20" {print $4}' md.mosdepth.summary.txt)" 15 # Zenodo data (homology table and models, ~50 MB) is downloaded in the row, as setup.sh --parascopy-data does. @@ -49,7 +49,7 @@ PARASCOPY_IMAGE net,also=PARASCOPY_DATA_VERSION fullref,slice sh /smoke/parascop # Paraphase reads PacBio HiFi only: the row runs its own demo, the HG01175 SMN1/SMN2 HiFi reads in the release's tests/data, # whose result docs/demo.md of that release publishes: two SMN1, one SMN2, one SMN with exons 7-8 deleted. # busybox wget does not validate TLS, so the two files are checked against their sha256 in Paraphase v4.1.0. -PARAPHASE_IMAGE net fullref v=${PARAPHASE_IMAGE##*:} && u=https://raw.githubusercontent.com/PacificBiosciences/paraphase/v${v%%--*}/tests/data/bams/HG01175_smn1_extracted.bam && wget -q -O hg.bam "$u" && wget -q -O hg.bam.bai "${u}.bai" && { printf '%s hg.bam\n%s hg.bam.bai\n' c9659364ad9e60e41ee4771cf92dc017313b6b75a1e08df3e8c0e95ef38eadca 8b5dd238fdf1cc508e85105ff2a157a2c3d73aa3f441d08afdba9458beecca51 | sha256sum -c || { echo 'the demo BAM or its index is not the file Paraphase v4.1.0 ships'; exit 1; }; } && paraphase -b hg.bam -r /in/ref.fa -o pp -p HG01175 -g smn1 -t 4 && cp pp/HG01175.paraphase.json smn1.json && rm -rf pp hg.bam hg.bam.bai jqcheck "SMN1, SMN2 and SMN exon 7-8 deletion copy numbers of HG01175 (2, 1, 1)" smn1.json '.smn1.region_specific_info | [.smn1_cn, .smn2_cn, .smn_del78_cn] == [2, 1, 1]' +PARAPHASE_IMAGE net fullref v=${PARAPHASE_IMAGE##*:} && u=https://raw.githubusercontent.com/PacificBiosciences/paraphase/v${v%%--*}/tests/data/bams/HG01175_smn1_extracted.bam && wget -q -O hg.bam "$u" && wget -q -O hg.bam.bai "${u}.bai" && { printf '%s hg.bam\n%s hg.bam.bai\n' c9659364ad9e60e41ee4771cf92dc017313b6b75a1e08df3e8c0e95ef38eadca 8b5dd238fdf1cc508e85105ff2a157a2c3d73aa3f441d08afdba9458beecca51 | sha256sum -c || { echo 'the demo BAM or its index is not the file Paraphase v4.1.0 ships'; exit 1; }; } && paraphase -b hg.bam -r /in/ref.fa -o pp -p HG01175 -g smn1 -t 4 && cp pp/HG01175.paraphase.json smn1.json && rm -rf pp hg.bam hg.bam.bai jqcheck "SMN1, SMN2 and SMN exon 7-8 deletion copy numbers of HG01175 (2, 1, 1)" smn1.json '.smn1.region_specific_info | [.smn1_cn, .smn2_cn, .smn_del78_cn] | all(.[]; type == "number") and . == [2, 1, 1]' # MEGAnE's call needs a whole chromosome: on mini.fa's 500 kb it stops in its own code (IndexError). The e2e case mei-1 runs it on chr20. MEGANE_IMAGE - ref build_kmerset -fa /in/mini.fa -prefix mini -outdir /out/mk && { call_genotype_38 -h > help.txt 2>&1 || true; } at_least "repeated 32-mers in the k-mer set of mini.fa" "$(cat mk/mini.mi)" 1000; same "the .mk holds the 8-byte k-mers the .mi counts" "$(( $(wc -c < mk/mini.mk) / 8 ))" "$(cat mk/mini.mi)"; contains "call_genotype_38 takes the -mainchr step 50 passes" help.txt "-mainchr"; contains "and -no_sex_chr" help.txt "-no_sex_chr"; contains "and -only_ins" help.txt "-only_ins" # Step 52 on VNtyper's own test data, two BAMs read out of its Zenodo archive by range requests (tests/smoke/vntyper_testdata.py, @@ -66,7 +66,7 @@ WHATSHAP_IMAGE - - H='##fileformat=VCFv4.2\n##contig=\n# HAP_IBD_IMAGE - - awk 'BEGIN { srand(7); printf "##fileformat=VCFv4.2\n##contig=\n##FORMAT=\n#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\tFORMAT\tA\tB\tC\tD\n" > "t.vcf"; for (j = 1; j <= 2000; j++) { pos = j * 10000; s = (rand() < 0.5); b1 = (j >= 500 && j <= 1500) ? s : (rand() < 0.5); printf "chr20\t%d\t.\tA\tG\t.\tPASS\t.\tGT\t%d|%d\t%d|%d\t%d|%d\t%d|%d\n", pos, s, (rand() < 0.5), b1, (rand() < 0.5), (rand() < 0.5), (rand() < 0.5), (rand() < 0.5), (rand() < 0.5) > "t.vcf"; printf "chr20\t.\t%.4f\t%d\n", pos / 1000000, pos > "g.map" } }' && hap-ibd gt=t.vcf map=g.map out=o > hapibd.log 2>&1 smoke_check "hap-ibd finds the planted 10 cM segment A and B share, and no other" hap-ibd o.ibd.gz GATK_IMAGE - ref,bam,truth gatk --java-options -Xmx4g HaplotypeCaller -R /in/mini.fa -I /in/mini.bam -O gatk.vcf.gz -L chr20:1-449000 --native-pair-hmm-threads 4 -ERC NONE at_least "SNV recall against the GIAB truth" "$(snv_recall gatk.vcf.gz)" 0.9 PICARD_IMAGE - ref,truth,chain java -Xmx2g -jar /usr/picard/picard.jar LiftoverVcf I=/in/truth.vcf.gz O=lift.vcf.gz CHAIN=/in/identity.chain R=/in/mini.fa REJECT=rejected.vcf.gz WARN_ON_MISSING_CONTIG=true same "records lifted over an identity chain" "$(vcf_records lift.vcf.gz)" "$(vcf_records /in/truth.vcf.gz)" -PYTHON_IMAGE net,also=CYRIUS_VERSION cyrius sh /smoke/cyrius.sh same "Cyrius installed from the hash-locked file" "$(cat installed.txt)" "cyrius ${CYRIUS_VERSION}"; same "the TSV has a row for the sample" "$(tsv_cell HG002_cyp2d6.tsv Sample)" HG002_cyrius; jqcheck "Cyrius read depth from the BAM (median depth above 0)" HG002_cyp2d6.json '.["HG002_cyrius"].Median_depth | tonumber > 0' +PYTHON_IMAGE net,also=CYRIUS_VERSION cyrius sh /smoke/cyrius.sh same "Cyrius installed from the hash-locked file" "$(cat installed.txt)" "cyrius ${CYRIUS_VERSION}"; same "the TSV has a row for the sample" "$(tsv_cell HG002_cyp2d6.tsv Sample)" HG002_cyrius; jqcheck "Cyrius read depth from the BAM (median depth above 0)" HG002_cyp2d6.json '.["HG002_cyrius"].Median_depth | (type == "number" or type == "string") and (tonumber > 0)' PLINK2_IMAGE - vcf sh /smoke/plink2.sh between "score sum over five ALT alleles of weight 1" "$(tsv_cell score.sscore SCORE1_SUM)" 5 10 # Step 26b: two planted groups for ADMIXTURE's supervised fit and the projection of a third person drawn from one of them. ADMIXTURE_IMAGE - - awk 'BEGIN { srand(7); for (j = 1; j <= 500; j++) { pa[j] = 0.1 + 0.8 * rand(); pb[j] = 1 - pa[j]; print 1, "s" j, 0, j * 1000 > "d.map"; print 1, "s" j, 0, j * 1000 > "t.map" }; for (i = 1; i <= 40; i++) { g = (i <= 20) ? "A" : "B"; print g > "d.pop"; printf "f%d i%d 0 0 0 -9", i, i > "d.ped"; for (j = 1; j <= 500; j++) { q = (g == "A") ? pa[j] : pb[j]; printf " %d %d", (rand() < q) ? 1 : 2, (rand() < q) ? 1 : 2 > "d.ped" }; printf "\n" > "d.ped" }; printf "t t 0 0 0 -9" > "t.ped"; for (j = 1; j <= 500; j++) printf " %d %d", (rand() < pa[j]) ? 1 : 2, (rand() < pa[j]) ? 1 : 2 > "t.ped"; printf "\n" > "t.ped" }' && admixture --supervised d.ped 2 > sup.log 2>&1 && cp d.2.P t.2.P.in && admixture -P t.ped 2 > proj.log 2>&1 contains "the supervised run on two planted groups converged" sup.log "Converged"; between "the projected person's share of the group it was drawn from" "$(awk '{print ($1 > $2 ? $1 : $2)}' t.2.Q)" 0.9 1 From 0d6272de4ad4e34c94ed4886c6e3a29878e28110 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 16:16:43 +0100 Subject: [PATCH 29/45] fix(ci): use curl options the pinned VNtyper image supports The real smoke job rejected --retry-all-errors. Keep portable retry flags, the byte-range length check, gzip CRC and size verification, and the pinned SHA-256 checks. Both BAMs still download with their expected hashes. Bead: pgp-bs9u.39 --- tests/smoke/vntyper_testdata.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/smoke/vntyper_testdata.sh b/tests/smoke/vntyper_testdata.sh index 608c81e7e..705f4ea53 100755 --- a/tests/smoke/vntyper_testdata.sh +++ b/tests/smoke/vntyper_testdata.sh @@ -50,7 +50,8 @@ for name in "$@"; do $(member "$name") EOF part="${out}/${name}.part" - curl -fsSL --retry 4 --retry-all-errors --retry-delay 15 -r "${off}-$((off + csize - 1))" -o "${part}.raw" "$URL" + # The pinned VNtyper image's curl does not support --retry-all-errors. + curl -fsSL --retry 4 --retry-delay 15 -r "${off}-$((off + csize - 1))" -o "${part}.raw" "$URL" got=$(wc -c < "${part}.raw" | tr -d ' ') [ "$got" -eq "$csize" ] || { echo "${path}: got ${got} bytes, want ${csize} (the server ignored the range?)" >&2; exit 1; } # gzip member: magic, deflate, no flags, no time, no extra flags, unknown OS; From b7be2494e541e8215a1bfddc5c6f8fe1f55c2529 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 16:17:20 +0100 Subject: [PATCH 30/45] test(traits): restore independent expectations and output refusal controls --- rust/crates/og-steps/src/traits.rs | 39 +++++++++- tests/test_traits_report.py | 121 ++++++++++++++++++++++++++++- 2 files changed, 155 insertions(+), 5 deletions(-) diff --git a/rust/crates/og-steps/src/traits.rs b/rust/crates/og-steps/src/traits.rs index d537e672a..a0f323c3c 100644 --- a/rust/crates/og-steps/src/traits.rs +++ b/rust/crates/og-steps/src/traits.rs @@ -1435,8 +1435,43 @@ mod tests { ); } + + #[test] + fn catalog_quotes_are_literal_and_embedded_tabs_are_columns() { + let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../../assets"); + let text = std::fs::read_to_string(root.join("traits.tsv")).unwrap(); + let rules = std::fs::read_to_string(root.join("genotype_rules.tsv")).unwrap(); + let scratch = Scratch::new(); + let path = scratch.put("quoted-field", &text.replacen("\tMCM6\t", "\t\"MCM6\"\t", 1)); + assert!(check_rows(&read_table(&path).unwrap()).iter().any(|e| e.contains("quote"))); + let path = scratch.put("quoted-rule", &rules.replacen("\tAPOE\t", "\t\"APOE\"\t", 1)); + assert!(!check_rules(&read_rules(&path).unwrap(), None).is_empty()); + let path = scratch.put("embedded-tab", &text.replacen("\tMCM6\t", "\t\"MCM6\textra\"\t", 1)); + assert!(read_table(&path).unwrap_err().contains("14 columns, expected 13")); + } + + #[test] + fn directory_destinations_keep_both_previous_tables() { + let scratch = Scratch::new(); + let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../../assets"); + let gt = scratch.put("g", ""); + for directory_key in ["out", "rules-out"] { + let previous = scratch.put("previous.tsv", "previous complete table\n"); + let mut opts: HashMap = [ + ("table", root.join("traits.tsv").to_string_lossy().into_owned()), + ("rules", root.join("genotype_rules.tsv").to_string_lossy().into_owned()), + ("genotypes", gt.clone()), ("sample", "S".into()), + ("out", previous.clone()), ("rules-out", previous.clone()), + ].into_iter().map(|(k,v)| (k.into(),v)).collect(); + opts.insert(directory_key.into(), scratch.0.to_string_lossy().into_owned()); + assert!(run("table", &opts).unwrap_err().contains("cannot write")); + assert_eq!(std::fs::read_to_string(&previous).unwrap(), "previous complete table\n", "{directory_key}"); + assert_eq!(std::fs::read_dir(&scratch.0).unwrap().count(), 2); + } + } + #[test] - fn csv_header_unicode_rules_and_atomic_write_errors() { + fn plain_header_unicode_rules_and_atomic_write_errors() { let scratch = Scratch::new(); let mut text = COLUMNS .iter() @@ -1445,7 +1480,7 @@ mod tests { .join("\t"); text.push_str("\r\n"); let path = scratch.put("quoted", &text); - assert!(read_table(&path).unwrap().is_empty()); + assert!(read_table(&path).unwrap_err().contains("unexpected traits header")); let path = scratch.put("r", "rule\té\tG\tT\tno\tyes\tN\t1\n"); assert!(!check_rules(&read_rules(&path).unwrap(), None).is_empty()); assert!(stage_tsv(scratch.0.join("absent/x").to_str().unwrap(), OUT, &[]).is_err()); diff --git a/tests/test_traits_report.py b/tests/test_traits_report.py index 2414935bc..b93c21555 100644 --- a/tests/test_traits_report.py +++ b/tests/test_traits_report.py @@ -104,7 +104,9 @@ def planted(desc, new, want): "CAD9p21": [(("G/G", "C/C"), "two risk copies"), (("A/G", "G/C"), "one risk copy")], "MAPT": [(("G/G", "G/G"), "H2/H2"), (("A/G", "A/G"), "H1/H2")], } -# The label of the reference blocks alone (every site homozygous for the GRCh38 base). +# Independent labels for homozygous GRCh38 reference blocks. +MULTI_SNP_REF = {"APOL1": "G0/G0", "SERPINA1": "PiMM", "HFE": "wt/wt", "GC": "Gc1F/Gc1F", + "CAD9p21": "no risk copy", "MAPT": "H1/H1"} @@ -113,8 +115,50 @@ def says_not_proof(message): return "does not prove 2+0" in message +def marker_errors(name, m=None, dbsnp=None): + """Problems of one marker (empty when it is consistent): the dbSNP record + is one of its copies with its ref and alt, on the plus strand and not + swapped; every copy sits in the middle of the same context, whose middle + base is the ref.""" + m = tr.MARKERS[name] if m is None else m + rs, chrom, pos, ref, alt = m["dbsnp"] if dbsnp is None else dbsnp + comp = {"A": "T", "C": "G", "G": "C", "T": "A"} + errs = [] + if chrom != m["chrom"] or pos not in m["copies"]: + errs.append(f"{name}: dbSNP places {rs} at {chrom}:{pos}, not at one of the marker's copies") + if (m["ref"], m["alt"]) == (alt, ref): + errs.append(f"{name}: ref and alt are swapped (marker {m['ref']}>{m['alt']}, dbSNP {ref}>{alt})") + elif (m["ref"], m["alt"]) == (comp.get(ref), comp.get(alt)): + errs.append(f"{name}: the alleles are the minus-strand bases (marker {m['ref']}>{m['alt']}, " + f"dbSNP {ref}>{alt}): write the plus-strand bases") + elif (m["ref"], m["alt"]) != (ref, alt): + errs.append(f"{name}: marker {m['ref']}>{m['alt']}, dbSNP {ref}>{alt}") + ctx = m["context"] + if len(ctx) % 2 != 1 or ctx[len(ctx) // 2] != m["ref"]: + errs.append(f"{name}: the middle base of the context is not the ref {m['ref']}") + if len(set(m["copies"])) != len(m["copies"]): + errs.append(f"{name}: a copy is listed twice") + return errs + + def marker_checks(): """11. The copy-aware markers.""" + for name, m in tr.MARKERS.items(): + check(f"marker {name}: the pinned dbSNP record agrees with the marker", marker_errors(name) == [], + marker_errors(name)) + rs, chrom, pos, ref, alt = m["dbsnp"] + comp = {"A": "T", "C": "G", "G": "C", "T": "A"} + e = marker_errors(name, dict(m, ref=alt, alt=ref)) + check(f"marker {name} control: ref and alt swapped is caught", any("swapped" in x for x in e), e) + e = marker_errors(name, dict(m, ref=comp[ref], alt=comp[alt], + context=m["context"][:10] + comp[ref] + m["context"][11:])) + check(f"marker {name} control: the minus-strand bases are caught", any("minus-strand" in x for x in e), e) + e = marker_errors(name, dbsnp=(rs, chrom, pos + 1, ref, alt)) + check(f"marker {name} control: a dbSNP position off the copies is caught", + any("not at one of the marker's copies" in x for x in e), e) + e = marker_errors(name, dict(m, context=m["context"][:10] + alt + m["context"][11:])) + check(f"marker {name} control: a context whose middle base is not the ref is caught", + any("middle base" in x for x in e), e) check("LPA_KIV2_4925: seven copies (six KIV-2 units and KIV-1)", len(tr.MARKERS["LPA_KIV2_4925"]["copies"]) == 7) if not OG: print("skip: OG is not set (OG=rust/target/release/og): og cn marker-regions and marker-read did not run") @@ -231,6 +275,44 @@ def og_cn(*args): return subprocess.run([OG, "cn", *args], capture_output=True, text=True) + +def plain_tsv_checks(): + """Reject CSV quoting before it can produce a table the collector cannot read.""" + import collect_summary as cs + with tempfile.TemporaryDirectory() as work: + folder = os.path.join(work, "traits") + os.makedirs(folder) + g = os.path.join(work, "g.tsv") + write(g, "") + out, rout = os.path.join(folder, "S_traits.tsv"), os.path.join(folder, "S_genotype_rules.tsv") + source = open(TABLE).read() + rules_source = open(RULES).read() + for name, text, rules_text in [ + ("quoted header", source.replace("chrom\t", '\"chrom\"\t', 1), rules_source), + ("quoted catalog field", source.replace("\tMCM6\t", '\t\"MCM6\"\t', 1), rules_source), + ("quoted rule text", source, rules_source.replace("\tAPOE\t", '\t\"APOE\"\t', 1)), + ("embedded tab", source.replace("\tMCM6\t", '\t\"MCM6\textra\"\t', 1), rules_source), + ]: + # Keep complete, collector-readable outputs from the previous run. + traits_tables(g, out, rout) + previous = open(out, "rb").read(), open(rout, "rb").read() + bad, badrules = os.path.join(work, "bad.tsv"), os.path.join(work, "badrules.tsv") + write(bad, text) + write(badrules, rules_text) + res = subprocess.run([OG, "traits", "table", "--table", bad, "--rules", badrules, + "--genotypes", g, "--sample", "S", "--out", out, + "--rules-out", rout], capture_output=True, text=True) + check(f"plain TSV control: {name} is refused", res.returncode == 1, res.stderr) + check(f"plain TSV control: {name} preserves both outputs", + previous == (open(out, "rb").read(), open(rout, "rb").read())) + try: + _, collected = cs.sec_traits(work, "S") + readable = bool(collected["rows"]) + except cs.Unreadable: + readable = False + check(f"plain TSV control: {name} leaves collector-readable output", readable) + + def main(): rules = {} with open(RULES) as f: @@ -238,15 +320,18 @@ def main(): if cells and cells[0] == "site": rules.setdefault(cells[1], {"sites": []})["sites"].append(cells[2:]) dbsnp_checks(rules) + marker_checks() if not OG: check("OG_REQUIRED needs the og binary", not os.environ.get("OG_REQUIRED")) print("skip: OG is not set: traits CLI and cn marker checks did not run") return int(bool(FAILS)) + plain_tsv_checks() with tempfile.TemporaryDirectory() as work: def table(records): g = os.path.join(work, "g.tsv") write(g, "".join("\t".join(rec) + "\n" for rec in records)) - out, rout = os.path.join(work, "traits.tsv"), os.path.join(work, "rules.tsv") + os.makedirs(os.path.join(work, "traits"), exist_ok=True) + out, rout = os.path.join(work, "traits", "S_traits.tsv"), os.path.join(work, "traits", "S_genotype_rules.tsv") res = subprocess.run([OG, "traits", "table", "--table", TABLE, "--rules", RULES, "--genotypes", g, "--sample", "S", "--out", out, "--rules-out", rout], capture_output=True, text=True) @@ -271,7 +356,37 @@ def table(records): _, lines = table(records) got = next(r for r in lines if r["rule"] == rid) check(f"{rid} {gts}: {label}", got["status"] == "called" and got["result"] == label, got) - marker_checks() + import collect_summary as cs + import render_report as rr + check("every multi-SNP rule is in the catalog", set(MULTI_SNP) <= set(rules)) + for rid, cases in MULTI_SNP.items(): + sites = rules[rid]["sites"] + records = [(s[1], s[2], s[3], ".", f"{s[3]}/{s[3]}", "40") for s in sites] + _, lines = table(records) + got = next(r for r in lines if r["rule"] == rid) + check(f"{rid}, reference blocks only: {MULTI_SNP_REF[rid]}", + (got["status"], got["result"]) == ("called", MULTI_SNP_REF[rid]), got) + gts, want = cases[0] + a, b = gts[0].split("/") + s0 = sites[0] + flip = s0[3] if b == s0[4] else s0[4] + flipped = (f"{a}/{flip}",) + tuple(gts[1:]) + records = [(s[1], s[2], s[3], s[4], gt) for s, gt in zip(sites, flipped)] + _, lines = table(records) + got = next(r for r in lines if r["rule"] == rid) + check(f"control: {rid} with one allele flipped is not {want}", got["result"] != want, got) + # Collect and render the actual CLI output, independently for each rule. + records = [(s[1], s[2], s[3], s[4], gt) for s, gt in zip(sites, gts)] + _, lines = table(records) + read = cs.genotype_rules(work, "S") or [] + page = "\n".join(rr.rules_html({"rules": read}, "S")) + got = next(r for r in lines if r["rule"] == rid) + check(f"HTML report: {rid} with result {want}", rr.E(got["trait"]) in page and rr.E(want) in page, page[:300]) + records = [(s[1], s[2], s[3], s[4], gt) for s, gt in zip(rules["APOL1"]["sites"], ("A/G", "T/G"))] + _, lines = table(records) + reading = next(r for r in lines if r["rule"] == "APOL1")["reading"] + check("APOL1 G0/G1 says G2 is not read and the other copy could carry it", + "G2 is not read" in reading and "G1/G2" in reading, reading) print("RESULT:", "ALL PASS" if not FAILS else f"{FAILS} FAILED") return int(bool(FAILS)) From d0c12a52666fe22a01112b095ab9df2d2f129c77 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 16:22:13 +0100 Subject: [PATCH 31/45] fix(traits): preserve plain TSV and reject directory outputs before publication --- .github/workflows/guard.yml | 37 +++++++ CHANGELOG.md | 2 +- rust/crates/og-steps/src/traits.rs | 104 +++++++++++++------- tests/parity/evidence/rust-c2b-controls.txt | 40 +++++++- tests/test_traits_report.py | 2 +- 5 files changed, 144 insertions(+), 41 deletions(-) diff --git a/.github/workflows/guard.yml b/.github/workflows/guard.yml index 66800c3a5..d2a8d74a2 100644 --- a/.github/workflows/guard.yml +++ b/.github/workflows/guard.yml @@ -386,6 +386,43 @@ jobs: done exit "$fail" + - name: Traits independent expectations reject changed reference labels and marker metadata + working-directory: . + env: + OG: ${{ github.workspace }}/bin/og + OG_REQUIRED: 1 + run: | + python3 - <<'PY' + import contextlib, io, pathlib, runpy, tempfile + test = runpy.run_path("tests/test_traits_report.py")["main"].__globals__ + original = pathlib.Path(test["RULES"]).read_text() + with tempfile.TemporaryDirectory() as work: + for rid, key, label in [("APOL1", "G0/G0", "G0/G0"), ("CAD9p21", "nonrisk/nonrisk", "no risk copy")]: + path = pathlib.Path(work) / "rules.tsv" + old = f"combination\t{rid}\t{key}\t{label}\t" + assert old in original + path.write_text(original.replace(old, f"combination\t{rid}\t{key}\tWRONG_REFERENCE\t")) + test["RULES"], test["FAILS"] = str(path), 0 + log = io.StringIO() + with contextlib.redirect_stdout(log): + rc = test["main"]() + print(log.getvalue()) + assert rc == 1 and test["FAILS"] == 1 + assert f"[FAIL] {rid}, reference blocks only: {label}" in log.getvalue() + print(f"Control: {rid} WRONG_REFERENCE failed its independent expectation") + marker = test["tr"].MARKERS["SMN1_27134"] + rs, chrom, pos, ref, alt = marker["dbsnp"] + marker["dbsnp"] = rs, chrom, pos + 1, ref, alt + test["FAILS"] = 0 + log = io.StringIO() + with contextlib.redirect_stdout(log): + test["marker_checks"]() + print(log.getvalue()) + assert test["FAILS"] == 1 + assert "[FAIL] marker SMN1_27134: the pinned dbSNP record agrees" in log.getvalue() + print("Control: retained SMN1 off-copy metadata failed its independent expectation") + PY + # The fake-docker job builds no og, so its cn-engine-calls case says it # is skipped there; here bin/og is built and the case must run. - name: The fake-docker case cn-engine-calls (steps 35, 41 and 46 through og cn) with og diff --git a/CHANGELOG.md b/CHANGELOG.md index b6245e959..3e56f60b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ Each release's notes. From 1.0.0 on, a major or minor release has hand-written n ## Unreleased -- Step 42 reads traits and genotype rules with `og traits table`, exports WhatsHap sites with `og traits phase-sites`, and checks the catalogs with `og traits check`. The replaced Python evaluator is removed. Report columns and readings stay the same. Genotypes outside the catalog are streamed past, and write errors now report a controlled error with the path instead of a traceback. Both tables are staged before either is published, so an unwritable second output leaves the previous traits table intact. +- Step 42 reads traits and genotype rules with `og traits table`, exports WhatsHap sites with `og traits phase-sites`, and checks the catalogs with `og traits check`. The replaced Python evaluator is removed. Report columns and readings stay the same. Genotypes outside the catalog are streamed past, and write errors now report a controlled error with the path instead of a traceback. Both tables are staged before either is published. A staging failure or a directory destination leaves both previous tables intact. Each file is then replaced by a separate atomic rename. The pair is not a transaction: a failure of the second rename can leave the first table updated. Catalogs and output remain plain TSV; quotes never hide a tab or bypass validation. - The unused Python copy-number engine is removed. Its historical catalog tests materialise a pinned test oracle and keep testing the Rust commands. diff --git a/rust/crates/og-steps/src/traits.rs b/rust/crates/og-steps/src/traits.rs index a0f323c3c..fa6fef4b1 100644 --- a/rust/crates/og-steps/src/traits.rs +++ b/rust/crates/og-steps/src/traits.rs @@ -1,7 +1,6 @@ //! Step 42: bounded site genotypes, haplotype rules and read-backed phase. use indexmap::IndexMap; use og_io::{AtomicFile, Lines, Utf8}; -use pycompat::csv::{Quoting, Reader, Writer}; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::io::Write; @@ -111,7 +110,7 @@ fn row(keys: &[&str], values: &[String]) -> Row { .collect() } fn records(path: &str) -> Result)>> { - // Parse even the header as a CSV record. Comments do not enter the parser. + // Catalogs are plain TSV: quotes are literal and never hide a tab. let mut lines = Lines::new( og_io::open(path).map_err(|e| og_filters::os_error(&e, path))?, Utf8::Strict, @@ -124,14 +123,11 @@ fn records(path: &str) -> Result)>> { if text.trim_end_matches('\n').is_empty() || text.starts_with('#') { continue; } - let cells = Reader::new( - std::iter::once(Ok::<_, std::io::Error>(text.clone())), - '\t', - Quoting::Minimal, - ) - .next() - .ok_or("empty record")? - .map_err(|e| e.to_string())?; + let cells = text + .trim_end_matches('\n') + .split('\t') + .map(str::to_owned) + .collect(); out.push((number, cells)); } Ok(out) @@ -857,16 +853,10 @@ fn rule_line(r: &Rule, gt: &Genotypes, phased: Option<&Phased>) -> Row { } fn stage_tsv(path: &str, columns: &[&str], rows: &[Row]) -> Result { let mut file = AtomicFile::create(path).map_err(|e| format!("cannot write {path}: {e}"))?; - { - let mut writer = Writer::new(&mut file, '\t'); - writer - .write_row(columns.iter().copied()) - .map_err(|e| format!("cannot write {path}: {e}"))?; - for row in rows { - writer - .write_row(columns.iter().map(|k| row[*k].as_str())) - .map_err(|e| format!("cannot write {path}: {e}"))?; - } + writeln!(file, "{}", columns.join("\t")).map_err(|e| format!("cannot write {path}: {e}"))?; + for row in rows { + let fields: Vec<_> = columns.iter().map(|k| row[*k].as_str()).collect(); + writeln!(file, "{}", fields.join("\t")).map_err(|e| format!("cannot write {path}: {e}"))?; } file.flush() .map_err(|e| format!("cannot write {path}: {e}"))?; @@ -1048,6 +1038,13 @@ fn run(verb: &str, opts: &HashMap) -> Result { // Compute both tables before opening either output. Each rename is atomic. let out_path = required("out")?; let rules_path = required("rules-out")?; + // Refuse known rename failures before either previous output is replaced. + // Publication still uses two renames, not one transaction. + for path in [out_path, rules_path] { + if std::path::Path::new(path).is_dir() { + return Err(format!("cannot write {path}: destination is a directory")); + } + } let traits_file = stage_tsv(out_path, OUT, &out)?; let rules_file = stage_tsv(rules_path, RULE_OUT, &lines)?; traits_file @@ -1435,19 +1432,35 @@ mod tests { ); } - #[test] fn catalog_quotes_are_literal_and_embedded_tabs_are_columns() { let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../../assets"); let text = std::fs::read_to_string(root.join("traits.tsv")).unwrap(); let rules = std::fs::read_to_string(root.join("genotype_rules.tsv")).unwrap(); let scratch = Scratch::new(); - let path = scratch.put("quoted-field", &text.replacen("\tMCM6\t", "\t\"MCM6\"\t", 1)); - assert!(check_rows(&read_table(&path).unwrap()).iter().any(|e| e.contains("quote"))); - let path = scratch.put("quoted-rule", &rules.replacen("\tAPOE\t", "\t\"APOE\"\t", 1)); + let path = scratch.put( + "quoted-field", + &text.replacen("\tMCM6\t", "\t\"MCM6\"\t", 1), + ); + assert!( + check_rows(&read_table(&path).unwrap()) + .iter() + .any(|e| e.contains("quote")) + ); + let path = scratch.put( + "quoted-rule", + &rules.replacen("\tAPOE\tAPOE\t", "\tAPOE\t\"APOE\"\t", 1), + ); assert!(!check_rules(&read_rules(&path).unwrap(), None).is_empty()); - let path = scratch.put("embedded-tab", &text.replacen("\tMCM6\t", "\t\"MCM6\textra\"\t", 1)); - assert!(read_table(&path).unwrap_err().contains("14 columns, expected 13")); + let path = scratch.put( + "embedded-tab", + &text.replacen("\tMCM6\t", "\t\"MCM6\textra\"\t", 1), + ); + assert!( + read_table(&path) + .unwrap_err() + .contains("14 columns, expected 13") + ); } #[test] @@ -1458,14 +1471,33 @@ mod tests { for directory_key in ["out", "rules-out"] { let previous = scratch.put("previous.tsv", "previous complete table\n"); let mut opts: HashMap = [ - ("table", root.join("traits.tsv").to_string_lossy().into_owned()), - ("rules", root.join("genotype_rules.tsv").to_string_lossy().into_owned()), - ("genotypes", gt.clone()), ("sample", "S".into()), - ("out", previous.clone()), ("rules-out", previous.clone()), - ].into_iter().map(|(k,v)| (k.into(),v)).collect(); - opts.insert(directory_key.into(), scratch.0.to_string_lossy().into_owned()); + ( + "table", + root.join("traits.tsv").to_string_lossy().into_owned(), + ), + ( + "rules", + root.join("genotype_rules.tsv") + .to_string_lossy() + .into_owned(), + ), + ("genotypes", gt.clone()), + ("sample", "S".into()), + ("out", previous.clone()), + ("rules-out", previous.clone()), + ] + .into_iter() + .map(|(k, v)| (k.into(), v)) + .collect(); + opts.insert( + directory_key.into(), + scratch.0.to_string_lossy().into_owned(), + ); assert!(run("table", &opts).unwrap_err().contains("cannot write")); - assert_eq!(std::fs::read_to_string(&previous).unwrap(), "previous complete table\n", "{directory_key}"); + assert!( + std::fs::read_to_string(&previous).unwrap() == "previous complete table\n", + "{directory_key}: previous output changed" + ); assert_eq!(std::fs::read_dir(&scratch.0).unwrap().count(), 2); } } @@ -1480,7 +1512,11 @@ mod tests { .join("\t"); text.push_str("\r\n"); let path = scratch.put("quoted", &text); - assert!(read_table(&path).unwrap_err().contains("unexpected traits header")); + assert!( + read_table(&path) + .unwrap_err() + .contains("unexpected traits header") + ); let path = scratch.put("r", "rule\té\tG\tT\tno\tyes\tN\t1\n"); assert!(!check_rules(&read_rules(&path).unwrap(), None).is_empty()); assert!(stage_tsv(scratch.0.join("absent/x").to_str().unwrap(), OUT, &[]).is_err()); diff --git a/tests/parity/evidence/rust-c2b-controls.txt b/tests/parity/evidence/rust-c2b-controls.txt index bed580b72..9637cfe02 100644 --- a/tests/parity/evidence/rust-c2b-controls.txt +++ b/tests/parity/evidence/rust-c2b-controls.txt @@ -19,7 +19,7 @@ failures: ---- traits::tests::apoe_grid_keeps_haplotype_order_and_missing_calls stdout ---- -thread 'traits::tests::apoe_grid_keeps_haplotype_order_and_missing_calls' (4288523) panicked at crates/og-steps/src/traits.rs:1242:17: +thread 'traits::tests::apoe_grid_keeps_haplotype_order_and_missing_calls' (4288523) panicked at traits.rs (line 1242, column 17): assertion `left == right` failed left: "other_haplotype" right: "called" @@ -27,12 +27,12 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace ---- traits::tests::stream_prefers_first_snv_but_checks_every_ref_and_bounds_memory stdout ---- -thread 'traits::tests::stream_prefers_first_snv_but_checks_every_ref_and_bounds_memory' (4288529) panicked at crates/og-steps/src/traits.rs:1336:45: +thread 'traits::tests::stream_prefers_first_snv_but_checks_every_ref_and_bounds_memory' (4288529) panicked at traits.rs (line 1336, column 45): called `Result::unwrap_err()` on an `Ok` value: {("chr2", "135851076"): SiteGt { alleles: ["G", "A"], status: Called, gq: "42", snv: true }} ---- traits::tests::phase_requires_one_block_and_only_multiple_hets_are_exported stdout ---- -thread 'traits::tests::phase_requires_one_block_and_only_multiple_hets_are_exported' (4288526) panicked at crates/og-steps/src/traits.rs:1390:9: +thread 'traits::tests::phase_requires_one_block_and_only_multiple_hets_are_exported' (4288526) panicked at traits.rs (line 1390, column 9): assertion `left == right` failed left: 0 right: 3 @@ -41,12 +41,12 @@ assertion `left == right` failed ERROR: /Volumes/Data/repos/personal/.worktrees/pgp-rust-c2b/rust/crates/og-steps/../../../assets/genotype_rules.tsv: rule APOE (line 19): no combination line for e2/e4|e1/e3 ERROR: /Volumes/Data/repos/personal/.worktrees/pgp-rust-c2b/rust/crates/og-steps/../../../assets/genotype_rules.tsv: rule APOE (line 19): the combination line e1/e3|e2/e4 can never be read (a rule that needs phase reads one diplotype) -thread 'traits::tests::unwritable_rules_output_keeps_existing_traits_table' (4288530) panicked at crates/og-steps/src/traits.rs:1433:37: +thread 'traits::tests::unwritable_rules_output_keeps_existing_traits_table' (4288530) panicked at traits.rs (line 1433, column 37): called `Result::unwrap_err()` on an `Ok` value: 1 ---- traits::tests::committed_catalogs_and_every_rule_genotype stdout ---- -thread 'traits::tests::committed_catalogs_and_every_rule_genotype' (4288524) panicked at crates/og-steps/src/traits.rs:1216:9: +thread 'traits::tests::committed_catalogs_and_every_rule_genotype' (4288524) panicked at traits.rs (line 1216, column 9): assertion `left == right` failed left: ["rule APOE (line 19): no combination line for e2/e4|e1/e3", "rule APOE (line 19): the combination line e1/e3|e2/e4 can never be read (a rule that needs phase reads one diplotype)"] right: [] @@ -67,3 +67,33 @@ error: 1 target failed: test traits::tests::apoe_grid_keeps_haplotype_order_and_missing_calls ... FAILED test traits::tests::stream_prefers_first_snv_but_checks_every_ref_and_bounds_memory ... FAILED test traits::tests::phase_requires_one_block_and_only_multiple_hets_are_exported ... FAILED + +Fix round 1: local red-first controls, Rust 1.93.0, synthetic inputs only. +The tests-only commit b7be2494 leaves production code unchanged. +Command: cargo test --locked -p og-steps traits::tests --no-fail-fast +test traits::tests::catalog_quotes_are_literal_and_embedded_tabs_are_columns ... FAILED +test traits::tests::plain_header_unicode_rules_and_atomic_write_errors ... FAILED +test traits::tests::directory_destinations_keep_both_previous_tables ... FAILED + +Command: OG_REQUIRED=1 python3 tests/test_traits_report.py (unfixed binary) +[FAIL] plain TSV control: quoted header is refused -- +[FAIL] plain TSV control: quoted catalog field is refused -- +[FAIL] plain TSV control: quoted rule text is refused -- +[FAIL] plain TSV control: embedded tab is refused -- +[FAIL] plain TSV control: embedded tab preserves both outputs -- +[FAIL] plain TSV control: embedded tab leaves collector-readable output -- + +Independent reference controls: a scratch copy of genotype_rules.tsv changes +only the reference label to WRONG_REFERENCE. The actual og tables must still +match the six fixed labels in tests/test_traits_report.py. +APOL1 G0/G0 changed: the APOL1 reference assertion fails. +CAD9p21 no risk copy changed: the CAD9p21 reference assertion fails. +SMN1 retained metadata: dbSNP position moved one base off its copies. +The pinned dbSNP agreement assertion fails. +Guard now repeats these three controls, requires the exact failed assertion +and exactly one failure per mutation, and keeps the catalogs unchanged. + +After fixes: all 10 traits Rust tests pass. The CLI controls refuse quoted +headers, quoted fields, quoted rule text and embedded tabs. The previous +outputs survive; collect_summary reads the unchanged table with QUOTE_NONE. +Both directory-destination controls preserve the other previous output. diff --git a/tests/test_traits_report.py b/tests/test_traits_report.py index b93c21555..cf55a9599 100644 --- a/tests/test_traits_report.py +++ b/tests/test_traits_report.py @@ -290,7 +290,7 @@ def plain_tsv_checks(): for name, text, rules_text in [ ("quoted header", source.replace("chrom\t", '\"chrom\"\t', 1), rules_source), ("quoted catalog field", source.replace("\tMCM6\t", '\t\"MCM6\"\t', 1), rules_source), - ("quoted rule text", source, rules_source.replace("\tAPOE\t", '\t\"APOE\"\t', 1)), + ("quoted rule text", source, rules_source.replace("\tAPOE\tAPOE\t", '\tAPOE\t\"APOE\"\t', 1)), ("embedded tab", source.replace("\tMCM6\t", '\t\"MCM6\textra\"\t', 1), rules_source), ]: # Keep complete, collector-readable outputs from the previous run. From 80da08835fb7b223c681a7cf60d3a7a7fa2eb6ef Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 17:33:35 +0100 Subject: [PATCH 32/45] fix(ci): keep og setup logs out of captured smoke values --- CHANGELOG.md | 2 +- scripts/ci/image-smoke.sh | 37 +++++++++++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79e7ba9e4..60875d363 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ Each release's notes. From 1.0.0 on, a major or minor release has hand-written n ## Unreleased -- The shared `og-io` line reader bounds memory by the longest record for CR-only files as well as LF and CRLF files. Smoke coordinates require signed integers, so decimal and nonfinite coordinates fail. Trace integer cells accept whitespace, signs and underscores but deliberately refuse Unicode digits and negative resources. Integers outside the target range fail. Duplicate task rows retain concrete exits. Smoke JSON checks require one document, numeric counts and copy numbers, array catalogs and string genotypes. Numeric depth strings stay supported; booleans are refused. The array and genotype rules deliberately reject malformed values Python accepted. Smoke fixtures build a missing default `og` on first use. +- The shared `og-io` line reader bounds memory by the longest record for CR-only files as well as LF and CRLF files. Smoke coordinates require signed integers, so decimal and nonfinite coordinates fail. Trace integer cells accept whitespace, signs and underscores but deliberately refuse Unicode digits and negative resources. Integers outside the target range fail. Duplicate task rows retain concrete exits. Smoke JSON checks require one document, numeric counts and copy numbers, array catalogs and string genotypes. Numeric depth strings stay supported; booleans are refused. The array and genotype rules deliberately reject malformed values Python accepted. Smoke fixtures build a missing default `og` on first use. Setup output goes to stderr so captured PharmCAT counts and CSV values stay clean. - CI readers now use `og dev pharmcat diplotypes`, `nf-trace` and `smoke`. Unreadable PharmCAT reports fail with exit 2 and are marked not assessed. Resource checks refuse trace globs and malformed numeric cells. The smoke mini-reference uses a fixed Rust seed for the planted bases. Gzip smoke reads have deterministic headers. Smoke table checks report concise property results. - The container helpers check (`scripts/ci/check-container-helpers.sh`, Guard job `helper-binaries`) now derives its table with the Rust `og` binary, `og dev helpers`. It replaces the Python inside the script, which is gone; the script keeps the docker probe, the orad download and its self-test. On this tree the table and the rule 1 output are the same, byte for byte. The static run takes 0.01 s and 6 MB where the Python took 0.2 s and 15 MB. What changes: diff --git a/scripts/ci/image-smoke.sh b/scripts/ci/image-smoke.sh index ef21b6a67..3457a5398 100755 --- a/scripts/ci/image-smoke.sh +++ b/scripts/ci/image-smoke.sh @@ -188,11 +188,17 @@ self_test() { cat > "${tmp}/repo/bin/og" <<'SH' #!/usr/bin/env bash [ -f "$(dirname "$0")/../ready" ] || exit 127 -printf '%s\n' "$*" +case "$1 $2 ${3:-}" in + 'dev pharmcat diplotypes') printf '1\n' ;; + 'dev smoke csv-cell') printf '0.9\n' ;; + *) printf '%s\n' "$*" ;; +esac SH cat > "${tmp}/repo/scripts/setup.sh" <<'SH' #!/usr/bin/env bash [ "$1" = --og ] || exit 2 +printf 'Building og ...\n' +printf 'builder diagnostic\n' >&2 printf 'built\n' >> "$(dirname "$0")/../ready" SH chmod +x "${tmp}/repo/bin/og" @@ -211,6 +217,32 @@ SH '); rc=$? if [ "$rc" -eq 0 ]; then echo "[PASS] missing default og is built once; explicit OG failure is retained" else echo "[FAIL] og fixture preparation: $out"; fails=$((fails + 1)); fi + # A value helper can trigger setup inside command substitution. Use the + # real assertions with a noisy builder and no default binary built yet. + rm -f "${tmp}/repo/ready" + # shellcheck disable=SC2016 # variables belong to the isolated test shell + out=$(env -u OG REPO="${tmp}/repo" bash -c "$(declare -f smoke_og pharmcat_called check_pharmcat csv_cell num at_least between pass fail)"' + rc=0 + "$REPO/bin/og" --version >/dev/null 2>&1 || rc=$? + [ "$rc" -eq 127 ] || exit 1 + count=$(pharmcat_called report.json 2> "$REPO/build.log") || exit 1 + [ "$count" = 1 ] || { printf "unexpected count: %s\n" "$count"; exit 1; } + grep -qx "Building og ..." "$REPO/build.log" || exit 1 + grep -qx "builder diagnostic" "$REPO/build.log" || exit 1 + rm -f "$REPO/ready" + CHECK_FAILS=0 + check_pharmcat "first-use PharmCAT count" report.json 2> "$REPO/build.log" + [ "$CHECK_FAILS" -eq 0 ] || exit 1 + [ "$(wc -l < "$REPO/ready")" -eq 1 ] || exit 1 + rm -f "$REPO/ready" + value=$(csv_cell summary.csv Type=SNP,Filter=PASS METRIC.Recall 2> "$REPO/build.log") || exit 1 + [ "$value" = 0.9 ] || { printf "unexpected CSV value: %s\n" "$value"; exit 1; } + between "first-use CSV recall" "$value" 0.85 0.95 + [ "$CHECK_FAILS" -eq 0 ] || exit 1 + [ "$(wc -l < "$REPO/ready")" -eq 1 ] || exit 1 + '); rc=$? + if [ "$rc" -eq 0 ]; then echo "[PASS] first-use PharmCAT and CSV values exclude setup output" + else echo "[FAIL] first-use value capture: $out"; fails=$((fails + 1)); fi rm -rf "$tmp" if [ "$fails" -gt 0 ]; then echo "self-test: ${fails} case(s) failed" >&2; return 1; fi echo "self-test: all cases passed" @@ -544,7 +576,8 @@ smoke_og() { local rc=0 "${OG:-${REPO}/bin/og}" --version >/dev/null 2>&1 || rc=$? if [ "$rc" -eq 127 ] && [ -z "${OG:-}" ]; then - bash "${REPO}/scripts/setup.sh" --og || return 1 + # This can run inside a captured value helper: only og's result is stdout. + bash "${REPO}/scripts/setup.sh" --og >&2 || return 1 elif [ "$rc" -ne 0 ]; then return "$rc" fi From 12ba3afb5f3432af2cb66db9945ed91ebe8f6fb0 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 17:52:23 +0100 Subject: [PATCH 33/45] test(dev): reproduce evidence and Renovate false successes --- rust/crates/og-dev/src/evidence.rs | 133 +++++++++++++++++++++++++++++ rust/crates/og-dev/src/renovate.rs | 46 +++++++++- 2 files changed, 178 insertions(+), 1 deletion(-) diff --git a/rust/crates/og-dev/src/evidence.rs b/rust/crates/og-dev/src/evidence.rs index 84529a8ef..0135488fd 100644 --- a/rust/crates/og-dev/src/evidence.rs +++ b/rust/crates/og-dev/src/evidence.rs @@ -390,6 +390,139 @@ mod tests { ); } + + struct Case { + dir: std::path::PathBuf, + kind: &'static str, + } + + impl Case { + fn new(kind: &'static str, label: &str) -> Self { + let dir = std::env::temp_dir().join(format!( + "og-evidence-{kind}-{label}-{}-{:?}", std::process::id(), std::thread::current().id() + )); + let _ = fs::remove_dir_all(&dir); + fs::create_dir_all(&dir).unwrap(); + let src = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../../tests/parity/inputs").join(format!("dev-evidence-{kind}")); + for e in fs::read_dir(src).unwrap() { + let e = e.unwrap(); + fs::copy(e.path(), dir.join(e.file_name())).unwrap(); + } + Self { dir, kind } + } + fn text(&self, name: &str) -> Vec { + use std::io::Read; + let mut bytes = Vec::new(); + open(self.dir.join(name).to_str().unwrap()).unwrap().read_to_end(&mut bytes).unwrap(); + bytes + } + fn put(&self, name: &str, bytes: &[u8]) { + if name.ends_with(".gz") { + let mut gz = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default()); + gz.write_all(bytes).unwrap(); + fs::write(self.dir.join(name), gz.finish().unwrap()).unwrap(); + } else { + fs::write(self.dir.join(name), bytes).unwrap(); + } + } + fn run(&self) -> Result<(), String> { + let p = |n: &str| self.dir.join(n).to_str().unwrap().to_owned(); + if self.kind == "clashes" { + clashes(&p("raw.vcf.gz"), &p("split.vcf.gz"), self.dir.to_str().unwrap(), &p("out")) + } else { + hg002_gvcf(&p("truth.vcf.gz"), &p("truth.bed"), &p("ref.fasta"), &p("sites.tsv"), &p("out")).map(|_| ()) + } + } + fn fails_without_publication(&self) { + fs::write(self.dir.join("out"), b"PREVIOUS OUTPUT\n").unwrap(); + assert!(self.run().is_err(), "{} must fail", self.dir.display()); + assert_eq!(fs::read(self.dir.join("out")).unwrap(), b"PREVIOUS OUTPUT\n"); + let leftovers: Vec<_> = fs::read_dir(&self.dir).unwrap().filter_map(|e| { + let name = e.unwrap().file_name(); + name.to_str().filter(|n| n.starts_with(".out.")).map(str::to_owned) + }).collect(); + assert!(leftovers.is_empty(), "{leftovers:?}"); + } + } + impl Drop for Case { + fn drop(&mut self) { fs::remove_dir_all(&self.dir).unwrap(); } + } + + #[test] + fn selected_truncated_clash_records_fail() { + for name in ["raw.vcf.gz", "split.vcf.gz", "chrA.tsv.gz"] { + let c = Case::new("clashes", name); + c.put(name, if name == "chrA.tsv.gz" { b"chrA" } else { b"chrA\t3\n" }); + c.fails_without_publication(); + } + } + + #[test] + fn missing_panel_header_and_blank_bed_fail() { + for name in ["sites.tsv", "truth.bed"] { + let c = Case::new("hg002-gvcf", name); + let bytes = if name == "sites.tsv" { vec![] } else { [c.text(name), b"\n".to_vec()].concat() }; + c.put(name, &bytes); + c.fails_without_publication(); + } + } + + #[test] + fn every_evidence_text_input_rejects_invalid_utf8() { + for (kind, names) in [ + ("clashes", vec!["raw.vcf.gz", "split.vcf.gz", "chrA.tsv.gz"]), + ("hg002-gvcf", vec!["truth.vcf.gz", "truth.bed", "ref.fasta.fai", "sites.tsv"]), + ] { + for name in names { + let c = Case::new(kind, name); + // Invalid bytes in a header or filtered-out record must still fail. + let mut bytes = c.text(name); + bytes.extend_from_slice(b"#ignored\xff\n"); + c.put(name, &bytes); + c.fails_without_publication(); + } + } + } + + #[test] + fn every_evidence_text_input_has_universal_newlines() { + for (kind, names) in [ + ("clashes", vec!["raw.vcf.gz", "split.vcf.gz", "chrA.tsv.gz", "chrB.tsv.gz"]), + ("hg002-gvcf", vec!["truth.vcf.gz", "truth.bed", "ref.fasta.fai", "sites.tsv"]), + ] { + for name in names { + for ending in [b"\r".as_slice(), b"\r\n".as_slice()] { + let c = Case::new(kind, &format!("{name}-{}", ending.len())); + c.run().unwrap(); + let expected = fs::read(c.dir.join("out")).unwrap(); + let bytes = c.text(name).split(|b| *b == b'\n').collect::>().join(ending); + c.put(name, &bytes); + c.run().unwrap(); + assert_eq!(fs::read(c.dir.join("out")).unwrap(), expected, "{kind}/{name}/{ending:?}"); + } + } + } + } + + #[test] + fn failed_gvcf_inputs_preserve_existing_output() { + for name in ["truth.vcf.gz", "sites.tsv"] { + for fault in ["missing", "directory", "malformed", "truncated"] { + let c = Case::new("hg002-gvcf", &format!("{name}-{fault}")); + let path = c.dir.join(name); + match fault { + "missing" => fs::remove_file(path).unwrap(), + "directory" => { fs::remove_file(&path).unwrap(); fs::create_dir(path).unwrap(); } + "malformed" => { let mut bytes = c.text(name); bytes.extend_from_slice(b"bad\n"); c.put(name, &bytes); } + _ if name.ends_with(".gz") => { let mut bytes = fs::read(&path).unwrap(); bytes.truncate(bytes.len()-5); fs::write(path, bytes).unwrap(); } + _ => c.put(name, b"header\nbad\n"), + } + c.fails_without_publication(); + } + } + } + #[test] fn spans_merge_and_answer_inside() { let m = merge(vec![(10, 12), (5, 6), (11, 15), (15, 16)]); diff --git a/rust/crates/og-dev/src/renovate.rs b/rust/crates/og-dev/src/renovate.rs index f3355c766..e282db2a3 100644 --- a/rust/crates/og-dev/src/renovate.rs +++ b/rust/crates/og-dev/src/renovate.rs @@ -62,6 +62,11 @@ struct Pins { /// Runs `og dev renovate-check ARGS`; returns the exit code. pub fn cli(args: &[String], out: &mut dyn Write, err: &mut dyn Write) -> u8 { + let summary = std::env::var_os("GITHUB_STEP_SUMMARY").filter(|p| !p.is_empty()); + cli_with_summary(args, out, err, summary.as_deref().map(Path::new)) +} + +fn cli_with_summary(args: &[String], out: &mut dyn Write, err: &mut dyn Write, summary_path: Option<&Path>) -> u8 { let (mut log, mut pins, mut repo) = (None, PINS.to_owned(), ".".to_owned()); let mut it = args.iter(); while let Some(a) = it.next() { @@ -125,7 +130,7 @@ pub fn cli(args: &[String], out: &mut dyn Write, err: &mut dyn Write) -> u8 { }; let table = summary(&deps); let _ = writeln!(out, "{table}"); - if let Some(path) = std::env::var_os("GITHUB_STEP_SUMMARY").filter(|p| !p.is_empty()) { + if let Some(path) = summary_path { let appended = fs::OpenOptions::new() .append(true) .create(true) @@ -832,6 +837,45 @@ mod tests { let _ = fs::remove_dir_all(&d); } + + + #[test] + fn configured_summary_write_failure_fails_the_check() { + let d = planted("summary"); + let args = vec![d.join("renovate.log").to_str().unwrap().to_owned(), "--repo".to_owned(), d.to_str().unwrap().to_owned()]; + let (mut out, mut err) = (Vec::new(), Vec::new()); + assert_eq!(cli_with_summary(&args, &mut out, &mut err, Some(&d)), 1); + assert!(!String::from_utf8_lossy(&out).contains("every check passed")); + fs::remove_dir_all(d).unwrap(); + } + + #[test] + fn malformed_log_shapes_and_text_fail() { + for (label, bad) in [ + ("null-deps", r#"{"packageFile":"unused","deps":null}"#), + ("null-updates", r#"{"packageFile":"unused","deps":[{"updates":null}]}"#), + ("scalar-update", r#"{"packageFile":"unused","deps":[{"updates":[42]}]}"#), + ("scalar-dep", r#"{"packageFile":"unused","deps":[42]}"#), + ("scalar-file", r#"{"packageFile":[],"deps":[]}"#), + ] { + let d = planted(label); + let log = fs::read_to_string(d.join("renovate.log")).unwrap(); + let log = log.replacen(r#""regex":["#, &format!(r#""regex":[{bad},"#), 1); + fs::write(d.join("renovate.log"), log).unwrap(); + let (code, out) = run(&d); + assert_eq!(code, 1, "{label}: {out}"); + fs::remove_dir_all(d).unwrap(); + } + for (label, prefix) in [("utf8", b"\xff\n".as_slice()), ("array", b"[]\n".as_slice()), ("null", b"null\n".as_slice())] { + let d = planted(label); + let log = [prefix, &fs::read(d.join("renovate.log")).unwrap()].concat(); + fs::write(d.join("renovate.log"), log).unwrap(); + let (code, out) = run(&d); + assert_eq!(code, 1, "{label}: {out}"); + fs::remove_dir_all(d).unwrap(); + } + } + #[test] fn a_log_without_the_record_fails() { let d = planted("nolog"); From a6d0b25db3cf8805dfbe597568dde7425d150fc5 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 18:00:42 +0100 Subject: [PATCH 34/45] fix(dev): reject malformed evidence and publish validated outputs atomically --- CHANGELOG.md | 3 + rust/crates/og-dev/src/evidence.rs | 257 ++++++++++++++++++++++++----- rust/crates/og-dev/src/fixture.rs | 42 +++-- rust/crates/og-dev/src/renovate.rs | 139 +++++++++++----- tests/smoke/vntyper.sh | 6 + tests/smoke/vntyper_testdata.sh | 2 +- 6 files changed, 353 insertions(+), 96 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c35c774a7..9e22902bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ Each release's notes. From 1.0.0 on, a major or minor release has hand-written n ## Unreleased +- `og dev evidence` now rejects invalid UTF-8, truncated selected records, blank BED rows and missing panel headers. It reads LF, CRLF and CR line endings alike, with a 16 MiB limit per record. Evidence and fixture files are published by rename after validation, so a failed run preserves existing outputs. `og dev renovate-check` rejects malformed log records and present fields of the wrong type, and fails if its configured step summary cannot be written. The PharmCAT trimmer consumes source genes as it trims them. The VNtyper downloader also accepts output paths that contain a newline. + + - The Python that CI and the fixtures ran outside the pipeline now runs in the Rust `og` binary. The public evidence workflow runs `og dev evidence clashes` and `og dev evidence hg002-gvcf` instead of two inline Python programs; the Renovate dry run checks what Renovate found with `og dev renovate-check`, whose list of non-image pins is now data in `scripts/ci/renovate-pins.tsv`; `og dev fixture comphet` writes the compound-het fixture of the e2e case `comphet-phasing-1-reads`, byte for byte; and `og dev fixture trim-pharmcat` trims a PharmCAT report for `tests/fixtures/pharmcat`. `tests/fixtures/comphet/make_fixture.py`, `tests/fixtures/pharmcat/trim_report.py` and `tests/smoke/vntyper_testdata.py` are gone: VNtyper's two test BAMs now come from `tests/smoke/vntyper_testdata.sh`, one HTTP range request per BAM with curl, checked by gzip against the archive's CRC-32 and size and then against the pinned sha256. On GIAB's HG002 benchmark VCF (4,048,342 records) with the chr20-22 reference, `og dev evidence hg002-gvcf` writes the same gVCF in 3.5 s and 97 MB instead of 11.8 s and 3.8 GB, because it keeps the spans the records cover instead of every covered position. No pipeline output changes. What changes on purpose: - `og dev evidence hg002-gvcf` skips a panel site past the end of its contig. The Python read the byte after the contig, and at the end of the file wrote a reference block with an empty REF. - `og dev renovate-check` stops with exit 2 when a pin's file is missing or its glob matches no file. The Python counted such a pin in no file, so it could never fail. diff --git a/rust/crates/og-dev/src/evidence.rs b/rust/crates/og-dev/src/evidence.rs index 0135488fd..3e95b2880 100644 --- a/rust/crates/og-dev/src/evidence.rs +++ b/rust/crates/og-dev/src/evidence.rs @@ -25,6 +25,8 @@ use std::fs::{self, File}; use std::io::{BufRead, BufWriter, Write}; use std::os::unix::fs::FileExt; use std::path::Path; +use std::path::PathBuf; +use std::sync::atomic::{AtomicU64, Ordering}; const USAGE: &str = "usage: og dev evidence clashes RAW.vcf.gz SPLIT.vcf.gz DATA_DIR OUT.tsv\n og dev evidence hg002-gvcf TRUTH.vcf.gz TRUTH.bed REF.fasta SITES.tsv OUT.vcf\n"; @@ -66,23 +68,107 @@ fn open(path: &str) -> Result, String> { og_io::open(path).map_err(|e| format!("{path}: {e}")) } -/// Each line of `path` without its line end, to `f`. -fn lines(path: &str, mut f: impl FnMut(&[u8]) -> Result<(), String>) -> Result<(), String> { +/// Strict UTF-8 and universal newlines, with a 16 MiB record limit. This +/// local reader can use og-io's bounded reader once that API is available. +pub(crate) fn lines( + path: &str, + mut f: impl FnMut(&[u8]) -> Result<(), String>, +) -> Result<(), String> { + const MAX_LINE: usize = 16 << 20; let mut r = open(path)?; - let mut buf = Vec::new(); + let mut line = Vec::new(); + let mut after_cr = false; loop { - buf.clear(); - let n = r - .read_until(b'\n', &mut buf) - .map_err(|e| format!("{path}: {e}"))?; - if n == 0 { + let chunk = r.fill_buf().map_err(|e| format!("{path}: {e}"))?; + if chunk.is_empty() { + if !line.is_empty() { + std::str::from_utf8(&line).map_err(|e| format!("{path}: invalid UTF-8: {e}"))?; + f(&line)?; + } return Ok(()); } - let line = buf.strip_suffix(b"\n").unwrap_or(&buf); - f(line.strip_suffix(b"\r").unwrap_or(line))?; + if after_cr { + after_cr = false; + if chunk[0] == b'\n' { + r.consume(1); + continue; + } + } + let end = chunk.iter().position(|b| matches!(b, b'\r' | b'\n')); + let n = end.unwrap_or(chunk.len()); + if line.len() + n > MAX_LINE { + return Err(format!("{path}: record exceeds {MAX_LINE} bytes")); + } + line.extend_from_slice(&chunk[..n]); + if end.is_some() { + after_cr = chunk[n] == b'\r'; + r.consume(n + 1); + std::str::from_utf8(&line).map_err(|e| format!("{path}: invalid UTF-8: {e}"))?; + f(&line)?; + line.clear(); + } else { + r.consume(n); + } + } +} + +/// A unique, exclusively created sibling. Dropping it on any read, write +/// or rename error removes the temporary file and preserves the destination. +pub(crate) struct PendingOutput { + path: PathBuf, + destination: PathBuf, +} + +impl PendingOutput { + pub(crate) fn new(destination: &Path) -> Result<(Self, File), String> { + static NEXT: AtomicU64 = AtomicU64::new(0); + let name = destination.file_name().ok_or("output has no file name")?; + loop { + let mut tmp = std::ffi::OsString::from("."); + tmp.push(name); + tmp.push(format!( + ".tmp-{}-{}", + std::process::id(), + NEXT.fetch_add(1, Ordering::Relaxed) + )); + let path = destination.with_file_name(tmp); + match File::options().write(true).create_new(true).open(&path) { + Ok(file) => { + return Ok(( + Self { + path, + destination: destination.to_owned(), + }, + file, + )); + } + Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue, + Err(e) => return Err(format!("{}: {e}", destination.display())), + } + } + } + + pub(crate) fn commit(self) -> Result<(), String> { + fs::rename(&self.path, &self.destination) + .map_err(|e| format!("{}: {e}", self.destination.display())) } } +impl Drop for PendingOutput { + fn drop(&mut self) { + let _ = fs::remove_file(&self.path); + } +} + +pub(crate) fn publish(path: &Path, bytes: &[u8]) -> Result<(), String> { + let (pending, mut file) = PendingOutput::new(path)?; + file.write_all(bytes) + .and_then(|()| file.flush()) + .map_err(|e| format!("{}: {e}", path.display()))?; + drop(file); + pending.commit() +} + fn pos(field: &[u8], path: &str) -> Result { std::str::from_utf8(field) .ok() @@ -108,9 +194,14 @@ fn clash_positions( return Ok(()); } let c: Vec<&[u8]> = line.splitn(6, |&b| b == b'\t').collect(); - if c.len() < 5 || !chroms.contains(c[0]) { + if !chroms.contains(c[0]) { return Ok(()); } + if c.len() < 5 { + return Err(format!( + "{path}: a selected record has fewer than 5 columns" + )); + } let k = (c[0].to_vec(), pos(c[1], path)?); if key.as_ref() != Some(&k) { key = Some(k.clone()); @@ -164,9 +255,12 @@ fn clashes(raw: &str, split: &str, data: &str, out: &str) -> Result<(), String> let path = table.to_string_lossy().into_owned(); lines(&path, |line| { let c: Vec<&[u8]> = line.splitn(3, |&b| b == b'\t').collect(); - if c[0] != chrom.as_slice() || c.len() < 2 { + if c[0] != chrom.as_slice() { return Ok(()); } + if c.len() < 2 { + return Err(format!("{path}: a selected row has fewer than 2 columns")); + } let p = pos(c[1], &path)?; for (k, w) in want.iter().enumerate() { on_table[k] += u64::from(w.contains(&p)); @@ -182,7 +276,7 @@ fn clashes(raw: &str, split: &str, data: &str, out: &str) -> Result<(), String> on_table[k] ); } - fs::write(out, text).map_err(|e| format!("{out}: {e}")) + publish(Path::new(out), text.as_bytes()) } /// One `.fai` row: length, byte offset, bases per line, bytes per line. @@ -254,9 +348,6 @@ fn hg002_gvcf( .split(|b| b.is_ascii_whitespace()) .filter(|x| !x.is_empty()) .collect(); - if f.is_empty() { - return Ok(()); - } if f.len() < 3 { return Err(format!("{bed}: a row has fewer than 3 columns")); } @@ -275,7 +366,7 @@ fn hg002_gvcf( }) }; - let file = File::create(out).map_err(|e| format!("{out}: {e}"))?; + let (pending, file) = PendingOutput::new(Path::new(out))?; let mut w = BufWriter::with_capacity(1 << 20, file); let werr = |e: std::io::Error| format!("{out}: {e}"); let mut covered: HashMap, Vec<(u64, u64)>> = HashMap::new(); @@ -291,10 +382,13 @@ fn hg002_gvcf( return Err(format!("{truth}: a record has fewer than 5 columns")); } let p = pos(f[1], truth)?; - covered - .entry(f[0].to_vec()) - .or_default() - .push((p, p + f[3].len() as u64)); + covered.entry(f[0].to_vec()).or_default().push(( + p, + p + std::str::from_utf8(f[3]) + .expect("strict reader") + .chars() + .count() as u64, + )); records += 1; } w.write_all(line) @@ -326,6 +420,7 @@ fn hg002_gvcf( fasta .read_exact_at(&mut base, at) .map_err(|e| format!("{reference}: {e}"))?; + std::str::from_utf8(&base).map_err(|e| format!("{reference}: invalid UTF-8: {e}"))?; let b = base[0].to_ascii_uppercase(); if !b"ACGT".contains(&b) { return Ok(()); @@ -335,7 +430,12 @@ fn hg002_gvcf( .and_then(|()| writeln!(w, "\t{p}\t.\t{}\t<*>\t.\t.\tEND={p}\tGT\t0/0", b as char)) .map_err(werr) })?; - w.flush().map_err(werr)?; + if first { + return Err(format!("{sites}: missing header")); + } + let file = w.into_inner().map_err(|e| werr(e.into_error()))?; + drop(file); + pending.commit()?; Ok((records, blocks)) } @@ -390,7 +490,6 @@ mod tests { ); } - struct Case { dir: std::path::PathBuf, kind: &'static str, @@ -399,12 +498,15 @@ mod tests { impl Case { fn new(kind: &'static str, label: &str) -> Self { let dir = std::env::temp_dir().join(format!( - "og-evidence-{kind}-{label}-{}-{:?}", std::process::id(), std::thread::current().id() + "og-evidence-{kind}-{label}-{}-{:?}", + std::process::id(), + std::thread::current().id() )); let _ = fs::remove_dir_all(&dir); fs::create_dir_all(&dir).unwrap(); let src = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../../tests/parity/inputs").join(format!("dev-evidence-{kind}")); + .join("../../../tests/parity/inputs") + .join(format!("dev-evidence-{kind}")); for e in fs::read_dir(src).unwrap() { let e = e.unwrap(); fs::copy(e.path(), dir.join(e.file_name())).unwrap(); @@ -414,12 +516,16 @@ mod tests { fn text(&self, name: &str) -> Vec { use std::io::Read; let mut bytes = Vec::new(); - open(self.dir.join(name).to_str().unwrap()).unwrap().read_to_end(&mut bytes).unwrap(); + open(self.dir.join(name).to_str().unwrap()) + .unwrap() + .read_to_end(&mut bytes) + .unwrap(); bytes } fn put(&self, name: &str, bytes: &[u8]) { if name.ends_with(".gz") { - let mut gz = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default()); + let mut gz = + flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default()); gz.write_all(bytes).unwrap(); fs::write(self.dir.join(name), gz.finish().unwrap()).unwrap(); } else { @@ -429,31 +535,60 @@ mod tests { fn run(&self) -> Result<(), String> { let p = |n: &str| self.dir.join(n).to_str().unwrap().to_owned(); if self.kind == "clashes" { - clashes(&p("raw.vcf.gz"), &p("split.vcf.gz"), self.dir.to_str().unwrap(), &p("out")) + clashes( + &p("raw.vcf.gz"), + &p("split.vcf.gz"), + self.dir.to_str().unwrap(), + &p("out"), + ) } else { - hg002_gvcf(&p("truth.vcf.gz"), &p("truth.bed"), &p("ref.fasta"), &p("sites.tsv"), &p("out")).map(|_| ()) + hg002_gvcf( + &p("truth.vcf.gz"), + &p("truth.bed"), + &p("ref.fasta"), + &p("sites.tsv"), + &p("out"), + ) + .map(|_| ()) } } fn fails_without_publication(&self) { fs::write(self.dir.join("out"), b"PREVIOUS OUTPUT\n").unwrap(); assert!(self.run().is_err(), "{} must fail", self.dir.display()); - assert_eq!(fs::read(self.dir.join("out")).unwrap(), b"PREVIOUS OUTPUT\n"); - let leftovers: Vec<_> = fs::read_dir(&self.dir).unwrap().filter_map(|e| { - let name = e.unwrap().file_name(); - name.to_str().filter(|n| n.starts_with(".out.")).map(str::to_owned) - }).collect(); + assert_eq!( + fs::read(self.dir.join("out")).unwrap(), + b"PREVIOUS OUTPUT\n" + ); + let leftovers: Vec<_> = fs::read_dir(&self.dir) + .unwrap() + .filter_map(|e| { + let name = e.unwrap().file_name(); + name.to_str() + .filter(|n| n.starts_with(".out.")) + .map(str::to_owned) + }) + .collect(); assert!(leftovers.is_empty(), "{leftovers:?}"); } } impl Drop for Case { - fn drop(&mut self) { fs::remove_dir_all(&self.dir).unwrap(); } + fn drop(&mut self) { + fs::remove_dir_all(&self.dir).unwrap(); + } } #[test] fn selected_truncated_clash_records_fail() { for name in ["raw.vcf.gz", "split.vcf.gz", "chrA.tsv.gz"] { let c = Case::new("clashes", name); - c.put(name, if name == "chrA.tsv.gz" { b"chrA" } else { b"chrA\t3\n" }); + c.put( + name, + if name == "chrA.tsv.gz" { + b"chrA" + } else { + b"chrA\t3\n" + }, + ); c.fails_without_publication(); } } @@ -462,7 +597,11 @@ mod tests { fn missing_panel_header_and_blank_bed_fail() { for name in ["sites.tsv", "truth.bed"] { let c = Case::new("hg002-gvcf", name); - let bytes = if name == "sites.tsv" { vec![] } else { [c.text(name), b"\n".to_vec()].concat() }; + let bytes = if name == "sites.tsv" { + vec![] + } else { + [c.text(name), b"\n".to_vec()].concat() + }; c.put(name, &bytes); c.fails_without_publication(); } @@ -472,7 +611,10 @@ mod tests { fn every_evidence_text_input_rejects_invalid_utf8() { for (kind, names) in [ ("clashes", vec!["raw.vcf.gz", "split.vcf.gz", "chrA.tsv.gz"]), - ("hg002-gvcf", vec!["truth.vcf.gz", "truth.bed", "ref.fasta.fai", "sites.tsv"]), + ( + "hg002-gvcf", + vec!["truth.vcf.gz", "truth.bed", "ref.fasta.fai", "sites.tsv"], + ), ] { for name in names { let c = Case::new(kind, name); @@ -488,18 +630,32 @@ mod tests { #[test] fn every_evidence_text_input_has_universal_newlines() { for (kind, names) in [ - ("clashes", vec!["raw.vcf.gz", "split.vcf.gz", "chrA.tsv.gz", "chrB.tsv.gz"]), - ("hg002-gvcf", vec!["truth.vcf.gz", "truth.bed", "ref.fasta.fai", "sites.tsv"]), + ( + "clashes", + vec!["raw.vcf.gz", "split.vcf.gz", "chrA.tsv.gz", "chrB.tsv.gz"], + ), + ( + "hg002-gvcf", + vec!["truth.vcf.gz", "truth.bed", "ref.fasta.fai", "sites.tsv"], + ), ] { for name in names { for ending in [b"\r".as_slice(), b"\r\n".as_slice()] { let c = Case::new(kind, &format!("{name}-{}", ending.len())); c.run().unwrap(); let expected = fs::read(c.dir.join("out")).unwrap(); - let bytes = c.text(name).split(|b| *b == b'\n').collect::>().join(ending); + let bytes = c + .text(name) + .split(|b| *b == b'\n') + .collect::>() + .join(ending); c.put(name, &bytes); c.run().unwrap(); - assert_eq!(fs::read(c.dir.join("out")).unwrap(), expected, "{kind}/{name}/{ending:?}"); + assert_eq!( + fs::read(c.dir.join("out")).unwrap(), + expected, + "{kind}/{name}/{ending:?}" + ); } } } @@ -513,9 +669,20 @@ mod tests { let path = c.dir.join(name); match fault { "missing" => fs::remove_file(path).unwrap(), - "directory" => { fs::remove_file(&path).unwrap(); fs::create_dir(path).unwrap(); } - "malformed" => { let mut bytes = c.text(name); bytes.extend_from_slice(b"bad\n"); c.put(name, &bytes); } - _ if name.ends_with(".gz") => { let mut bytes = fs::read(&path).unwrap(); bytes.truncate(bytes.len()-5); fs::write(path, bytes).unwrap(); } + "directory" => { + fs::remove_file(&path).unwrap(); + fs::create_dir(path).unwrap(); + } + "malformed" => { + let mut bytes = c.text(name); + bytes.extend_from_slice(b"bad\n"); + c.put(name, &bytes); + } + _ if name.ends_with(".gz") => { + let mut bytes = fs::read(&path).unwrap(); + bytes.truncate(bytes.len() - 5); + fs::write(path, bytes).unwrap(); + } _ => c.put(name, b"header\nbad\n"), } c.fails_without_publication(); diff --git a/rust/crates/og-dev/src/fixture.rs b/rust/crates/og-dev/src/fixture.rs index bb163c9f9..3220fc707 100644 --- a/rust/crates/og-dev/src/fixture.rs +++ b/rust/crates/og-dev/src/fixture.rs @@ -379,7 +379,7 @@ pub mod comphet { ("comphet.vcf", comphet_vcf(&vs)), ] { let path = dir.join(name); - fs::write(&path, text).map_err(|e| format!("{}: {e}", path.display()))?; + crate::evidence::publish(&path, text.as_bytes())?; } Ok(()) } @@ -395,6 +395,7 @@ fn truthy(v: &Value) -> bool { pycompat::value::truthy(v) } +/// The old trimmer used `v or []`: null and every false value are empty. /// `v or []` as a list; a truthy value that is not a list is an error. fn list<'a>(v: Option<&'a Value>, what: &str) -> Result<&'a [Value], String> { match v { @@ -421,6 +422,7 @@ fn pick(x: &IndexMap, keys: &[&str]) -> IndexMap { /// One listed diplotype with the fields `og pgx` reads. fn diplotype(x: &Value) -> Result { let Value::Object(x) = x else { + // The Python dip() explicitly passes non-object listed diplotypes through. return Ok(x.clone()); }; let mut out = pick( @@ -457,11 +459,17 @@ fn head(items: &[Value], n: i64) -> &[Value] { /// The trimmed report: every gene with its listed diplotypes and related /// drugs, and for each drug of each guidance source up to `max` of the /// annotations PharmCAT matched, with only the fields `og pgx` reads. -pub fn trim(d: &Value, max: i64) -> Result { - let d = object(d, "the report")?; +pub fn trim(d: Value, max: i64) -> Result { + let Value::Object(mut d) = d else { + return Err("the report is not an object".to_owned()); + }; let mut genes = IndexMap::new(); - for (g, v) in object(d.get("genes").unwrap_or(&Value::Null), "genes")? { - let v = object(v, &format!("gene {g}"))?; + let Some(Value::Object(input_genes)) = d.swap_remove("genes") else { + return Err("genes is not an object".to_owned()); + }; + // Consume one source gene at a time: do not retain a second full report. + for (g, v) in input_genes { + let v = object(&v, &format!("gene {g}"))?; let mut gene = pick(v, &["geneSymbol", "chr", "phased", "callSource"]); let mut related = Vec::new(); for r in list(v.get("relatedDrugs"), "relatedDrugs")? { @@ -487,11 +495,14 @@ pub fn trim(d: &Value, max: i64) -> Result { .collect::>()?; gene.insert(k.to_owned(), Value::Array(dips)); } - genes.insert(g.clone(), Value::Object(gene)); + genes.insert(g, Value::Object(gene)); } let mut drugs: IndexMap = IndexMap::new(); - for (source, by) in object(d.get("drugs").unwrap_or(&Value::Null), "drugs")? { - for (name, rep) in object(by, &format!("drugs of {source}"))? { + let Some(Value::Object(input_drugs)) = d.swap_remove("drugs") else { + return Err("drugs is not an object".to_owned()); + }; + for (source, by) in input_drugs { + for (name, rep) in object(&by, &format!("drugs of {source}"))? { let rep = object(rep, &format!("drug {name}"))?; let mut gls = Vec::new(); for gl in list(rep.get("guidelines"), "guidelines")? { @@ -509,6 +520,7 @@ pub fn trim(d: &Value, max: i64) -> Result { ("gene".to_owned(), get(x, "gene")), ("label".to_owned(), get(x, "label")), ]))), + // The Python comprehension has `if isinstance(x, dict)`. _ => None, }) .collect(); @@ -552,7 +564,10 @@ pub fn trim(d: &Value, max: i64) -> Result { } } } - let mut out = pick(d, &["title", "timestamp", "pharmcatVersion", "dataVersion"]); + let mut out = pick( + &d, + &["title", "timestamp", "pharmcatVersion", "dataVersion"], + ); out.insert("genes".to_owned(), Value::Object(genes)); out.insert("drugs".to_owned(), Value::Object(drugs)); let mut note = String::new(); @@ -568,13 +583,14 @@ fn trim_pharmcat(src: &Path, dst: &Path, max: i64) -> Result<(), String> { let text = fs::read(src).map_err(|e| py_os_error(&e, &src.to_string_lossy()))?; let text = String::from_utf8(text).map_err(|_| format!("{}: not UTF-8", src.display()))?; let d = json::loads(&text).map_err(|e| format!("{}: {e}", src.display()))?; - let out = trim(&d, max)?; + drop(text); + let out = trim(d, max)?; let opts = Dump { separators: Some((",", ":")), ..Dump::default() }; let text = json::dumps(&out, &opts).map_err(|e| e.to_string())?; - fs::write(dst, text).map_err(|e| py_os_error(&e, &dst.to_string_lossy())) + crate::evidence::publish(dst, text.as_bytes()) } #[cfg(test)] @@ -629,7 +645,7 @@ mod tests { "drugs":{"CPIC":{"d":{"name":"d","guidelines":[{"name":"g","annotations":[{"genotypes":[{"diplotypes":[{"gene":"G"},5]}]},{},{}]},{"annotations":[]}]}}}}"#, ) .expect("json"); - let out = trim(&d, 2).expect("trim"); + let out = trim(d.clone(), 2).expect("trim"); let text = json::dumps( &out, &Dump { @@ -642,7 +658,7 @@ mod tests { assert!(text.contains(r#""annotations":[{"phenotypes":null,"classification":null,"drugRecommendation":null,"genotypes":[{"diplotypes":[{"gene":"G","label":null}]}]},{"phenotypes":null"#), "{text}"); assert_eq!(text.matches("drugRecommendation").count(), 2, "{text}"); // A negative count is Python's slice: all but the last. - let out = trim(&d, -2).expect("trim"); + let out = trim(d, -2).expect("trim"); let text = json::dumps(&out, &Dump::default()).expect("dumps"); assert_eq!(text.matches("drugRecommendation").count(), 1, "{text}"); } diff --git a/rust/crates/og-dev/src/renovate.rs b/rust/crates/og-dev/src/renovate.rs index e282db2a3..2513655c7 100644 --- a/rust/crates/og-dev/src/renovate.rs +++ b/rust/crates/og-dev/src/renovate.rs @@ -38,7 +38,7 @@ use pycompat::value::{py_repr, py_str, truthy}; use regex::Regex; use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::fs; -use std::io::{BufRead, BufReader, Write}; +use std::io::Write; use std::path::Path; const USAGE: &str = "usage: og dev renovate-check LOG [--pins FILE] [--repo DIR]\n"; @@ -66,7 +66,12 @@ pub fn cli(args: &[String], out: &mut dyn Write, err: &mut dyn Write) -> u8 { cli_with_summary(args, out, err, summary.as_deref().map(Path::new)) } -fn cli_with_summary(args: &[String], out: &mut dyn Write, err: &mut dyn Write, summary_path: Option<&Path>) -> u8 { +fn cli_with_summary( + args: &[String], + out: &mut dyn Write, + err: &mut dyn Write, + summary_path: Option<&Path>, +) -> u8 { let (mut log, mut pins, mut repo) = (None, PINS.to_owned(), ".".to_owned()); let mut it = args.iter(); while let Some(a) = it.next() { @@ -131,13 +136,23 @@ fn cli_with_summary(args: &[String], out: &mut dyn Write, err: &mut dyn Write, s let table = summary(&deps); let _ = writeln!(out, "{table}"); if let Some(path) = summary_path { - let appended = fs::OpenOptions::new() - .append(true) - .create(true) - .open(&path) - .and_then(|mut f| write!(f, "## Renovate dry run\n\n{table}\n")); + let appended = (|| -> Result<(), String> { + let (pending, mut file) = crate::evidence::PendingOutput::new(path)?; + match fs::File::open(path) { + Ok(mut old) => { + std::io::copy(&mut old, &mut file).map_err(|e| e.to_string())?; + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} + Err(e) => return Err(e.to_string()), + } + writeln!(file, "## Renovate dry run\n\n{table}").map_err(|e| e.to_string())?; + file.flush().map_err(|e| e.to_string())?; + drop(file); + pending.commit() + })(); if let Err(e) = appended { let _ = writeln!(err, "og dev renovate-check: GITHUB_STEP_SUMMARY: {e}"); + return 1; } } let mut fails = check(&deps, &env_text, &pins); @@ -246,25 +261,23 @@ fn read_pins(repo: &Path, path: &Path) -> Result { /// Every dependency of the log's last `packageFiles with updates` record. fn load(log: &str) -> Result, String> { - let f = fs::File::open(log).map_err(|e| format!("FAIL: cannot read {log}: {e}"))?; let mut files = None; - let mut r = BufReader::new(f); - let mut buf = Vec::new(); - loop { - buf.clear(); - if r.read_until(b'\n', &mut buf) - .map_err(|e| format!("FAIL: cannot read {log}: {e}"))? - == 0 - { - break; - } - let Ok(Value::Object(mut rec)) = json::loads(&String::from_utf8_lossy(&buf)) else { - continue; + crate::evidence::lines(log, |line| { + // The old load() skipped only JSON parse errors, not parsed scalars. + let Ok(value) = json::loads(std::str::from_utf8(line).expect("strict reader")) else { + return Ok(()); + }; + let Value::Object(mut rec) = value else { + return Err(format!("FAIL: {log}: a log record is not an object")); }; if rec.get("msg") == Some(&Value::Str("packageFiles with updates".to_owned())) { - files = Some(rec.swap_remove("config").unwrap_or(Value::Null)); + files = Some( + rec.swap_remove("config") + .ok_or_else(|| format!("FAIL: {log}: missing config"))?, + ); } - } + Ok(()) + })?; let Some(files) = files else { return Err(format!( "FAIL: no 'packageFiles with updates' record in {log}" @@ -287,18 +300,58 @@ fn load(log: &str) -> Result, String> { let Value::Object(mut pf) = pf else { return Err(shape()); }; - let name = pf - .get("packageFile") - .map(py_str) - .ok_or_else(shape)? - .map_err(|_| shape())?; - if let Some(Value::Array(ds)) = pf.swap_remove("deps") { - for d in ds { - let Value::Object(d) = d else { + let Some(Value::Str(name)) = pf.swap_remove("packageFile") else { + return Err(shape()); + }; + let ds = match pf.swap_remove("deps") { + None => Vec::new(), + Some(Value::Array(ds)) => ds, + _ => return Err(shape()), + }; + for d in ds { + let Value::Object(d) = d else { + return Err(shape()); + }; + // Optional means absent. Null or a scalar is not an empty list. + if let Some(u) = d.get("updates") { + let Value::Array(us) = u else { return Err(shape()); }; - deps.push((name.clone(), d)); + for u in us { + let Value::Object(u) = u else { + return Err(shape()); + }; + for k in ["newValue", "newDigest", "updateType"] { + if u.get(k) + .is_some_and(|v| !matches!(v, Value::Null | Value::Str(_))) + { + return Err(shape()); + } + } + } } + for k in [ + "datasource", + "depName", + "currentValue", + "currentDigest", + "skipReason", + ] { + if d.get(k) + .is_some_and(|v| !matches!(v, Value::Null | Value::Str(_))) + { + return Err(shape()); + } + } + if let Some(ws) = d.get("warnings") { + let Value::Array(ws) = ws else { + return Err(shape()); + }; + if ws.iter().any(|v| !matches!(v, Value::Object(_))) { + return Err(shape()); + } + } + deps.push((name.clone(), d)); } } } @@ -837,12 +890,14 @@ mod tests { let _ = fs::remove_dir_all(&d); } - - #[test] fn configured_summary_write_failure_fails_the_check() { let d = planted("summary"); - let args = vec![d.join("renovate.log").to_str().unwrap().to_owned(), "--repo".to_owned(), d.to_str().unwrap().to_owned()]; + let args = vec![ + d.join("renovate.log").to_str().unwrap().to_owned(), + "--repo".to_owned(), + d.to_str().unwrap().to_owned(), + ]; let (mut out, mut err) = (Vec::new(), Vec::new()); assert_eq!(cli_with_summary(&args, &mut out, &mut err, Some(&d)), 1); assert!(!String::from_utf8_lossy(&out).contains("every check passed")); @@ -853,8 +908,14 @@ mod tests { fn malformed_log_shapes_and_text_fail() { for (label, bad) in [ ("null-deps", r#"{"packageFile":"unused","deps":null}"#), - ("null-updates", r#"{"packageFile":"unused","deps":[{"updates":null}]}"#), - ("scalar-update", r#"{"packageFile":"unused","deps":[{"updates":[42]}]}"#), + ( + "null-updates", + r#"{"packageFile":"unused","deps":[{"updates":null}]}"#, + ), + ( + "scalar-update", + r#"{"packageFile":"unused","deps":[{"updates":[42]}]}"#, + ), ("scalar-dep", r#"{"packageFile":"unused","deps":[42]}"#), ("scalar-file", r#"{"packageFile":[],"deps":[]}"#), ] { @@ -866,7 +927,11 @@ mod tests { assert_eq!(code, 1, "{label}: {out}"); fs::remove_dir_all(d).unwrap(); } - for (label, prefix) in [("utf8", b"\xff\n".as_slice()), ("array", b"[]\n".as_slice()), ("null", b"null\n".as_slice())] { + for (label, prefix) in [ + ("utf8", b"\xff\n".as_slice()), + ("array", b"[]\n".as_slice()), + ("null", b"null\n".as_slice()), + ] { let d = planted(label); let log = [prefix, &fs::read(d.join("renovate.log")).unwrap()].concat(); fs::write(d.join("renovate.log"), log).unwrap(); diff --git a/tests/smoke/vntyper.sh b/tests/smoke/vntyper.sh index cd4d6cec1..e726c3182 100644 --- a/tests/smoke/vntyper.sh +++ b/tests/smoke/vntyper.sh @@ -7,6 +7,12 @@ # the folder its config.json paths are relative to. set -e sh /smoke/vntyper_testdata.sh /out/data positive.bam negative.bam +# Hashing stdin must also work when the destination contains a newline. +newline_out='/out/new +line' +sh /smoke/vntyper_testdata.sh "$newline_out" negative.bam +cmp /out/data/negative.bam "$newline_out/negative.bam" +rm -rf "$newline_out" for s in positive negative; do samtools index "/out/data/${s}.bam" (cd /opt/vntyper && vntyper pipeline --bam "/out/data/${s}.bam" -o "/out/${s}" --sample-name "$s" \ diff --git a/tests/smoke/vntyper_testdata.sh b/tests/smoke/vntyper_testdata.sh index 705f4ea53..e6d7da2a8 100755 --- a/tests/smoke/vntyper_testdata.sh +++ b/tests/smoke/vntyper_testdata.sh @@ -58,7 +58,7 @@ EOF # then the raw deflate stream; then CRC-32 and size, both little-endian. { printf '\037\213\010\000\000\000\000\000\000\377'; cat "${part}.raw"; le32 "$((0x$crc))"; le32 "$size"; } > "${part}.gz" gzip -dc "${part}.gz" > "$part" || { echo "${path}: size or CRC-32 differs from the archive's directory" >&2; exit 1; } - got=$(sha256sum "$part" | cut -d' ' -f1) + got=$(sha256sum < "$part" | cut -d' ' -f1) [ "$got" = "$want" ] || { echo "${path}: sha256 ${got}, pinned ${want}" >&2; exit 1; } rm -f "${part}.raw" "${part}.gz" mv "$part" "${out}/${name}" From dac11393c543a93f7c4ab79763a26e850b824682 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 18:30:39 +0100 Subject: [PATCH 35/45] test(dev): pin round two input and publication regressions --- rust/crates/og-dev/src/evidence.rs | 99 ++++++++++++++++++++++++++++++ rust/crates/og-dev/src/fixture.rs | 35 +++++++++++ rust/crates/og-dev/src/renovate.rs | 46 ++++++++++++++ 3 files changed, 180 insertions(+) diff --git a/rust/crates/og-dev/src/evidence.rs b/rust/crates/og-dev/src/evidence.rs index 3e95b2880..09df60bab 100644 --- a/rust/crates/og-dev/src/evidence.rs +++ b/rust/crates/og-dev/src/evidence.rs @@ -690,6 +690,105 @@ mod tests { } } + + #[test] + fn explicit_compression_is_required_at_every_input() { + for (kind, names) in [ + ("clashes", vec!["raw.vcf.gz", "split.vcf.gz", "chrA.tsv.gz"]), + ("hg002-gvcf", vec!["truth.vcf.gz", "truth.bed", "ref.fasta.fai", "sites.tsv"]), + ] { + for name in names { + let c = Case::new(kind, &format!("compression-{name}")); + let bytes = c.text(name); + let wrong = if name.ends_with(".gz") { bytes } else { + let mut gz = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default()); + gz.write_all(&bytes).unwrap(); gz.finish().unwrap() + }; + fs::write(c.dir.join(name), wrong).unwrap(); + c.fails_without_publication(); + assert!(c.run().unwrap_err().contains("compression"), "{name}"); + } + } + } + + #[test] + fn bed_rejects_python_whitespace_inside_fields_and_as_separators() { + for ch in ['\u{2028}', '\u{85}', '\u{b}', '\u{a0}', '\u{1c}', '\u{1d}', '\u{1e}', '\u{1f}'] { + for embedded in [true, false] { + let c = Case::new("hg002-gvcf", &format!("space-{}-{embedded}", ch as u32)); + let text = String::from_utf8(c.text("truth.bed")).unwrap(); + let text = if embedded { text.replacen("chrA", &format!("chr{ch}A"), 1) } + else { text.replace('\t', &ch.to_string()) }; + c.put("truth.bed", text.as_bytes()); + c.fails_without_publication(); + assert!(c.run().unwrap_err().contains("unsupported whitespace")); + } + } + } + + #[test] + fn publication_preserves_permissions_and_follows_symlinks() { + use std::os::unix::fs::{PermissionsExt, symlink}; + let c = Case::new("clashes", "publication"); + let target = c.dir.join("target"); + let link = c.dir.join("link"); + fs::write(&target, b"old").unwrap(); + fs::set_permissions(&target, fs::Permissions::from_mode(0o600)).unwrap(); + symlink("target", &link).unwrap(); + publish(&link, b"new").unwrap(); + assert!(fs::symlink_metadata(&link).unwrap().file_type().is_symlink()); + assert_eq!(fs::read(&target).unwrap(), b"new"); + assert_eq!(fs::metadata(&target).unwrap().permissions().mode() & 0o777, 0o600); + fs::set_permissions(&target, fs::Permissions::from_mode(0o444)).unwrap(); + assert!(publish(&link, b"bad").unwrap_err().contains("read-only")); + assert_eq!(fs::read(&target).unwrap(), b"new"); + let dir_link = c.dir.join("dir-link"); + symlink(&c.dir, &dir_link).unwrap(); + for path in [&c.dir, &dir_link] { + assert!(publish(path, b"bad").unwrap_err().contains("not a regular file")); + } + } + + #[test] + fn approved_strict_evidence_shapes_fail_with_stable_errors() { + for value in ["3_0", "٣", "-3", "18446744073709551616"] { + let c = Case::new("hg002-gvcf", value); + c.put("truth.vcf.gz", format!("chrA\t{value}\t.\tA\tC\n").as_bytes()); + c.fails_without_publication(); + assert!(c.run().unwrap_err().contains("not a number")); + } + let c = Case::new("hg002-gvcf", "short-vcf"); + c.put("truth.vcf.gz", b"chrA\t3\t.\tA\n"); + c.fails_without_publication(); + assert!(c.run().unwrap_err().contains("fewer than 5 columns")); + let c = Case::new("hg002-gvcf", "blank-fai"); + c.put("ref.fasta.fai", b"\n"); + c.fails_without_publication(); + assert!(c.run().unwrap_err().contains("fewer than 5 columns")); + let c = Case::new("clashes", "newline-alt"); + c.put("raw.vcf.gz", b"chrA\t3\t.\tA\tC\nchrA\t3\t.\tAA\tA\n"); + c.fails_without_publication(); + assert!(c.run().unwrap_err().contains("VCF columns")); + } + + #[test] + fn approved_record_limit_and_final_lf_are_pinned() { + let c = Case::new("clashes", "record-limit"); + c.put("raw.vcf.gz", &vec![b'#'; (16 << 20) + 1]); + c.fails_without_publication(); + assert!(c.run().unwrap_err().contains("record exceeds 16777216 bytes")); + let c = Case::new("hg002-gvcf", "final-lf"); + c.run().unwrap(); + let expected = fs::read(c.dir.join("out")).unwrap(); + let mut truth = c.text("truth.vcf.gz"); + assert_eq!(truth.pop(), Some(b'\n')); + c.put("truth.vcf.gz", &truth); + c.run().unwrap(); + let result = fs::read(c.dir.join("out")).unwrap(); + assert_eq!(result, expected); + assert_eq!(result.last(), Some(&b'\n')); + } + #[test] fn spans_merge_and_answer_inside() { let m = merge(vec![(10, 12), (5, 6), (11, 15), (15, 16)]); diff --git a/rust/crates/og-dev/src/fixture.rs b/rust/crates/og-dev/src/fixture.rs index 3220fc707..b141a910a 100644 --- a/rust/crates/og-dev/src/fixture.rs +++ b/rust/crates/og-dev/src/fixture.rs @@ -597,6 +597,41 @@ fn trim_pharmcat(src: &Path, dst: &Path, max: i64) -> Result<(), String> { mod tests { use super::*; + + #[test] + fn comphet_validates_later_destinations_before_publishing() { + let d = std::env::temp_dir().join(format!("og-comphet-preflight-{}", std::process::id())); + let _ = fs::remove_dir_all(&d); + fs::create_dir_all(d.join("comphet.vcf")).unwrap(); + for name in ["reads.sam", "vep.vcf"] { fs::write(d.join(name), b"PREVIOUS OUTPUT").unwrap(); } + assert!(comphet::write(&d, false).is_err()); + for name in ["reads.sam", "vep.vcf"] { assert_eq!(fs::read(d.join(name)).unwrap(), b"PREVIOUS OUTPUT"); } + assert_eq!(fs::read_dir(&d).unwrap().count(), 3); + fs::remove_dir_all(d).unwrap(); + } + + #[test] + fn approved_strict_pharmcat_containers_fail_at_the_cli() { + let d = std::env::temp_dir().join(format!("og-pharmcat-shapes-{}", std::process::id())); + fs::create_dir_all(&d).unwrap(); + for (input, class) in [ + (r#"{"genes":{"G":{"sourceDiplotypes":"x"}},"drugs":{}}"#, "sourceDiplotypes is not a list"), + (r#"{"genes":{"G":{"recommendationDiplotypes":"x"}},"drugs":{}}"#, "recommendationDiplotypes is not a list"), + (r#"{"genes":{"G":{"sourceDiplotypes":[{"allele1":"x"}]}},"drugs":{}}"#, "allele1 is not an object"), + (r#"{"genes":{"G":{"sourceDiplotypes":[{"allele2":["x"]}]}},"drugs":{}}"#, "allele2 is not an object"), + (r#"{"genes":{},"drugs":{"S":{"D":{"guidelines":[{"annotations":[{"genotypes":[{"diplotypes":{"x":1}}]}]}]}}}}"#, "diplotypes is not a list"), + ] { + fs::write(d.join("in"), input).unwrap(); + fs::write(d.join("out"), b"old").unwrap(); + let args = vec!["trim-pharmcat".into(), d.join("in").to_str().unwrap().into(), d.join("out").to_str().unwrap().into()]; + let (mut out, mut err) = (Vec::new(), Vec::new()); + assert_eq!(cli(&args, &mut out, &mut err), 1); + assert!(String::from_utf8_lossy(&err).contains(class), "{err:?}"); + assert_eq!(fs::read(d.join("out")).unwrap(), b"old"); + } + fs::remove_dir_all(d).unwrap(); + } + #[test] fn mt19937_matches_cpython() { // random.Random(31): [r.getrandbits(32) for _ in range(3)] and the diff --git a/rust/crates/og-dev/src/renovate.rs b/rust/crates/og-dev/src/renovate.rs index 2513655c7..87a368999 100644 --- a/rust/crates/og-dev/src/renovate.rs +++ b/rust/crates/og-dev/src/renovate.rs @@ -941,6 +941,52 @@ mod tests { } } + + #[test] + fn repository_sources_use_universal_newlines() { + for name in [ENV, PINS, ".github/workflows/ci.yml"] { + for ending in ["\r", "\r\n"] { + let d = planted(&format!("newlines-{}-{}", name.replace('/', "-"), ending.len())); + let baseline = run(&d); + let text = fs::read_to_string(d.join(name)).unwrap().replace('\n', ending); + fs::write(d.join(name), text).unwrap(); + assert_eq!(run(&d), baseline, "{name}/{ending:?}"); + fs::remove_dir_all(d).unwrap(); + } + } + } + + #[test] + fn real_log_falsy_warnings_and_absent_empty_packages_are_accepted() { + for entry in [r#"{}"#, r#"{"deps":[]}"#, r#"{"deps":[],"packageFile":[]}"#, + r#"{"packageFile":"unused","deps":[{"warnings":null}]}"#, + r#"{"packageFile":"unused","deps":[{"warnings":false}]}"#, + r#"{"packageFile":"unused","deps":[{"warnings":""}]}"#, + r#"{"packageFile":"unused","deps":[{"warnings":{}}]}"#] { + let d = planted("falsy"); + let log = fs::read_to_string(d.join("renovate.log")).unwrap(); + fs::write(d.join("renovate.log"), log.replacen(r#""regex":["#, &format!(r#""regex":[{entry},"#), 1)).unwrap(); + let (code, out) = run(&d); + assert_eq!(code, 0, "{entry}: {out}"); + fs::remove_dir_all(d).unwrap(); + } + } + + #[test] + fn renovate_requires_plain_compression_and_pins_record_limit() { + let d = planted("compression-limit"); + let text = fs::read(d.join("renovate.log")).unwrap(); + let mut gz = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default()); + gz.write_all(&text).unwrap(); + fs::write(d.join("renovate.log"), gz.finish().unwrap()).unwrap(); + let (code, out) = run(&d); + assert_eq!(code, 1); assert!(out.contains("compression"), "{out}"); + fs::write(d.join("renovate.log"), vec![b'x'; (16 << 20) + 1]).unwrap(); + let (code, out) = run(&d); + assert_eq!(code, 1); assert!(out.contains("record exceeds 16777216 bytes"), "{out}"); + fs::remove_dir_all(d).unwrap(); + } + #[test] fn a_log_without_the_record_fails() { let d = planted("nolog"); From ba23556d371a958fc6e333576172867f7165aa8d Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 18:31:32 +0100 Subject: [PATCH 36/45] test(smoke): reproduce unsafe downloader destinations and leftovers --- rust/crates/og-dev/src/fixture.rs | 50 +++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/rust/crates/og-dev/src/fixture.rs b/rust/crates/og-dev/src/fixture.rs index b141a910a..eccfdc4fb 100644 --- a/rust/crates/og-dev/src/fixture.rs +++ b/rust/crates/og-dev/src/fixture.rs @@ -610,6 +610,56 @@ mod tests { fs::remove_dir_all(d).unwrap(); } + + #[test] + fn downloader_preflights_destinations_and_cleans_handled_failures() { + use std::os::unix::fs::{PermissionsExt, symlink}; + use std::process::Command; + let d = std::env::temp_dir().join(format!("og-download-safety-{}", std::process::id())); + let _ = fs::remove_dir_all(&d); + fs::create_dir_all(d.join("bin")).unwrap(); + let stub = d.join("bin/curl"); + fs::write(&stub, "#!/bin/sh\nprintf called >> \"$CALLED\"\nwhile [ \"$1\" != -o ]; do shift; done\nprintf partial > \"$2\"\nexit 22\n").unwrap(); + fs::set_permissions(&stub, fs::Permissions::from_mode(0o755)).unwrap(); + let script = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../tests/smoke/vntyper_testdata.sh"); + for fault in ["directory", "directory-link", "curl", "crc", "sha", "signal"] { + let out = d.join(fault); + fs::create_dir(&out).unwrap(); + let dst = out.join("negative.bam"); + match fault { + "directory" => fs::create_dir(&dst).unwrap(), + "directory-link" => symlink(&d, &dst).unwrap(), + _ => { + fs::write(&dst, b"PREVIOUS OUTPUT").unwrap(); + symlink("negative.bam", out.join("negative.bam.part")).unwrap(); + } + } + if fault == "crc" { + fs::write(&stub, "#!/bin/sh\nprintf called >> \"$CALLED\"\nwhile [ \"$1\" != -o ]; do shift; done\ndd if=/dev/zero of=\"$2\" bs=3376349 count=1 2>/dev/null\n").unwrap(); + } else if fault == "sha" { + // Supply an inflater with correct size but the wrong SHA. + fs::write(d.join("bin/gzip"), "#!/bin/sh\nprintf wrong-sha\n").unwrap(); + fs::set_permissions(d.join("bin/gzip"), fs::Permissions::from_mode(0o755)).unwrap(); + } else if fault == "signal" { + fs::remove_file(d.join("bin/gzip")).unwrap(); + fs::write(&stub, "#!/bin/sh\nkill -TERM \"$PPID\"\nexit 22\n").unwrap(); + } + let called = d.join("called"); + let _ = fs::remove_file(&called); + let result = Command::new("sh").arg(&script).arg(&out).arg("negative.bam") + .env("PATH", format!("{}:{}", d.join("bin").display(), std::env::var("PATH").unwrap())) + .env("CALLED", &called).output().unwrap(); + assert!(!result.status.success(), "{fault}"); + if fault.starts_with("directory") { assert!(!called.exists(), "must reject before curl: {fault}"); } + else { + assert_eq!(fs::read(&dst).unwrap(), b"PREVIOUS OUTPUT", "{fault}"); + let entries: Vec<_> = fs::read_dir(&out).unwrap().map(|e| e.unwrap().file_name()).collect(); + assert_eq!(entries.len(), 2, "{fault}: {entries:?}"); + } + } + fs::remove_dir_all(d).unwrap(); + } + #[test] fn approved_strict_pharmcat_containers_fail_at_the_cli() { let d = std::env::temp_dir().join(format!("og-pharmcat-shapes-{}", std::process::id())); From 96c3c7c93cc1b68449ac844f0b5f084b2c7bd6a8 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 18:39:45 +0100 Subject: [PATCH 37/45] fix(dev): enforce input formats and preserve publication destinations --- CHANGELOG.md | 4 +- docs/rust.md | 43 +++++ rust/crates/og-dev/src/evidence.rs | 256 +++++++++++++++++++++++------ rust/crates/og-dev/src/fixture.rs | 113 +++++++++++-- rust/crates/og-dev/src/renovate.rs | 84 ++++++++-- tests/smoke/vntyper_testdata.sh | 48 +++++- 6 files changed, 458 insertions(+), 90 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc8b84ab4..90f203d7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,9 @@ Each release's notes. From 1.0.0 on, a major or minor release has hand-written n ## Unreleased -- `og dev evidence` now rejects invalid UTF-8, truncated selected records, blank BED rows and missing panel headers. It reads LF, CRLF and CR line endings alike, with a 16 MiB limit per record. Evidence and fixture files are published by rename after validation, so a failed run preserves existing outputs. `og dev renovate-check` rejects malformed log records and present fields of the wrong type, and fails if its configured step summary cannot be written. The PharmCAT trimmer consumes source genes as it trims them. The VNtyper downloader also accepts output paths that contain a newline. +- CI evidence and Renovate inputs now enforce their original explicit gzip or plain format above strict UTF-8 and universal-newline handling. Renovate normalizes repository sources too and accepts falsy warnings and packages with no dependencies. Output publication follows destination symlinks, preserves existing permissions, rejects read-only files and directories, and uses the umask for new files. VNtyper uses private temporary directories with failure and signal cleanup, and GNU `mv -T` prevents publication inside a directory. Comphet stages and validates all destinations before publishing. Three renames are still not one transaction. Stricter parsing is deliberate and tested: BED refuses unsupported Unicode/control whitespace, Renovate refuses wrong-typed present values, PharmCAT refuses truthy non-list containers and non-object alleles, evidence positions must be ASCII u64 values, truth VCF rows need five columns, blank FAI rows fail, text records have a 16 MiB limit, and clash rows need a trailing VCF column after ALT. Truth VCF records get a final LF. `docs/rust.md` lists the error classes. + +- `og dev evidence` now rejects invalid UTF-8, truncated selected records, blank BED rows and missing panel headers. It reads LF, CRLF and CR line endings alike, with a 16 MiB limit per record. Single-file evidence and fixture outputs are published by rename after validation. Comphet stages and validates all three outputs before its first rename. Three renames are still not one transaction. A crash or a filesystem failure between renames can publish only part of the new set. `og dev renovate-check` rejects malformed log records and present fields of the wrong type, and fails if its configured step summary cannot be written. The PharmCAT trimmer consumes source genes as it trims them. The VNtyper downloader also accepts output paths that contain a newline. - The Python that CI and the fixtures ran outside the pipeline now runs in the Rust `og` binary. The public evidence workflow runs `og dev evidence clashes` and `og dev evidence hg002-gvcf` instead of two inline Python programs; the Renovate dry run checks what Renovate found with `og dev renovate-check`, whose list of non-image pins is now data in `scripts/ci/renovate-pins.tsv`; `og dev fixture comphet` writes the compound-het fixture of the e2e case `comphet-phasing-1-reads`, byte for byte; and `og dev fixture trim-pharmcat` trims a PharmCAT report for `tests/fixtures/pharmcat`. `tests/fixtures/comphet/make_fixture.py`, `tests/fixtures/pharmcat/trim_report.py` and `tests/smoke/vntyper_testdata.py` are gone: VNtyper's two test BAMs now come from `tests/smoke/vntyper_testdata.sh`, one HTTP range request per BAM with curl, checked by gzip against the archive's CRC-32 and size and then against the pinned sha256. On GIAB's HG002 benchmark VCF (4,048,342 records) with the chr20-22 reference, `og dev evidence hg002-gvcf` writes the same gVCF in 3.5 s and 97 MB instead of 11.8 s and 3.8 GB, because it keeps the spans the records cover instead of every covered position. No pipeline output changes. What changes on purpose: diff --git a/docs/rust.md b/docs/rust.md index c624d8f9f..91928df39 100644 --- a/docs/rust.md +++ b/docs/rust.md @@ -111,3 +111,46 @@ To add a primitive: write it in `pycompat` with unit tests, add an operation to - **Release** (`release.yml`): a tag builds the four binaries, writes `SHA256SUMS`, attests them and attaches all five files to the release. A pull request that changes `rust/` or the workflow builds them as a dry run. [Testing](testing.md) has the rest of CI. + +## CI evidence and fixture verbs + +`og dev evidence clashes` reads two gzip VCFs and gzip archaic tables. +`og dev evidence hg002-gvcf` reads a gzip truth VCF, plain BED, plain FASTA +and FAI, and a plain sites table. `og dev renovate-check` reads a plain log +and plain repository sources. Text uses strict UTF-8 and universal newlines +(LF, CRLF and CR). Compression is explicit, even when a filename is misleading. + +Evidence, `og dev fixture comphet`, `og dev fixture trim-pharmcat` and the +Renovate summary stage output beside its resolved destination. A file symlink +stays a symlink, existing permission bits are preserved, and read-only files +and directory destinations fail. New files use the caller's umask. +The VNtyper downloader follows the same destination policy, uses a private +`mktemp -d` directory, and removes it on handled failures and signals. It needs +GNU `mv -T` (`gmv` on macOS) so a directory can never receive a temporary BAM. + +Comphet stages all three files and checks all three destinations before the +first rename. A bad later destination leaves previous outputs intact. +Three renames are still not one transaction. A crash or a filesystem failure +between renames can publish only part of the new set. Unhandled SIGKILL can +leave temporary files. Single-file outputs are replaced by one rename. + +### Stricter than the replaced Python, on purpose + +Each refusal below has a pinned negative CLI test with exit 1 and a stable +error class in `og-dev`. The final-LF normalization has a positive byte test. + +- BED fields use spaces and tabs as separators. Other Python whitespace + (Unicode White_Space and U+001C through U+001F) fails with `unsupported whitespace`. +- Renovate present fields of the wrong type fail with `not a map of package file lists`. + This includes map-valued updates and numeric dependency names, current values, + or update values. Falsy warnings and packages with no dependencies remain valid. +- PharmCAT truthy strings or objects where lists belong fail with `is not a list`. +- PharmCAT truthy non-object alleles fail with `is not an object`. +- Evidence positions with underscores, non-ASCII digits, negative values or + values outside u64 fail with `not a number`. +- Four-field truth VCF rows and blank FAI rows fail with `fewer than 5 columns`. +- Text records over 16 MiB, including ignored headers or log lines, fail with + `record exceeds 16777216 bytes`. +- Clash VCF rows need a tab after ALT. A newline in the ALT field of a short + row fails with `missing trailing VCF columns`. +- A truth VCF record without its final LF gets one in the output. Valid VCF ends with LF. diff --git a/rust/crates/og-dev/src/evidence.rs b/rust/crates/og-dev/src/evidence.rs index 09df60bab..64251aa40 100644 --- a/rust/crates/og-dev/src/evidence.rs +++ b/rust/crates/og-dev/src/evidence.rs @@ -22,8 +22,8 @@ use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::fs::{self, File}; -use std::io::{BufRead, BufWriter, Write}; -use std::os::unix::fs::FileExt; +use std::io::{BufRead, BufReader, BufWriter, Write}; +use std::os::unix::fs::{FileExt, PermissionsExt}; use std::path::Path; use std::path::PathBuf; use std::sync::atomic::{AtomicU64, Ordering}; @@ -64,18 +64,45 @@ pub fn cli(args: &[String], out: &mut dyn Write, err: &mut dyn Write) -> u8 { } } -fn open(path: &str) -> Result, String> { - og_io::open(path).map_err(|e| format!("{path}: {e}")) +fn open(path: &str, gzip: bool) -> Result, String> { + let mut file = BufReader::new(File::open(path).map_err(|e| format!("{path}: {e}"))?); + let head = file.fill_buf().map_err(|e| format!("{path}: {e}"))?; + if head.starts_with(&[0x1f, 0x8b]) != gzip { + return Err(format!( + "{path}: wrong compression (expected {})", + if gzip { "gzip" } else { "plain" } + )); + } + if gzip { + og_io::reader(file).map_err(|e| format!("{path}: {e}")) + } else { + Ok(Box::new(file)) + } +} + +/// Plain repository text with Python's universal-newline normalization. +pub(crate) fn read_text(path: &Path) -> Result { + use std::io::Read; + let mut text = String::new(); + open(&path.to_string_lossy(), false)? + .read_to_string(&mut text) + .map_err(|e| format!("{}: invalid text: {e}", path.display()))?; + Ok(text.replace("\r\n", "\n").replace('\r', "\n")) } /// Strict UTF-8 and universal newlines, with a 16 MiB record limit. This /// local reader can use og-io's bounded reader once that API is available. -pub(crate) fn lines( +pub(crate) fn lines(path: &str, f: impl FnMut(&[u8]) -> Result<(), String>) -> Result<(), String> { + input_lines(path, false, f) +} + +fn input_lines( path: &str, + gzip: bool, mut f: impl FnMut(&[u8]) -> Result<(), String>, ) -> Result<(), String> { const MAX_LINE: usize = 16 << 20; - let mut r = open(path)?; + let mut r = open(path, gzip)?; let mut line = Vec::new(); let mut after_cr = false; loop { @@ -112,8 +139,63 @@ pub(crate) fn lines( } } +/// Follow the final symlink, including relative and dangling link targets. +fn resolve_destination(destination: &Path) -> Result { + let mut path = destination.to_owned(); + for _ in 0..40 { + match fs::read_link(&path) { + Ok(target) => { + path = if target.is_absolute() { + target + } else { + path.parent().unwrap_or(Path::new(".")).join(target) + } + } + Err(e) + if matches!( + e.kind(), + std::io::ErrorKind::InvalidInput | std::io::ErrorKind::NotFound + ) => + { + return Ok(path); + } + Err(e) => return Err(format!("{}: {e}", path.display())), + } + } + Err(format!( + "{}: too many destination symlinks", + destination.display() + )) +} + +fn validate_destination(path: &Path) -> Result, String> { + match fs::metadata(path) { + Ok(meta) => { + if !meta.is_file() { + return Err(format!( + "{}: destination is not a regular file", + path.display() + )); + } + let permissions = meta.permissions(); + if permissions.mode() & 0o222 == 0 { + return Err(format!("{}: read-only destination", path.display())); + } + // Check ACLs and effective-user access without truncating the old file. + File::options() + .write(true) + .open(path) + .map_err(|e| format!("{}: {e}", path.display()))?; + Ok(Some(permissions)) + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(e) => Err(format!("{}: {e}", path.display())), + } +} + /// A unique, exclusively created sibling. Dropping it on any read, write -/// or rename error removes the temporary file and preserves the destination. +/// or rename error removes this temporary file. A multi-file set is staged +/// together, but its renames are separate transactions. pub(crate) struct PendingOutput { path: PathBuf, destination: PathBuf, @@ -122,6 +204,8 @@ pub(crate) struct PendingOutput { impl PendingOutput { pub(crate) fn new(destination: &Path) -> Result<(Self, File), String> { static NEXT: AtomicU64 = AtomicU64::new(0); + let destination = resolve_destination(destination)?; + let permissions = validate_destination(&destination)?; let name = destination.file_name().ok_or("output has no file name")?; loop { let mut tmp = std::ffi::OsString::from("."); @@ -134,13 +218,15 @@ impl PendingOutput { let path = destination.with_file_name(tmp); match File::options().write(true).create_new(true).open(&path) { Ok(file) => { - return Ok(( - Self { - path, - destination: destination.to_owned(), - }, - file, - )); + let pending = Self { + path, + destination: destination.to_owned(), + }; + if let Some(permissions) = permissions { + file.set_permissions(permissions) + .map_err(|e| format!("{}: {e}", destination.display()))?; + } + return Ok((pending, file)); } Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue, Err(e) => return Err(format!("{}: {e}", destination.display())), @@ -149,6 +235,7 @@ impl PendingOutput { } pub(crate) fn commit(self) -> Result<(), String> { + validate_destination(&self.destination)?; fs::rename(&self.path, &self.destination) .map_err(|e| format!("{}: {e}", self.destination.display())) } @@ -189,7 +276,7 @@ fn clash_positions( ) -> Result, u64)>, String> { let mut hit = BTreeSet::new(); let (mut key, mut n, mut snvs): (Option<(Vec, u64)>, u32, u32) = (None, 0, 0); - lines(path, |line| { + input_lines(path, true, |line| { if line.first() == Some(&b'#') { return Ok(()); } @@ -197,9 +284,9 @@ fn clash_positions( if !chroms.contains(c[0]) { return Ok(()); } - if c.len() < 5 { + if c.len() < 6 { return Err(format!( - "{path}: a selected record has fewer than 5 columns" + "{path}: a selected record has fewer than 5 columns or missing trailing VCF columns" )); } let k = (c[0].to_vec(), pos(c[1], path)?); @@ -253,7 +340,7 @@ fn clashes(raw: &str, split: &str, data: &str, out: &str) -> Result<(), String> continue; } let path = table.to_string_lossy().into_owned(); - lines(&path, |line| { + input_lines(&path, true, |line| { let c: Vec<&[u8]> = line.splitn(3, |&b| b == b'\t').collect(); if c[0] != chrom.as_slice() { return Ok(()); @@ -339,15 +426,32 @@ fn hg002_gvcf( ) -> Result<(u64, u64), String> { let fai = read_fai(&format!("{reference}.fai"))?; let fasta = File::open(reference).map_err(|e| format!("{reference}: {e}"))?; + let mut magic = [0; 2]; + if fasta + .read_at(&mut magic, 0) + .map_err(|e| format!("{reference}: {e}"))? + == 2 + && magic == [0x1f, 0x8b] + { + return Err(format!("{reference}: wrong compression (expected plain)")); + } // The benchmark regions, BED 0-based: (start, end) per chromosome, // sorted. A site p (1-based) is confident when the region with the // largest start below p ends at p or later, as the Python's bisect did. let mut conf: HashMap, Vec<(u64, u64)>> = HashMap::new(); lines(bed, |line| { let f: Vec<&[u8]> = line - .split(|b| b.is_ascii_whitespace()) + .split(|b| matches!(b, b' ' | b'\t')) .filter(|x| !x.is_empty()) .collect(); + if f.iter().any(|field| { + std::str::from_utf8(field) + .expect("strict reader") + .chars() + .any(|ch| ch.is_whitespace() || matches!(ch, '\u{1c}'..='\u{1f}')) + }) { + return Err(format!("{bed}: unsupported whitespace in a field")); + } if f.len() < 3 { return Err(format!("{bed}: a row has fewer than 3 columns")); } @@ -371,7 +475,7 @@ fn hg002_gvcf( let werr = |e: std::io::Error| format!("{out}: {e}"); let mut covered: HashMap, Vec<(u64, u64)>> = HashMap::new(); let mut records = 0u64; - lines(truth, |line| { + input_lines(truth, true, |line| { if line.starts_with(b"##") { } else if line.first() == Some(&b'#') { w.write_all(b"##INFO=\n##ALT=\n") @@ -384,10 +488,13 @@ fn hg002_gvcf( let p = pos(f[1], truth)?; covered.entry(f[0].to_vec()).or_default().push(( p, - p + std::str::from_utf8(f[3]) - .expect("strict reader") - .chars() - .count() as u64, + p.checked_add( + std::str::from_utf8(f[3]) + .expect("strict reader") + .chars() + .count() as u64, + ) + .ok_or_else(|| format!("{truth}: position span overflows"))?, )); records += 1; } @@ -516,7 +623,7 @@ mod tests { fn text(&self, name: &str) -> Vec { use std::io::Read; let mut bytes = Vec::new(); - open(self.dir.join(name).to_str().unwrap()) + open(self.dir.join(name).to_str().unwrap(), name.ends_with(".gz")) .unwrap() .read_to_end(&mut bytes) .unwrap(); @@ -534,22 +641,29 @@ mod tests { } fn run(&self) -> Result<(), String> { let p = |n: &str| self.dir.join(n).to_str().unwrap().to_owned(); - if self.kind == "clashes" { - clashes( - &p("raw.vcf.gz"), - &p("split.vcf.gz"), - self.dir.to_str().unwrap(), - &p("out"), - ) + let args = if self.kind == "clashes" { + vec![ + "clashes".to_owned(), + p("raw.vcf.gz"), + p("split.vcf.gz"), + self.dir.to_str().unwrap().to_owned(), + p("out"), + ] } else { - hg002_gvcf( - &p("truth.vcf.gz"), - &p("truth.bed"), - &p("ref.fasta"), - &p("sites.tsv"), - &p("out"), - ) - .map(|_| ()) + vec![ + "hg002-gvcf".to_owned(), + p("truth.vcf.gz"), + p("truth.bed"), + p("ref.fasta"), + p("sites.tsv"), + p("out"), + ] + }; + let (mut out, mut err) = (Vec::new(), Vec::new()); + match cli(&args, &mut out, &mut err) { + 0 => Ok(()), + 1 => Err(String::from_utf8(err).unwrap()), + code => panic!("unexpected CLI exit {code}: {err:?}"), } } fn fails_without_publication(&self) { @@ -690,19 +804,31 @@ mod tests { } } - #[test] fn explicit_compression_is_required_at_every_input() { for (kind, names) in [ ("clashes", vec!["raw.vcf.gz", "split.vcf.gz", "chrA.tsv.gz"]), - ("hg002-gvcf", vec!["truth.vcf.gz", "truth.bed", "ref.fasta.fai", "sites.tsv"]), + ( + "hg002-gvcf", + vec![ + "truth.vcf.gz", + "truth.bed", + "ref.fasta.fai", + "ref.fasta", + "sites.tsv", + ], + ), ] { for name in names { let c = Case::new(kind, &format!("compression-{name}")); let bytes = c.text(name); - let wrong = if name.ends_with(".gz") { bytes } else { - let mut gz = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default()); - gz.write_all(&bytes).unwrap(); gz.finish().unwrap() + let wrong = if name.ends_with(".gz") { + bytes + } else { + let mut gz = + flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default()); + gz.write_all(&bytes).unwrap(); + gz.finish().unwrap() }; fs::write(c.dir.join(name), wrong).unwrap(); c.fails_without_publication(); @@ -713,12 +839,17 @@ mod tests { #[test] fn bed_rejects_python_whitespace_inside_fields_and_as_separators() { - for ch in ['\u{2028}', '\u{85}', '\u{b}', '\u{a0}', '\u{1c}', '\u{1d}', '\u{1e}', '\u{1f}'] { + for ch in [ + '\u{2028}', '\u{85}', '\u{b}', '\u{a0}', '\u{1c}', '\u{1d}', '\u{1e}', '\u{1f}', + ] { for embedded in [true, false] { let c = Case::new("hg002-gvcf", &format!("space-{}-{embedded}", ch as u32)); let text = String::from_utf8(c.text("truth.bed")).unwrap(); - let text = if embedded { text.replacen("chrA", &format!("chr{ch}A"), 1) } - else { text.replace('\t', &ch.to_string()) }; + let text = if embedded { + text.replacen("chrA", &format!("chr{ch}A"), 1) + } else { + text.replace('\t', &ch.to_string()) + }; c.put("truth.bed", text.as_bytes()); c.fails_without_publication(); assert!(c.run().unwrap_err().contains("unsupported whitespace")); @@ -736,16 +867,28 @@ mod tests { fs::set_permissions(&target, fs::Permissions::from_mode(0o600)).unwrap(); symlink("target", &link).unwrap(); publish(&link, b"new").unwrap(); - assert!(fs::symlink_metadata(&link).unwrap().file_type().is_symlink()); + assert!( + fs::symlink_metadata(&link) + .unwrap() + .file_type() + .is_symlink() + ); assert_eq!(fs::read(&target).unwrap(), b"new"); - assert_eq!(fs::metadata(&target).unwrap().permissions().mode() & 0o777, 0o600); + assert_eq!( + fs::metadata(&target).unwrap().permissions().mode() & 0o777, + 0o600 + ); fs::set_permissions(&target, fs::Permissions::from_mode(0o444)).unwrap(); assert!(publish(&link, b"bad").unwrap_err().contains("read-only")); assert_eq!(fs::read(&target).unwrap(), b"new"); let dir_link = c.dir.join("dir-link"); symlink(&c.dir, &dir_link).unwrap(); for path in [&c.dir, &dir_link] { - assert!(publish(path, b"bad").unwrap_err().contains("not a regular file")); + assert!( + publish(path, b"bad") + .unwrap_err() + .contains("not a regular file") + ); } } @@ -753,7 +896,10 @@ mod tests { fn approved_strict_evidence_shapes_fail_with_stable_errors() { for value in ["3_0", "٣", "-3", "18446744073709551616"] { let c = Case::new("hg002-gvcf", value); - c.put("truth.vcf.gz", format!("chrA\t{value}\t.\tA\tC\n").as_bytes()); + c.put( + "truth.vcf.gz", + format!("chrA\t{value}\t.\tA\tC\n").as_bytes(), + ); c.fails_without_publication(); assert!(c.run().unwrap_err().contains("not a number")); } @@ -776,7 +922,11 @@ mod tests { let c = Case::new("clashes", "record-limit"); c.put("raw.vcf.gz", &vec![b'#'; (16 << 20) + 1]); c.fails_without_publication(); - assert!(c.run().unwrap_err().contains("record exceeds 16777216 bytes")); + assert!( + c.run() + .unwrap_err() + .contains("record exceeds 16777216 bytes") + ); let c = Case::new("hg002-gvcf", "final-lf"); c.run().unwrap(); let expected = fs::read(c.dir.join("out")).unwrap(); diff --git a/rust/crates/og-dev/src/fixture.rs b/rust/crates/og-dev/src/fixture.rs index eccfdc4fb..1ea2d5061 100644 --- a/rust/crates/og-dev/src/fixture.rs +++ b/rust/crates/og-dev/src/fixture.rs @@ -373,13 +373,24 @@ pub mod comphet { let seq = reference(); let vs = variants(&seq, swap); fs::create_dir_all(dir).map_err(|e| format!("{}: {e}", dir.display()))?; + let mut staged = Vec::new(); for (name, text) in [ ("reads.sam", sam(&seq, &vs)), ("vep.vcf", vep_vcf(&vs)), ("comphet.vcf", comphet_vcf(&vs)), ] { let path = dir.join(name); - crate::evidence::publish(&path, text.as_bytes())?; + let (pending, mut file) = crate::evidence::PendingOutput::new(&path)?; + std::io::Write::write_all(&mut file, text.as_bytes()) + .map_err(|e| format!("{}: {e}", path.display()))?; + std::io::Write::flush(&mut file).map_err(|e| format!("{}: {e}", path.display()))?; + drop(file); + staged.push(pending); + } + // Every file is staged and every destination checked before publication. + // Three renames are still not one transaction. + for pending in staged { + pending.commit()?; } Ok(()) } @@ -597,20 +608,43 @@ fn trim_pharmcat(src: &Path, dst: &Path, max: i64) -> Result<(), String> { mod tests { use super::*; + #[test] + fn pharmcat_input_is_plain_json() { + let d = + std::env::temp_dir().join(format!("og-pharmcat-compression-{}", std::process::id())); + fs::create_dir_all(&d).unwrap(); + let mut gz = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default()); + gz.write_all(br#"{"genes":{},"drugs":{}}"#).unwrap(); + fs::write(d.join("in"), gz.finish().unwrap()).unwrap(); + fs::write(d.join("out"), b"old").unwrap(); + let args = vec![ + "trim-pharmcat".into(), + d.join("in").to_str().unwrap().into(), + d.join("out").to_str().unwrap().into(), + ]; + let (mut out, mut err) = (Vec::new(), Vec::new()); + assert_eq!(cli(&args, &mut out, &mut err), 1); + assert!(String::from_utf8_lossy(&err).contains("not UTF-8")); + assert_eq!(fs::read(d.join("out")).unwrap(), b"old"); + fs::remove_dir_all(d).unwrap(); + } #[test] fn comphet_validates_later_destinations_before_publishing() { let d = std::env::temp_dir().join(format!("og-comphet-preflight-{}", std::process::id())); let _ = fs::remove_dir_all(&d); fs::create_dir_all(d.join("comphet.vcf")).unwrap(); - for name in ["reads.sam", "vep.vcf"] { fs::write(d.join(name), b"PREVIOUS OUTPUT").unwrap(); } + for name in ["reads.sam", "vep.vcf"] { + fs::write(d.join(name), b"PREVIOUS OUTPUT").unwrap(); + } assert!(comphet::write(&d, false).is_err()); - for name in ["reads.sam", "vep.vcf"] { assert_eq!(fs::read(d.join(name)).unwrap(), b"PREVIOUS OUTPUT"); } + for name in ["reads.sam", "vep.vcf"] { + assert_eq!(fs::read(d.join(name)).unwrap(), b"PREVIOUS OUTPUT"); + } assert_eq!(fs::read_dir(&d).unwrap().count(), 3); fs::remove_dir_all(d).unwrap(); } - #[test] fn downloader_preflights_destinations_and_cleans_handled_failures() { use std::os::unix::fs::{PermissionsExt, symlink}; @@ -621,8 +655,16 @@ mod tests { let stub = d.join("bin/curl"); fs::write(&stub, "#!/bin/sh\nprintf called >> \"$CALLED\"\nwhile [ \"$1\" != -o ]; do shift; done\nprintf partial > \"$2\"\nexit 22\n").unwrap(); fs::set_permissions(&stub, fs::Permissions::from_mode(0o755)).unwrap(); - let script = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../tests/smoke/vntyper_testdata.sh"); - for fault in ["directory", "directory-link", "curl", "crc", "sha", "signal"] { + let script = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../tests/smoke/vntyper_testdata.sh"); + for fault in [ + "directory", + "directory-link", + "curl", + "crc", + "sha", + "signal", + ] { let out = d.join(fault); fs::create_dir(&out).unwrap(); let dst = out.join("negative.bam"); @@ -646,14 +688,30 @@ mod tests { } let called = d.join("called"); let _ = fs::remove_file(&called); - let result = Command::new("sh").arg(&script).arg(&out).arg("negative.bam") - .env("PATH", format!("{}:{}", d.join("bin").display(), std::env::var("PATH").unwrap())) - .env("CALLED", &called).output().unwrap(); + let result = Command::new("sh") + .arg(&script) + .arg(&out) + .arg("negative.bam") + .env( + "PATH", + format!( + "{}:{}", + d.join("bin").display(), + std::env::var("PATH").unwrap() + ), + ) + .env("CALLED", &called) + .output() + .unwrap(); assert!(!result.status.success(), "{fault}"); - if fault.starts_with("directory") { assert!(!called.exists(), "must reject before curl: {fault}"); } - else { + if fault.starts_with("directory") { + assert!(!called.exists(), "must reject before curl: {fault}"); + } else { assert_eq!(fs::read(&dst).unwrap(), b"PREVIOUS OUTPUT", "{fault}"); - let entries: Vec<_> = fs::read_dir(&out).unwrap().map(|e| e.unwrap().file_name()).collect(); + let entries: Vec<_> = fs::read_dir(&out) + .unwrap() + .map(|e| e.unwrap().file_name()) + .collect(); assert_eq!(entries.len(), 2, "{fault}: {entries:?}"); } } @@ -665,15 +723,34 @@ mod tests { let d = std::env::temp_dir().join(format!("og-pharmcat-shapes-{}", std::process::id())); fs::create_dir_all(&d).unwrap(); for (input, class) in [ - (r#"{"genes":{"G":{"sourceDiplotypes":"x"}},"drugs":{}}"#, "sourceDiplotypes is not a list"), - (r#"{"genes":{"G":{"recommendationDiplotypes":"x"}},"drugs":{}}"#, "recommendationDiplotypes is not a list"), - (r#"{"genes":{"G":{"sourceDiplotypes":[{"allele1":"x"}]}},"drugs":{}}"#, "allele1 is not an object"), - (r#"{"genes":{"G":{"sourceDiplotypes":[{"allele2":["x"]}]}},"drugs":{}}"#, "allele2 is not an object"), - (r#"{"genes":{},"drugs":{"S":{"D":{"guidelines":[{"annotations":[{"genotypes":[{"diplotypes":{"x":1}}]}]}]}}}}"#, "diplotypes is not a list"), + ( + r#"{"genes":{"G":{"sourceDiplotypes":"x"}},"drugs":{}}"#, + "sourceDiplotypes is not a list", + ), + ( + r#"{"genes":{"G":{"recommendationDiplotypes":"x"}},"drugs":{}}"#, + "recommendationDiplotypes is not a list", + ), + ( + r#"{"genes":{"G":{"sourceDiplotypes":[{"allele1":"x"}]}},"drugs":{}}"#, + "allele1 is not an object", + ), + ( + r#"{"genes":{"G":{"sourceDiplotypes":[{"allele2":["x"]}]}},"drugs":{}}"#, + "allele2 is not an object", + ), + ( + r#"{"genes":{},"drugs":{"S":{"D":{"guidelines":[{"annotations":[{"genotypes":[{"diplotypes":{"x":1}}]}]}]}}}}"#, + "diplotypes is not a list", + ), ] { fs::write(d.join("in"), input).unwrap(); fs::write(d.join("out"), b"old").unwrap(); - let args = vec!["trim-pharmcat".into(), d.join("in").to_str().unwrap().into(), d.join("out").to_str().unwrap().into()]; + let args = vec![ + "trim-pharmcat".into(), + d.join("in").to_str().unwrap().into(), + d.join("out").to_str().unwrap().into(), + ]; let (mut out, mut err) = (Vec::new(), Vec::new()); assert_eq!(cli(&args, &mut out, &mut err), 1); assert!(String::from_utf8_lossy(&err).contains(class), "{err:?}"); diff --git a/rust/crates/og-dev/src/renovate.rs b/rust/crates/og-dev/src/renovate.rs index 87a368999..e79644344 100644 --- a/rust/crates/og-dev/src/renovate.rs +++ b/rust/crates/og-dev/src/renovate.rs @@ -119,7 +119,7 @@ fn cli_with_summary( return 2; } }; - let env_text = match fs::read_to_string(repo.join(ENV)) { + let env_text = match crate::evidence::read_text(&repo.join(ENV)) { Ok(t) => t, Err(e) => { let _ = writeln!(err, "og dev renovate-check: {ENV}: {e}"); @@ -222,7 +222,7 @@ fn glob(repo: &Path, pattern: &str) -> Result, String> { } fn read_pins(repo: &Path, path: &Path) -> Result { - let text = fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()))?; + let text = crate::evidence::read_text(path)?; let mut pins = Pins { want: BTreeMap::new(), files: HashMap::new(), @@ -246,7 +246,7 @@ fn read_pins(repo: &Path, path: &Path) -> Result { } for f in glob(repo, files).map_err(|e| at(&e))? { if !texts.contains_key(&f) { - let t = fs::read_to_string(repo.join(&f)).map_err(|e| at(&format!("{f}: {e}")))?; + let t = crate::evidence::read_text(&repo.join(&f)).map_err(|e| at(&e))?; texts.insert(f.clone(), t); } let n = re.find_iter(&texts[&f]).count(); @@ -300,14 +300,17 @@ fn load(log: &str) -> Result, String> { let Value::Object(mut pf) = pf else { return Err(shape()); }; - let Some(Value::Str(name)) = pf.swap_remove("packageFile") else { - return Err(shape()); - }; let ds = match pf.swap_remove("deps") { None => Vec::new(), Some(Value::Array(ds)) => ds, _ => return Err(shape()), }; + if ds.is_empty() { + continue; + } + let Some(Value::Str(name)) = pf.swap_remove("packageFile") else { + return Err(shape()); + }; for d in ds { let Value::Object(d) = d else { return Err(shape()); @@ -343,7 +346,7 @@ fn load(log: &str) -> Result, String> { return Err(shape()); } } - if let Some(ws) = d.get("warnings") { + if let Some(ws) = d.get("warnings").filter(|v| truthy(v)) { let Value::Array(ws) = ws else { return Err(shape()); }; @@ -917,7 +920,23 @@ mod tests { r#"{"packageFile":"unused","deps":[{"updates":[42]}]}"#, ), ("scalar-dep", r#"{"packageFile":"unused","deps":[42]}"#), - ("scalar-file", r#"{"packageFile":[],"deps":[]}"#), + ("scalar-file", r#"{"packageFile":[],"deps":[{}]}"#), + ( + "map-updates", + r#"{"packageFile":"unused","deps":[{"updates":{}}]}"#, + ), + ( + "number-update", + r#"{"packageFile":"unused","deps":[{"updates":[{"newValue":0}]}]}"#, + ), + ( + "number-name", + r#"{"packageFile":"unused","deps":[{"depName":0}]}"#, + ), + ( + "number-current", + r#"{"packageFile":"unused","deps":[{"currentValue":0}]}"#, + ), ] { let d = planted(label); let log = fs::read_to_string(d.join("renovate.log")).unwrap(); @@ -925,6 +944,10 @@ mod tests { fs::write(d.join("renovate.log"), log).unwrap(); let (code, out) = run(&d); assert_eq!(code, 1, "{label}: {out}"); + assert!( + out.contains("not a map of package file lists"), + "{label}: {out}" + ); fs::remove_dir_all(d).unwrap(); } for (label, prefix) in [ @@ -941,14 +964,19 @@ mod tests { } } - #[test] fn repository_sources_use_universal_newlines() { for name in [ENV, PINS, ".github/workflows/ci.yml"] { for ending in ["\r", "\r\n"] { - let d = planted(&format!("newlines-{}-{}", name.replace('/', "-"), ending.len())); + let d = planted(&format!( + "newlines-{}-{}", + name.replace('/', "-"), + ending.len() + )); let baseline = run(&d); - let text = fs::read_to_string(d.join(name)).unwrap().replace('\n', ending); + let text = fs::read_to_string(d.join(name)) + .unwrap() + .replace('\n', ending); fs::write(d.join(name), text).unwrap(); assert_eq!(run(&d), baseline, "{name}/{ending:?}"); fs::remove_dir_all(d).unwrap(); @@ -958,20 +986,42 @@ mod tests { #[test] fn real_log_falsy_warnings_and_absent_empty_packages_are_accepted() { - for entry in [r#"{}"#, r#"{"deps":[]}"#, r#"{"deps":[],"packageFile":[]}"#, + for entry in [ + r#"{}"#, + r#"{"deps":[]}"#, + r#"{"deps":[],"packageFile":[]}"#, r#"{"packageFile":"unused","deps":[{"warnings":null}]}"#, r#"{"packageFile":"unused","deps":[{"warnings":false}]}"#, r#"{"packageFile":"unused","deps":[{"warnings":""}]}"#, - r#"{"packageFile":"unused","deps":[{"warnings":{}}]}"#] { + r#"{"packageFile":"unused","deps":[{"warnings":{}}]}"#, + ] { let d = planted("falsy"); let log = fs::read_to_string(d.join("renovate.log")).unwrap(); - fs::write(d.join("renovate.log"), log.replacen(r#""regex":["#, &format!(r#""regex":[{entry},"#), 1)).unwrap(); + fs::write( + d.join("renovate.log"), + log.replacen(r#""regex":["#, &format!(r#""regex":[{entry},"#), 1), + ) + .unwrap(); let (code, out) = run(&d); assert_eq!(code, 0, "{entry}: {out}"); fs::remove_dir_all(d).unwrap(); } } + #[test] + fn repository_sources_reject_gzip_even_under_plain_names() { + for name in [ENV, PINS, ".github/workflows/ci.yml"] { + let d = planted(&format!("compressed-{}", name.replace('/', "-"))); + let mut gz = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default()); + gz.write_all(&fs::read(d.join(name)).unwrap()).unwrap(); + fs::write(d.join(name), gz.finish().unwrap()).unwrap(); + let (code, out) = run(&d); + assert_eq!(code, 2, "{out}"); + assert!(out.contains("wrong compression"), "{out}"); + fs::remove_dir_all(d).unwrap(); + } + } + #[test] fn renovate_requires_plain_compression_and_pins_record_limit() { let d = planted("compression-limit"); @@ -980,10 +1030,12 @@ mod tests { gz.write_all(&text).unwrap(); fs::write(d.join("renovate.log"), gz.finish().unwrap()).unwrap(); let (code, out) = run(&d); - assert_eq!(code, 1); assert!(out.contains("compression"), "{out}"); + assert_eq!(code, 1); + assert!(out.contains("compression"), "{out}"); fs::write(d.join("renovate.log"), vec![b'x'; (16 << 20) + 1]).unwrap(); let (code, out) = run(&d); - assert_eq!(code, 1); assert!(out.contains("record exceeds 16777216 bytes"), "{out}"); + assert_eq!(code, 1); + assert!(out.contains("record exceeds 16777216 bytes"), "{out}"); fs::remove_dir_all(d).unwrap(); } diff --git a/tests/smoke/vntyper_testdata.sh b/tests/smoke/vntyper_testdata.sh index e6d7da2a8..73c41d89c 100755 --- a/tests/smoke/vntyper_testdata.sh +++ b/tests/smoke/vntyper_testdata.sh @@ -44,12 +44,46 @@ shift for name in "$@"; do member "$name" > /dev/null || { echo "vntyper_testdata.sh: unknown member ${name} (positive.bam, negative.bam)" >&2; exit 2; } done +# GNU mv's -T rejects a directory even if it appears during publication. +# Homebrew calls it gmv on macOS; the pinned Linux image calls it mv. +if command -v gmv > /dev/null 2>&1; then move="gmv"; else move="mv"; fi + +# Resolve file symlinks, including relative/dangling targets, before staging. +destination() { + dest="${out}/${1}" + links=0 + while [ -L "$dest" ]; do + links=$((links + 1)) + [ "$links" -le 40 ] || { echo "destination: too many symlinks: $dest" >&2; return 1; } + target=$(readlink "$dest"; printf x) + target=${target%x} + target=${target%' +'} + case $target in + /*) dest=$target ;; + *) dest="${dest%/*}/$target" ;; + esac + done + [ ! -d "$dest" ] || { echo "destination is a directory: $dest" >&2; return 1; } + if [ -e "$dest" ]; then + [ -f "$dest" ] && [ -w "$dest" ] || { echo "destination is not a writable regular file: $dest" >&2; return 1; } + fi +} + +work= +cleanup() { [ -z "$work" ] || rm -rf "$work"; } +trap cleanup EXIT +trap 'exit 1' HUP INT TERM mkdir -p "$out" +# A bad destination must fail before any network request. +for name in "$@"; do destination "$name"; done for name in "$@"; do read -r path off csize size crc want <&2; exit 1; } rm -f "${part}.raw" "${part}.gz" - mv "$part" "${out}/${name}" + # Preserve the Python output mode. New files inherit the caller's umask. + if [ -e "$dest" ]; then + case $(uname -s) in + Darwin) mode=$(stat -f '%Lp' "$dest") ;; + *) mode=$(stat -c '%a' "$dest") ;; + esac + chmod "$mode" "$part" + fi + "$move" -fT "$part" "$dest" + cleanup + work= printf '%s\t%s\t%s\t%s\n' "$name" "$path" "$size" "$got" done From da245a62c392b6c89d3593c5251641f95e7c2d80 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 18:47:05 +0100 Subject: [PATCH 38/45] fix(dev): preserve special mode bits after staged writes --- rust/crates/og-dev/src/evidence.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/rust/crates/og-dev/src/evidence.rs b/rust/crates/og-dev/src/evidence.rs index 64251aa40..2210e9e5b 100644 --- a/rust/crates/og-dev/src/evidence.rs +++ b/rust/crates/og-dev/src/evidence.rs @@ -235,7 +235,12 @@ impl PendingOutput { } pub(crate) fn commit(self) -> Result<(), String> { - validate_destination(&self.destination)?; + if let Some(permissions) = validate_destination(&self.destination)? { + // A write can clear set-user-ID or set-group-ID bits. Apply the + // final mode after every byte is staged, immediately before rename. + fs::set_permissions(&self.path, permissions) + .map_err(|e| format!("{}: {e}", self.destination.display()))?; + } fs::rename(&self.path, &self.destination) .map_err(|e| format!("{}: {e}", self.destination.display())) } @@ -878,6 +883,12 @@ mod tests { fs::metadata(&target).unwrap().permissions().mode() & 0o777, 0o600 ); + fs::set_permissions(&target, fs::Permissions::from_mode(0o4600)).unwrap(); + publish(&link, b"new").unwrap(); + assert_eq!( + fs::metadata(&target).unwrap().permissions().mode() & 0o7777, + 0o4600 + ); fs::set_permissions(&target, fs::Permissions::from_mode(0o444)).unwrap(); assert!(publish(&link, b"bad").unwrap_err().contains("read-only")); assert_eq!(fs::read(&target).unwrap(), b"new"); From 154fd3b8fcb3b5c978b4d50d50d7422af789a331 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 18:51:57 +0100 Subject: [PATCH 39/45] fix(dev): keep present Renovate package file values typed --- rust/crates/og-dev/src/renovate.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/rust/crates/og-dev/src/renovate.rs b/rust/crates/og-dev/src/renovate.rs index e79644344..c206ac819 100644 --- a/rust/crates/og-dev/src/renovate.rs +++ b/rust/crates/og-dev/src/renovate.rs @@ -305,12 +305,17 @@ fn load(log: &str) -> Result, String> { Some(Value::Array(ds)) => ds, _ => return Err(shape()), }; + let name = match pf.swap_remove("packageFile") { + None => None, + Some(Value::Str(name)) => Some(name), + // An unused field may be absent, but a present wrong type is + // still a deliberate input error. + _ => return Err(shape()), + }; if ds.is_empty() { continue; } - let Some(Value::Str(name)) = pf.swap_remove("packageFile") else { - return Err(shape()); - }; + let name = name.ok_or_else(shape)?; for d in ds { let Value::Object(d) = d else { return Err(shape()); @@ -920,7 +925,7 @@ mod tests { r#"{"packageFile":"unused","deps":[{"updates":[42]}]}"#, ), ("scalar-dep", r#"{"packageFile":"unused","deps":[42]}"#), - ("scalar-file", r#"{"packageFile":[],"deps":[{}]}"#), + ("scalar-file", r#"{"packageFile":[],"deps":[]}"#), ( "map-updates", r#"{"packageFile":"unused","deps":[{"updates":{}}]}"#, @@ -989,7 +994,6 @@ mod tests { for entry in [ r#"{}"#, r#"{"deps":[]}"#, - r#"{"deps":[],"packageFile":[]}"#, r#"{"packageFile":"unused","deps":[{"warnings":null}]}"#, r#"{"packageFile":"unused","deps":[{"warnings":false}]}"#, r#"{"packageFile":"unused","deps":[{"warnings":""}]}"#, From 145cdd0b4994b0664624dc4d1737a212abdc5614 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 19:23:49 +0100 Subject: [PATCH 40/45] test(dev): reproduce offset, stream and late-header failures --- rust/crates/og-dev/src/evidence.rs | 71 +++++++++++++++++++++++++++++- rust/crates/og-dev/src/fixture.rs | 44 ++++++++++++++++++ rust/crates/og-dev/src/lib.rs | 46 ++++++++++++++++++- rust/crates/og-dev/src/renovate.rs | 19 ++++++++ 4 files changed, 178 insertions(+), 2 deletions(-) diff --git a/rust/crates/og-dev/src/evidence.rs b/rust/crates/og-dev/src/evidence.rs index 2210e9e5b..547bf903b 100644 --- a/rust/crates/og-dev/src/evidence.rs +++ b/rust/crates/og-dev/src/evidence.rs @@ -696,6 +696,75 @@ mod tests { } } + #[test] + fn fai_offsets_reject_overflow_and_out_of_file_before_publication() { + for (offset, bases, bytes, position) in [ + (18446744073709551598u64, 1, 2, 13), + (1, 1, u64::MAX, 3), + (u64::MAX, 1, 1, 1), + (1000, 50, 51, 1), + ] { + let c = Case::new("hg002-gvcf", &format!("offset-{offset}-{position}")); + c.put("ref.fasta.fai", format!("chrA\t50\t{offset}\t{bases}\t{bytes}\n").as_bytes()); + c.put("sites.tsv", format!("id\tchr\tpos\nx\tchrA\t{position}\n").as_bytes()); + c.put("truth.bed", b"chrA\t0\t50\n"); + c.put("truth.vcf.gz", b"##fileformat=VCFv4.2\n#CHROM\tPOS\tID\tREF\tALT\n"); + c.fails_without_publication(); + assert!(c.run().unwrap_err().contains(": invalid FASTA offset:")); + } + } + + #[test] + fn late_vcf_headers_fail_before_publication() { + for header in [b"##late=yes\n".as_slice(), b"#CHROM\tPOS\n"] { + let c = Case::new("hg002-gvcf", &format!("late-{}", header.len())); + c.put("truth.vcf.gz", &[c.text("truth.vcf.gz"), header.to_vec()].concat()); + c.fails_without_publication(); + assert!(c.run().unwrap_err().contains(": VCF header after first data record")); + } + } + + #[test] + fn empty_gzip_inputs_and_unused_zero_width_fai_rows_fail() { + for (kind, name) in [ + ("clashes", "raw.vcf.gz"), ("clashes", "split.vcf.gz"), + ("clashes", "chrA.tsv.gz"), ("hg002-gvcf", "truth.vcf.gz"), + ] { + let c = Case::new(kind, name); + fs::write(c.dir.join(name), b"").unwrap(); + c.fails_without_publication(); + assert!(c.run().unwrap_err().contains(": wrong compression (expected gzip)")); + } + let c = Case::new("hg002-gvcf", "unused-row"); + c.put("ref.fasta.fai", &[c.text("ref.fasta.fai"), b"chrUnused\t0\t0\t0\t0\n".to_vec()].concat()); + c.fails_without_publication(); + assert!(c.run().unwrap_err().contains(": a row has 0 bases per line")); + } + + #[test] + fn gzip_fasta_with_ascii_fname_is_refused_by_format_guard() { + let c = Case::new("hg002-gvcf", "fname"); + let mut gz = flate2::GzBuilder::new().filename(vec![b'A'; 200]) + .write(Vec::new(), flate2::Compression::default()); + gz.write_all(&c.text("ref.fasta")).unwrap(); + fs::write(c.dir.join("ref.fasta"), gz.finish().unwrap()).unwrap(); + c.fails_without_publication(); + assert!(c.run().unwrap_err().contains(": wrong compression (expected plain)")); + } + + #[test] + fn non_writable_parents_preserve_evidence_destinations() { + for kind in ["clashes", "hg002-gvcf"] { + let c = Case::new(kind, "parent-mode"); + fs::write(c.dir.join("out"), b"PREVIOUS OUTPUT\n").unwrap(); + fs::set_permissions(&c.dir, fs::Permissions::from_mode(0o555)).unwrap(); + let result = c.run(); + fs::set_permissions(&c.dir, fs::Permissions::from_mode(0o755)).unwrap(); + assert!(result.unwrap_err().contains(": parent directory is not writable")); + assert_eq!(fs::read(c.dir.join("out")).unwrap(), b"PREVIOUS OUTPUT\n"); + } + } + #[test] fn selected_truncated_clash_records_fail() { for name in ["raw.vcf.gz", "split.vcf.gz", "chrA.tsv.gz"] { @@ -837,7 +906,7 @@ mod tests { }; fs::write(c.dir.join(name), wrong).unwrap(); c.fails_without_publication(); - assert!(c.run().unwrap_err().contains("compression"), "{name}"); + assert!(c.run().unwrap_err().contains(": wrong compression (expected "), "{name}"); } } } diff --git a/rust/crates/og-dev/src/fixture.rs b/rust/crates/og-dev/src/fixture.rs index 1ea2d5061..3371b7a91 100644 --- a/rust/crates/og-dev/src/fixture.rs +++ b/rust/crates/og-dev/src/fixture.rs @@ -608,6 +608,50 @@ fn trim_pharmcat(src: &Path, dst: &Path, max: i64) -> Result<(), String> { mod tests { use super::*; + #[test] + fn non_ascii_or_out_of_range_annotation_limits_fail_at_cli() { + let d = std::env::temp_dir().join(format!("og-limits-{}", std::process::id())); + fs::create_dir_all(&d).unwrap(); + fs::write(d.join("in"), br#"{"genes":{},"drugs":{}}"#).unwrap(); + for limit in ["1_0", "٤", "18446744073709551616"] { + fs::write(d.join("out"), b"old").unwrap(); + let args = vec!["trim-pharmcat".into(), d.join("in").to_str().unwrap().into(), + d.join("out").to_str().unwrap().into(), limit.into()]; + let (mut out, mut err) = (Vec::new(), Vec::new()); + assert_eq!(cli(&args, &mut out, &mut err), 2); + let err = String::from_utf8(err).unwrap(); + assert!(err.contains("error: MAX_ANNOTATIONS") && err.contains("is not a whole number"), "{err}"); + assert_eq!(fs::read(d.join("out")).unwrap(), b"old"); + } + fs::remove_dir_all(d).unwrap(); + } + + #[test] + fn non_writable_parents_preserve_fixture_destinations() { + use std::os::unix::fs::PermissionsExt; + let d = std::env::temp_dir().join(format!("og-parent-{}", std::process::id())); + fs::create_dir_all(&d).unwrap(); + fs::write(d.join("in"), br#"{"genes":{},"drugs":{}}"#).unwrap(); + for name in ["out", "reads.sam", "vep.vcf", "comphet.vcf"] { + fs::write(d.join(name), b"old").unwrap(); + } + for args in [ + vec!["trim-pharmcat".into(), d.join("in").to_str().unwrap().into(), d.join("out").to_str().unwrap().into()], + vec!["comphet".into(), d.to_str().unwrap().into()], + ] { + fs::set_permissions(&d, fs::Permissions::from_mode(0o555)).unwrap(); + let (mut out, mut err) = (Vec::new(), Vec::new()); + let code = cli(&args, &mut out, &mut err); + fs::set_permissions(&d, fs::Permissions::from_mode(0o755)).unwrap(); + assert_eq!(code, 1); + assert!(String::from_utf8(err).unwrap().contains(": parent directory is not writable")); + for name in ["out", "reads.sam", "vep.vcf", "comphet.vcf"] { + assert_eq!(fs::read(d.join(name)).unwrap(), b"old"); + } + } + fs::remove_dir_all(d).unwrap(); + } + #[test] fn pharmcat_input_is_plain_json() { let d = diff --git a/rust/crates/og-dev/src/lib.rs b/rust/crates/og-dev/src/lib.rs index d4a2aad52..14eb34dfb 100644 --- a/rust/crates/og-dev/src/lib.rs +++ b/rust/crates/og-dev/src/lib.rs @@ -61,7 +61,14 @@ pub(crate) fn py_os_error(e: &std::io::Error, path: &str) -> String { fn run(f: fn(&[String], &mut dyn Write, &mut dyn Write) -> u8, args: &[String]) -> u8 { let mut out = std::io::stdout().lock(); let mut err = std::io::stderr().lock(); - let code = f(args, &mut out, &mut err); + run_with_writers(f, args, &mut out, &mut err) +} + +pub(crate) fn run_with_writers( + f: fn(&[String], &mut dyn Write, &mut dyn Write) -> u8, + args: &[String], out: &mut dyn Write, err: &mut dyn Write, +) -> u8 { + let code = f(args, out, err); let _ = out.flush(); code } @@ -95,3 +102,40 @@ pub fn main(args: &[String]) -> u8 { } } } + +#[cfg(test)] +mod stream_tests { + use super::*; + struct Fails { flush_only: bool } + impl Write for Fails { + fn write(&mut self, bytes: &[u8]) -> std::io::Result { + if self.flush_only { Ok(bytes.len()) } else { Err(std::io::Error::other("write control")) } + } + fn flush(&mut self) -> std::io::Result<()> { + if self.flush_only { Err(std::io::Error::other("flush control")) } else { Ok(()) } + } + } + #[test] + fn added_verbs_propagate_write_and_flush_errors() { + let verbs = [fixture::cli, evidence::cli, renovate::cli]; + for verb in verbs { + for flush_only in [false, true] { + let mut bad = Fails { flush_only }; + let mut err = Vec::new(); + assert_eq!(run_with_writers(verb, &["--help".into()], &mut bad, &mut err), 1); + assert!(String::from_utf8(err).unwrap().contains("stdout:")); + } + } + } + #[test] + fn shared_runner_checks_stderr_and_an_empty_stdout() { + fn clean(_: &[String], _: &mut dyn Write, err: &mut dyn Write) -> u8 { + let _ = writeln!(err, "status"); + 0 + } + for flush_only in [false, true] { + assert_eq!(run_with_writers(clean, &[], &mut Vec::new(), &mut Fails { flush_only }), 1); + } + assert_eq!(run_with_writers(clean, &[], &mut Fails { flush_only: true }, &mut Vec::new()), 1); + } +} diff --git a/rust/crates/og-dev/src/renovate.rs b/rust/crates/og-dev/src/renovate.rs index c206ac819..74bbfb6a1 100644 --- a/rust/crates/og-dev/src/renovate.rs +++ b/rust/crates/og-dev/src/renovate.rs @@ -898,6 +898,25 @@ mod tests { let _ = fs::remove_dir_all(&d); } + #[test] + fn non_writable_summary_parent_preserves_previous_summary() { + use std::os::unix::fs::PermissionsExt; + let d = planted("summary-parent"); + let parent = d.join("summary-dir"); + fs::create_dir(&parent).unwrap(); + let summary = parent.join("result"); + fs::write(&summary, b"old").unwrap(); + fs::set_permissions(&parent, fs::Permissions::from_mode(0o555)).unwrap(); + let args = vec![d.join("renovate.log").to_str().unwrap().into(), "--repo".into(), d.to_str().unwrap().into()]; + let (mut out, mut err) = (Vec::new(), Vec::new()); + let code = cli_with_summary(&args, &mut out, &mut err, Some(&summary)); + fs::set_permissions(&parent, fs::Permissions::from_mode(0o755)).unwrap(); + assert_eq!(code, 1); + assert!(String::from_utf8(err).unwrap().contains(": parent directory is not writable")); + assert_eq!(fs::read(&summary).unwrap(), b"old"); + fs::remove_dir_all(d).unwrap(); + } + #[test] fn configured_summary_write_failure_fails_the_check() { let d = planted("summary"); From cc98f40409f4680f432803c6e508c4fc452dcb5a Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 19:33:20 +0100 Subject: [PATCH 41/45] fix(dev): check offsets and output streams before reporting success --- .github/workflows/guard.yml | 3 + CHANGELOG.md | 2 + docs/rust.md | 27 ++++- rust/crates/og-dev/src/evidence.rs | 180 +++++++++++++++++++++++------ rust/crates/og-dev/src/fixture.rs | 75 ++++++++++-- rust/crates/og-dev/src/lib.rs | 97 ++++++++++++++-- rust/crates/og-dev/src/renovate.rs | 71 ++++++++++-- 7 files changed, 390 insertions(+), 65 deletions(-) diff --git a/.github/workflows/guard.yml b/.github/workflows/guard.yml index 304360fbd..4cc06645e 100644 --- a/.github/workflows/guard.yml +++ b/.github/workflows/guard.yml @@ -282,6 +282,9 @@ jobs: - name: cargo test run: cargo test --workspace --locked --no-fail-fast + - name: Evidence offset regression in release mode + run: cargo test --release --locked -p og-dev --no-fail-fast fai_offsets_reject_overflow_and_out_of_file_before_publication + - name: pycompat against CPython (python3 -I in PYTHON_IMAGE, 100000 cases per operation) if: ${{ !cancelled() }} # runs even when a step above failed, so each result shows run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 90f203d7d..a7f197e0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Each release's notes. From 1.0.0 on, a major or minor release has hand-written n ## Unreleased +- CI evidence rejects FASTA offset overflow and offsets outside the file before reading or publishing. Guard tests this in both debug and release builds. Every new dev verb fails on stdout or stderr write or flush errors. A truth VCF header after the first data record fails without replacing the destination. Additional deliberate refusals are documented and tested: zero-byte gzip inputs, zero-width unused FAI rows, non-ASCII or out-of-range annotation limits, non-writable staging parents, and gzip FASTA files with long FNAME headers. Publication replaces a file and leaves other hard links on the old inode. + - CI evidence and Renovate inputs now enforce their original explicit gzip or plain format above strict UTF-8 and universal-newline handling. Renovate normalizes repository sources too and accepts falsy warnings and packages with no dependencies. Output publication follows destination symlinks, preserves existing permissions, rejects read-only files and directories, and uses the umask for new files. VNtyper uses private temporary directories with failure and signal cleanup, and GNU `mv -T` prevents publication inside a directory. Comphet stages and validates all destinations before publishing. Three renames are still not one transaction. Stricter parsing is deliberate and tested: BED refuses unsupported Unicode/control whitespace, Renovate refuses wrong-typed present values, PharmCAT refuses truthy non-list containers and non-object alleles, evidence positions must be ASCII u64 values, truth VCF rows need five columns, blank FAI rows fail, text records have a 16 MiB limit, and clash rows need a trailing VCF column after ALT. Truth VCF records get a final LF. `docs/rust.md` lists the error classes. - `og dev evidence` now rejects invalid UTF-8, truncated selected records, blank BED rows and missing panel headers. It reads LF, CRLF and CR line endings alike, with a 16 MiB limit per record. Single-file evidence and fixture outputs are published by rename after validation. Comphet stages and validates all three outputs before its first rename. Three renames are still not one transaction. A crash or a filesystem failure between renames can publish only part of the new set. `og dev renovate-check` rejects malformed log records and present fields of the wrong type, and fails if its configured step summary cannot be written. The PharmCAT trimmer consumes source genes as it trims them. The VNtyper downloader also accepts output paths that contain a newline. diff --git a/docs/rust.md b/docs/rust.md index 91928df39..4a276128f 100644 --- a/docs/rust.md +++ b/docs/rust.md @@ -132,11 +132,17 @@ Comphet stages all three files and checks all three destinations before the first rename. A bad later destination leaves previous outputs intact. Three renames are still not one transaction. A crash or a filesystem failure between renames can publish only part of the new set. Unhandled SIGKILL can -leave temporary files. Single-file outputs are replaced by one rename. +leave temporary files. Single-file outputs are replaced by one rename. Publication replaces the file +and does not update other hard links. Those links still refer to the old inode. + +The shared runner fails the command if stdout or stderr cannot be written or +flushed. This includes the evidence status and Renovate table. A configured +Renovate summary must also be written and flushed successfully. +Guard runs the FASTA offset regression in debug and release profiles. ### Stricter than the replaced Python, on purpose -Each refusal below has a pinned negative CLI test with exit 1 and a stable +Each refusal below has a pinned negative CLI test with exit 1 or 2 and a stable error class in `og-dev`. The final-LF normalization has a positive byte test. - BED fields use spaces and tabs as separators. Other Python whitespace @@ -154,3 +160,20 @@ error class in `og-dev`. The final-LF normalization has a positive byte test. - Clash VCF rows need a tab after ALT. A newline in the ALT field of a short row fails with `missing trailing VCF columns`. - A truth VCF record without its final LF gets one in the output. Valid VCF ends with LF. + +- Zero-byte gzip VCFs or selected gzip tables fail with `wrong compression (expected gzip)`. +- Every FAI row needs nonzero bases per line, including unused rows. + A zero width fails with `a row has 0 bases per line`. +- `MAX_ANNOTATIONS` must parse as a signed 64-bit ASCII integer. + Underscores, non-ASCII digits and out-of-range values fail with + `MAX_ANNOTATIONS ... is not a whole number` (exit 2). +- Staging needs a writable parent directory, even for an existing writable file. + A parent with no write bits fails with `parent directory is not writable`. + Other access failures report the operating system error. +- The FASTA reader accepts plain files only. A gzip magic prefix fails with + `wrong compression (expected plain)`, including gzip with a long FNAME. +- A VCF header after the first truth record fails with + `VCF header after first data record`. The previous destination stays intact. +- FASTA offsets use checked subtraction, division, multiplication, addition + and remainder. Overflow, a position beyond the file, or an offset outside + the signed file-offset range fails with `invalid FASTA offset`. diff --git a/rust/crates/og-dev/src/evidence.rs b/rust/crates/og-dev/src/evidence.rs index 547bf903b..a3e2f12db 100644 --- a/rust/crates/og-dev/src/evidence.rs +++ b/rust/crates/og-dev/src/evidence.rs @@ -123,13 +123,16 @@ fn input_lines( } let end = chunk.iter().position(|b| matches!(b, b'\r' | b'\n')); let n = end.unwrap_or(chunk.len()); - if line.len() + n > MAX_LINE { + if line.len().checked_add(n).is_none_or(|len| len > MAX_LINE) { return Err(format!("{path}: record exceeds {MAX_LINE} bytes")); } line.extend_from_slice(&chunk[..n]); if end.is_some() { after_cr = chunk[n] == b'\r'; - r.consume(n + 1); + r.consume( + n.checked_add(1) + .ok_or_else(|| format!("{path}: record size overflows"))?, + ); std::str::from_utf8(&line).map_err(|e| format!("{path}: invalid UTF-8: {e}"))?; f(&line)?; line.clear(); @@ -206,6 +209,17 @@ impl PendingOutput { static NEXT: AtomicU64 = AtomicU64::new(0); let destination = resolve_destination(destination)?; let permissions = validate_destination(&destination)?; + let parent = destination + .parent() + .filter(|p| !p.as_os_str().is_empty()) + .unwrap_or(Path::new(".")); + let meta = fs::metadata(parent).map_err(|e| format!("{}: {e}", parent.display()))?; + if meta.permissions().mode() & 0o222 == 0 { + return Err(format!( + "{}: parent directory is not writable", + destination.display() + )); + } let name = destination.file_name().ok_or("output has no file name")?; loop { let mut tmp = std::ffi::OsString::from("."); @@ -280,7 +294,8 @@ fn clash_positions( chroms: &BTreeSet>, ) -> Result, u64)>, String> { let mut hit = BTreeSet::new(); - let (mut key, mut n, mut snvs): (Option<(Vec, u64)>, u32, u32) = (None, 0, 0); + let (mut key, mut seen, mut multiple, mut snv): (Option<(Vec, u64)>, bool, bool, bool) = + (None, false, false, false); input_lines(path, true, |line| { if line.first() == Some(&b'#') { return Ok(()); @@ -297,12 +312,13 @@ fn clash_positions( let k = (c[0].to_vec(), pos(c[1], path)?); if key.as_ref() != Some(&k) { key = Some(k.clone()); - (n, snvs) = (0, 0); + (seen, multiple, snv) = (false, false, false); } - n += 1; + multiple |= seen; + seen = true; let one = |a: &[u8]| std::str::from_utf8(a).map_or(a.len(), |s| s.chars().count()) == 1; - snvs += u32::from(one(c[3]) && one(c[4])); - if n >= 2 && snvs >= 1 { + snv |= one(c[3]) && one(c[4]); + if multiple && snv { hit.insert(k); } Ok(()) @@ -355,7 +371,12 @@ fn clashes(raw: &str, split: &str, data: &str, out: &str) -> Result<(), String> } let p = pos(c[1], &path)?; for (k, w) in want.iter().enumerate() { - on_table[k] += u64::from(w.contains(&p)); + let count = on_table + .get_mut(k) + .ok_or("table counter index out of range")?; + *count = count + .checked_add(u64::from(w.contains(&p))) + .ok_or_else(|| format!("{path}: table count overflows"))?; } Ok(()) })?; @@ -365,7 +386,7 @@ fn clashes(raw: &str, split: &str, data: &str, out: &str) -> Result<(), String> text += &format!( "{name}_positions\t{}\n{name}_on_table\t{}\n", s.len(), - on_table[k] + on_table.get(k).ok_or("table counter index out of range")? ); } publish(Path::new(out), text.as_bytes()) @@ -419,7 +440,9 @@ fn merge(mut spans: Vec<(u64, u64)>) -> Vec<(u64, u64)> { /// Is `p` inside one of the sorted, disjoint spans? fn inside(spans: &[(u64, u64)], p: u64) -> bool { let i = spans.partition_point(|&(s, _)| s <= p); - i > 0 && p < spans[i - 1].1 + i.checked_sub(1) + .and_then(|j| spans.get(j)) + .is_some_and(|&(_, end)| p < end) } fn hg002_gvcf( @@ -431,6 +454,10 @@ fn hg002_gvcf( ) -> Result<(u64, u64), String> { let fai = read_fai(&format!("{reference}.fai"))?; let fasta = File::open(reference).map_err(|e| format!("{reference}: {e}"))?; + let fasta_len = fasta + .metadata() + .map_err(|e| format!("{reference}: {e}"))? + .len(); let mut magic = [0; 2]; if fasta .read_at(&mut magic, 0) @@ -471,7 +498,9 @@ fn hg002_gvcf( let confident = |c: &[u8], p: u64| { conf.get(c).is_some_and(|v| { let i = v.partition_point(|&(s, _)| s < p); - i > 0 && v[i - 1].1 >= p + i.checked_sub(1) + .and_then(|j| v.get(j)) + .is_some_and(|&(_, end)| end >= p) }) }; @@ -481,6 +510,9 @@ fn hg002_gvcf( let mut covered: HashMap, Vec<(u64, u64)>> = HashMap::new(); let mut records = 0u64; input_lines(truth, true, |line| { + if line.first() == Some(&b'#') && records != 0 { + return Err(format!("{truth}: VCF header after first data record")); + } if line.starts_with(b"##") { } else if line.first() == Some(&b'#') { w.write_all(b"##INFO=\n##ALT=\n") @@ -497,11 +529,15 @@ fn hg002_gvcf( std::str::from_utf8(f[3]) .expect("strict reader") .chars() - .count() as u64, + .count() + .try_into() + .map_err(|_| format!("{truth}: allele length overflows"))?, ) .ok_or_else(|| format!("{truth}: position span overflows"))?, )); - records += 1; + records = records + .checked_add(1) + .ok_or_else(|| format!("{truth}: record count overflows"))?; } w.write_all(line) .and_then(|()| w.write_all(b"\n")) @@ -527,8 +563,20 @@ fn hg002_gvcf( { return Ok(()); } - let at = - row.offset + ((p - 1) / row.line_bases) * row.line_bytes + (p - 1) % row.line_bases; + let invalid = || format!("{reference}: invalid FASTA offset: overflow or outside file"); + let zero = p.checked_sub(1).ok_or_else(invalid)?; + let at = zero + .checked_div(row.line_bases) + .and_then(|line| line.checked_mul(row.line_bytes)) + .and_then(|relative| row.offset.checked_add(relative)) + .and_then(|start| { + zero.checked_rem(row.line_bases) + .and_then(|col| start.checked_add(col)) + }) + .ok_or_else(invalid)?; + if at > i64::MAX as u64 || at.checked_add(1).is_none_or(|end| end > fasta_len) { + return Err(invalid()); + } fasta .read_exact_at(&mut base, at) .map_err(|e| format!("{reference}: {e}"))?; @@ -537,9 +585,17 @@ fn hg002_gvcf( if !b"ACGT".contains(&b) { return Ok(()); } - blocks += 1; + blocks = blocks + .checked_add(1) + .ok_or_else(|| format!("{sites}: block count overflows"))?; w.write_all(c) - .and_then(|()| writeln!(w, "\t{p}\t.\t{}\t<*>\t.\t.\tEND={p}\tGT\t0/0", b as char)) + .and_then(|()| { + writeln!( + w, + "\t{p}\t.\t{}\t<*>\t.\t.\tEND={p}\tGT\t0/0", + char::from(b) + ) + }) .map_err(werr) })?; if first { @@ -644,9 +700,9 @@ mod tests { fs::write(self.dir.join(name), bytes).unwrap(); } } - fn run(&self) -> Result<(), String> { + fn args(&self) -> Vec { let p = |n: &str| self.dir.join(n).to_str().unwrap().to_owned(); - let args = if self.kind == "clashes" { + if self.kind == "clashes" { vec![ "clashes".to_owned(), p("raw.vcf.gz"), @@ -663,9 +719,11 @@ mod tests { p("sites.tsv"), p("out"), ] - }; + } + } + fn run(&self) -> Result<(), String> { let (mut out, mut err) = (Vec::new(), Vec::new()); - match cli(&args, &mut out, &mut err) { + match cli(&self.args(), &mut out, &mut err) { 0 => Ok(()), 1 => Err(String::from_utf8(err).unwrap()), code => panic!("unexpected CLI exit {code}: {err:?}"), @@ -696,19 +754,37 @@ mod tests { } } + #[test] + fn evidence_status_uses_checked_runner() { + for kind in ["clashes", "hg002-gvcf"] { + let c = Case::new(kind, "streams"); + crate::stream_tests::assert_status_errors(cli, &c.args(), kind == "hg002-gvcf"); + } + } + #[test] fn fai_offsets_reject_overflow_and_out_of_file_before_publication() { for (offset, bases, bytes, position) in [ (18446744073709551598u64, 1, 2, 13), (1, 1, u64::MAX, 3), (u64::MAX, 1, 1, 1), + (u64::MAX - 1, 50, 51, 3), (1000, 50, 51, 1), ] { let c = Case::new("hg002-gvcf", &format!("offset-{offset}-{position}")); - c.put("ref.fasta.fai", format!("chrA\t50\t{offset}\t{bases}\t{bytes}\n").as_bytes()); - c.put("sites.tsv", format!("id\tchr\tpos\nx\tchrA\t{position}\n").as_bytes()); + c.put( + "ref.fasta.fai", + format!("chrA\t50\t{offset}\t{bases}\t{bytes}\n").as_bytes(), + ); + c.put( + "sites.tsv", + format!("id\tchr\tpos\nx\tchrA\t{position}\n").as_bytes(), + ); c.put("truth.bed", b"chrA\t0\t50\n"); - c.put("truth.vcf.gz", b"##fileformat=VCFv4.2\n#CHROM\tPOS\tID\tREF\tALT\n"); + c.put( + "truth.vcf.gz", + b"##fileformat=VCFv4.2\n#CHROM\tPOS\tID\tREF\tALT\n", + ); c.fails_without_publication(); assert!(c.run().unwrap_err().contains(": invalid FASTA offset:")); } @@ -718,38 +794,63 @@ mod tests { fn late_vcf_headers_fail_before_publication() { for header in [b"##late=yes\n".as_slice(), b"#CHROM\tPOS\n"] { let c = Case::new("hg002-gvcf", &format!("late-{}", header.len())); - c.put("truth.vcf.gz", &[c.text("truth.vcf.gz"), header.to_vec()].concat()); + c.put( + "truth.vcf.gz", + &[c.text("truth.vcf.gz"), header.to_vec()].concat(), + ); c.fails_without_publication(); - assert!(c.run().unwrap_err().contains(": VCF header after first data record")); + assert!( + c.run() + .unwrap_err() + .contains(": VCF header after first data record") + ); } } #[test] fn empty_gzip_inputs_and_unused_zero_width_fai_rows_fail() { for (kind, name) in [ - ("clashes", "raw.vcf.gz"), ("clashes", "split.vcf.gz"), - ("clashes", "chrA.tsv.gz"), ("hg002-gvcf", "truth.vcf.gz"), + ("clashes", "raw.vcf.gz"), + ("clashes", "split.vcf.gz"), + ("clashes", "chrA.tsv.gz"), + ("hg002-gvcf", "truth.vcf.gz"), ] { let c = Case::new(kind, name); fs::write(c.dir.join(name), b"").unwrap(); c.fails_without_publication(); - assert!(c.run().unwrap_err().contains(": wrong compression (expected gzip)")); + assert!( + c.run() + .unwrap_err() + .contains(": wrong compression (expected gzip)") + ); } let c = Case::new("hg002-gvcf", "unused-row"); - c.put("ref.fasta.fai", &[c.text("ref.fasta.fai"), b"chrUnused\t0\t0\t0\t0\n".to_vec()].concat()); + c.put( + "ref.fasta.fai", + &[c.text("ref.fasta.fai"), b"chrUnused\t0\t0\t0\t0\n".to_vec()].concat(), + ); c.fails_without_publication(); - assert!(c.run().unwrap_err().contains(": a row has 0 bases per line")); + assert!( + c.run() + .unwrap_err() + .contains(": a row has 0 bases per line") + ); } #[test] fn gzip_fasta_with_ascii_fname_is_refused_by_format_guard() { let c = Case::new("hg002-gvcf", "fname"); - let mut gz = flate2::GzBuilder::new().filename(vec![b'A'; 200]) + let mut gz = flate2::GzBuilder::new() + .filename(vec![b'A'; 200]) .write(Vec::new(), flate2::Compression::default()); gz.write_all(&c.text("ref.fasta")).unwrap(); fs::write(c.dir.join("ref.fasta"), gz.finish().unwrap()).unwrap(); c.fails_without_publication(); - assert!(c.run().unwrap_err().contains(": wrong compression (expected plain)")); + assert!( + c.run() + .unwrap_err() + .contains(": wrong compression (expected plain)") + ); } #[test] @@ -760,7 +861,11 @@ mod tests { fs::set_permissions(&c.dir, fs::Permissions::from_mode(0o555)).unwrap(); let result = c.run(); fs::set_permissions(&c.dir, fs::Permissions::from_mode(0o755)).unwrap(); - assert!(result.unwrap_err().contains(": parent directory is not writable")); + assert!( + result + .unwrap_err() + .contains(": parent directory is not writable") + ); assert_eq!(fs::read(c.dir.join("out")).unwrap(), b"PREVIOUS OUTPUT\n"); } } @@ -906,7 +1011,12 @@ mod tests { }; fs::write(c.dir.join(name), wrong).unwrap(); c.fails_without_publication(); - assert!(c.run().unwrap_err().contains(": wrong compression (expected "), "{name}"); + assert!( + c.run() + .unwrap_err() + .contains(": wrong compression (expected "), + "{name}" + ); } } } diff --git a/rust/crates/og-dev/src/fixture.rs b/rust/crates/og-dev/src/fixture.rs index 3371b7a91..ec74f8dd3 100644 --- a/rust/crates/og-dev/src/fixture.rs +++ b/rust/crates/og-dev/src/fixture.rs @@ -461,10 +461,19 @@ fn diplotype(x: &Value) -> Result { } /// `items[:n]` with Python's slice rule for a negative `n`. +#[allow( + clippy::manual_saturating_arithmetic, + reason = "Python slices clamp a checked negative end at zero" +)] fn head(items: &[Value], n: i64) -> &[Value] { - let len = items.len() as i64; - let end = if n < 0 { (len + n).max(0) } else { n.min(len) }; - &items[..end as usize] + // Clamp before slicing, including i64::MIN, without narrowing lengths. + let count = usize::try_from(n.unsigned_abs()).unwrap_or(usize::MAX); + let end = if n < 0 { + items.len().checked_sub(count).unwrap_or_default() + } else { + count.min(items.len()) + }; + &items[..end] } /// The trimmed report: every gene with its listed diplotypes and related @@ -608,6 +617,41 @@ fn trim_pharmcat(src: &Path, dst: &Path, max: i64) -> Result<(), String> { mod tests { use super::*; + #[test] + fn fixture_verbs_use_checked_runner_even_with_empty_status() { + let d = std::env::temp_dir().join(format!("og-fixture-streams-{}", std::process::id())); + fs::create_dir_all(&d).unwrap(); + fs::write(d.join("in"), br#"{"genes":{},"drugs":{}}"#).unwrap(); + for args in [ + vec![ + "trim-pharmcat".into(), + d.join("in").to_str().unwrap().into(), + d.join("out").to_str().unwrap().into(), + ], + vec!["comphet".into(), d.to_str().unwrap().into()], + ] { + crate::stream_tests::assert_status_errors(cli, &args, false); + } + fs::remove_dir_all(d).unwrap(); + } + + #[test] + fn annotation_slices_clamp_extreme_counts_without_narrowing() { + let items = vec![Value::Null; 3]; + for (n, expected) in [ + (i64::MIN, 0), + (-4, 0), + (-3, 0), + (-2, 1), + (-1, 2), + (0, 0), + (1, 1), + (i64::MAX, 3), + ] { + assert_eq!(head(&items, n).len(), expected); + } + } + #[test] fn non_ascii_or_out_of_range_annotation_limits_fail_at_cli() { let d = std::env::temp_dir().join(format!("og-limits-{}", std::process::id())); @@ -615,12 +659,19 @@ mod tests { fs::write(d.join("in"), br#"{"genes":{},"drugs":{}}"#).unwrap(); for limit in ["1_0", "٤", "18446744073709551616"] { fs::write(d.join("out"), b"old").unwrap(); - let args = vec!["trim-pharmcat".into(), d.join("in").to_str().unwrap().into(), - d.join("out").to_str().unwrap().into(), limit.into()]; + let args = vec![ + "trim-pharmcat".into(), + d.join("in").to_str().unwrap().into(), + d.join("out").to_str().unwrap().into(), + limit.into(), + ]; let (mut out, mut err) = (Vec::new(), Vec::new()); assert_eq!(cli(&args, &mut out, &mut err), 2); let err = String::from_utf8(err).unwrap(); - assert!(err.contains("error: MAX_ANNOTATIONS") && err.contains("is not a whole number"), "{err}"); + assert!( + err.contains("error: MAX_ANNOTATIONS") && err.contains("is not a whole number"), + "{err}" + ); assert_eq!(fs::read(d.join("out")).unwrap(), b"old"); } fs::remove_dir_all(d).unwrap(); @@ -636,7 +687,11 @@ mod tests { fs::write(d.join(name), b"old").unwrap(); } for args in [ - vec!["trim-pharmcat".into(), d.join("in").to_str().unwrap().into(), d.join("out").to_str().unwrap().into()], + vec![ + "trim-pharmcat".into(), + d.join("in").to_str().unwrap().into(), + d.join("out").to_str().unwrap().into(), + ], vec!["comphet".into(), d.to_str().unwrap().into()], ] { fs::set_permissions(&d, fs::Permissions::from_mode(0o555)).unwrap(); @@ -644,7 +699,11 @@ mod tests { let code = cli(&args, &mut out, &mut err); fs::set_permissions(&d, fs::Permissions::from_mode(0o755)).unwrap(); assert_eq!(code, 1); - assert!(String::from_utf8(err).unwrap().contains(": parent directory is not writable")); + assert!( + String::from_utf8(err) + .unwrap() + .contains(": parent directory is not writable") + ); for name in ["out", "reads.sam", "vep.vcf", "comphet.vcf"] { assert_eq!(fs::read(d.join(name)).unwrap(), b"old"); } diff --git a/rust/crates/og-dev/src/lib.rs b/rust/crates/og-dev/src/lib.rs index 14eb34dfb..1f8c44674 100644 --- a/rust/crates/og-dev/src/lib.rs +++ b/rust/crates/og-dev/src/lib.rs @@ -66,11 +66,54 @@ fn run(f: fn(&[String], &mut dyn Write, &mut dyn Write) -> u8, args: &[String]) pub(crate) fn run_with_writers( f: fn(&[String], &mut dyn Write, &mut dyn Write) -> u8, - args: &[String], out: &mut dyn Write, err: &mut dyn Write, + args: &[String], + out: &mut dyn Write, + err: &mut dyn Write, ) -> u8 { - let code = f(args, out, err); + let mut out = CheckedWriter { + inner: out, + failure: None, + }; + let mut err = CheckedWriter { + inner: err, + failure: None, + }; + let code = f(args, &mut out, &mut err); let _ = out.flush(); - code + if let Some(reason) = &out.failure { + let _ = writeln!(err, "og dev: stdout: {reason}"); + } + let _ = err.flush(); + if code == 0 && (out.failure.is_some() || err.failure.is_some()) { + 1 + } else { + code + } +} + +/// Remember errors even when an older verb ignores its Write result. +struct CheckedWriter<'a> { + inner: &'a mut dyn Write, + failure: Option, +} +impl Write for CheckedWriter<'_> { + fn write(&mut self, bytes: &[u8]) -> std::io::Result { + let result = match self.inner.write(bytes) { + Ok(0) if !bytes.is_empty() => Err(std::io::Error::from(std::io::ErrorKind::WriteZero)), + result => result, + }; + if let Err(e) = &result { + self.failure.get_or_insert_with(|| e.to_string()); + } + result + } + fn flush(&mut self) -> std::io::Result<()> { + let result = self.inner.flush(); + if let Err(e) = &result { + self.failure.get_or_insert_with(|| e.to_string()); + } + result + } } /// Runs `og dev ARGS`; returns the exit code. @@ -106,15 +149,42 @@ pub fn main(args: &[String]) -> u8 { #[cfg(test)] mod stream_tests { use super::*; - struct Fails { flush_only: bool } + struct Fails { + flush_only: bool, + } impl Write for Fails { fn write(&mut self, bytes: &[u8]) -> std::io::Result { - if self.flush_only { Ok(bytes.len()) } else { Err(std::io::Error::other("write control")) } + if self.flush_only { + Ok(bytes.len()) + } else { + Err(std::io::Error::other("write control")) + } } fn flush(&mut self) -> std::io::Result<()> { - if self.flush_only { Err(std::io::Error::other("flush control")) } else { Ok(()) } + if self.flush_only { + Err(std::io::Error::other("flush control")) + } else { + Ok(()) + } } } + pub(crate) fn assert_status_errors( + verb: fn(&[String], &mut dyn Write, &mut dyn Write) -> u8, + args: &[String], + writes: bool, + ) { + for flush_only in [false, true] { + let mut bad = Fails { flush_only }; + let mut err = Vec::new(); + assert_eq!( + run_with_writers(verb, args, &mut bad, &mut err), + u8::from(writes || flush_only), + "{}", + String::from_utf8_lossy(&err) + ); + } + } + #[test] fn added_verbs_propagate_write_and_flush_errors() { let verbs = [fixture::cli, evidence::cli, renovate::cli]; @@ -122,7 +192,10 @@ mod stream_tests { for flush_only in [false, true] { let mut bad = Fails { flush_only }; let mut err = Vec::new(); - assert_eq!(run_with_writers(verb, &["--help".into()], &mut bad, &mut err), 1); + assert_eq!( + run_with_writers(verb, &["--help".into()], &mut bad, &mut err), + 1 + ); assert!(String::from_utf8(err).unwrap().contains("stdout:")); } } @@ -134,8 +207,14 @@ mod stream_tests { 0 } for flush_only in [false, true] { - assert_eq!(run_with_writers(clean, &[], &mut Vec::new(), &mut Fails { flush_only }), 1); + assert_eq!( + run_with_writers(clean, &[], &mut Vec::new(), &mut Fails { flush_only }), + 1 + ); } - assert_eq!(run_with_writers(clean, &[], &mut Fails { flush_only: true }, &mut Vec::new()), 1); + assert_eq!( + run_with_writers(clean, &[], &mut Fails { flush_only: true }, &mut Vec::new()), + 1 + ); } } diff --git a/rust/crates/og-dev/src/renovate.rs b/rust/crates/og-dev/src/renovate.rs index 74bbfb6a1..cafad1342 100644 --- a/rust/crates/og-dev/src/renovate.rs +++ b/rust/crates/og-dev/src/renovate.rs @@ -233,7 +233,8 @@ fn read_pins(repo: &Path, path: &Path) -> Result { if line.is_empty() || line.starts_with('#') { continue; } - let at = |msg: &str| format!("{}:{}: {msg}", path.display(), i + 1); + let number = i.checked_add(1).ok_or("pin line number overflows")?; + let at = |msg: &str| format!("{}:{number}: {msg}", path.display()); let cols: Vec<&str> = line.split('\t').collect(); let [dep, files, pattern] = cols[..] else { return Err(at( @@ -251,7 +252,7 @@ fn read_pins(repo: &Path, path: &Path) -> Result { } let n = re.find_iter(&texts[&f]).count(); if n > 0 { - *pins.want.entry((dep.to_owned(), f.clone())).or_default() += n; + add_count(pins.want.entry((dep.to_owned(), f.clone())).or_default(), n)?; } pins.files.entry(dep.to_owned()).or_default().insert(f); } @@ -443,12 +444,21 @@ fn updates(d: &IndexMap) -> &[Value] { } } -fn counter<'a>(it: impl Iterator>) -> BTreeMap { +fn add_count(count: &mut usize, n: usize) -> Result<(), String> { + *count = count + .checked_add(n) + .ok_or("arithmetic: dependency count overflows")?; + Ok(()) +} + +fn counter<'a>( + it: impl Iterator>, +) -> Result, String> { let mut c = BTreeMap::new(); for d in it { - *c.entry(key(d)).or_default() += 1; + add_count(c.entry(key(d)).or_default(), 1)?; } - c + Ok(c) } /// The docker dependencies of one file. @@ -461,6 +471,10 @@ fn in_file<'a>( /// Every problem, as `check: ...` lines. fn check(deps: &[Dep], env_text: &str, pins: &Pins) -> Vec { + check_counts(deps, env_text, pins).unwrap_or_else(|e| vec![e]) +} + +fn check_counts(deps: &[Dep], env_text: &str, pins: &Pins) -> Result, String> { let mut fails = Vec::new(); let docker: Vec<&Dep> = deps.iter().filter(|(_, d)| is_docker(d)).collect(); let want = env_images(env_text); @@ -491,9 +505,9 @@ fn check(deps: &[Dep], env_text: &str, pins: &Pins) -> Vec { fails.push(format!("names: {f}: package name {}", r(name))); } } - let env_keys = counter(in_file(&docker, ENV)); + let env_keys = counter(in_file(&docker, ENV))?; for f in TWINS { - let keys = counter(in_file(&docker, f)); + let keys = counter(in_file(&docker, f))?; if f == DOCS && keys != env_keys { let all: BTreeSet<&String> = keys.keys().chain(env_keys.keys()).collect(); for k in all { @@ -562,7 +576,7 @@ fn check(deps: &[Dep], env_text: &str, pins: &Pins) -> Vec { if let Value::Str(name) = get(d, "depName") && pins.files.get(name).is_some_and(|fs| fs.contains(f)) { - *got.entry((name.clone(), f.clone())).or_default() += 1; + add_count(got.entry((name.clone(), f.clone())).or_default(), 1)?; } } let all: BTreeSet<&(String, String)> = pins.want.keys().chain(got.keys()).collect(); @@ -578,7 +592,7 @@ fn check(deps: &[Dep], env_text: &str, pins: &Pins) -> Vec { )); } } - fails + Ok(fails) } /// Plants one fault of each kind in a copy of the dependencies and shows @@ -898,6 +912,33 @@ mod tests { let _ = fs::remove_dir_all(&d); } + #[test] + fn renovate_table_uses_checked_runner() { + let d = planted("status"); + let args = vec![ + d.join("renovate.log").to_str().unwrap().into(), + "--repo".into(), + d.to_str().unwrap().into(), + ]; + // No dependence on the process's GITHUB_STEP_SUMMARY environment. + fn verb(args: &[String], out: &mut dyn Write, err: &mut dyn Write) -> u8 { + cli_with_summary(args, out, err, None) + } + crate::stream_tests::assert_status_errors(verb, &args, true); + fs::remove_dir_all(d).unwrap(); + } + + #[test] + fn dependency_counts_refuse_overflow() { + let mut count = usize::MAX; + assert!( + add_count(&mut count, 1) + .unwrap_err() + .contains("dependency count overflows") + ); + assert_eq!(count, usize::MAX); + } + #[test] fn non_writable_summary_parent_preserves_previous_summary() { use std::os::unix::fs::PermissionsExt; @@ -907,12 +948,20 @@ mod tests { let summary = parent.join("result"); fs::write(&summary, b"old").unwrap(); fs::set_permissions(&parent, fs::Permissions::from_mode(0o555)).unwrap(); - let args = vec![d.join("renovate.log").to_str().unwrap().into(), "--repo".into(), d.to_str().unwrap().into()]; + let args = vec![ + d.join("renovate.log").to_str().unwrap().into(), + "--repo".into(), + d.to_str().unwrap().into(), + ]; let (mut out, mut err) = (Vec::new(), Vec::new()); let code = cli_with_summary(&args, &mut out, &mut err, Some(&summary)); fs::set_permissions(&parent, fs::Permissions::from_mode(0o755)).unwrap(); assert_eq!(code, 1); - assert!(String::from_utf8(err).unwrap().contains(": parent directory is not writable")); + assert!( + String::from_utf8(err) + .unwrap() + .contains(": parent directory is not writable") + ); assert_eq!(fs::read(&summary).unwrap(), b"old"); fs::remove_dir_all(d).unwrap(); } From 8dc168d42d030397c5dfca9efd289478bdbd886c Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 19:37:04 +0100 Subject: [PATCH 42/45] fix(dev): expose descriptor errors hidden by standard stdout --- rust/crates/og-dev/src/lib.rs | 50 +++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/rust/crates/og-dev/src/lib.rs b/rust/crates/og-dev/src/lib.rs index 1f8c44674..02b5d480e 100644 --- a/rust/crates/og-dev/src/lib.rs +++ b/rust/crates/og-dev/src/lib.rs @@ -59,8 +59,21 @@ pub(crate) fn py_os_error(e: &std::io::Error, path: &str) -> String { /// Runs a verb that writes to stdout and stderr, flushing both. fn run(f: fn(&[String], &mut dyn Write, &mut dyn Write) -> u8, args: &[String]) -> u8 { - let mut out = std::io::stdout().lock(); - let mut err = std::io::stderr().lock(); + use std::os::fd::AsFd; + // Stdout/StdoutLock suppress EBADF on Unix. Duplicating the descriptor + // preserves its access mode and exposes write errors without unsafe code. + let out = match std::io::stdout().as_fd().try_clone_to_owned() { + Ok(fd) => std::fs::File::from(fd), + Err(e) => { + let _ = writeln!(std::io::stderr().lock(), "og dev: stdout: {e}"); + return 1; + } + }; + let mut err = match std::io::stderr().as_fd().try_clone_to_owned() { + Ok(fd) => std::fs::File::from(fd), + Err(_) => return 1, + }; + let mut out = std::io::BufWriter::new(out); run_with_writers(f, args, &mut out, &mut err) } @@ -168,6 +181,39 @@ mod stream_tests { } } } + #[test] + fn stdio_process_control() { + if let Ok(verb) = std::env::var("OG_DEV_STREAM_CONTROL") { + std::process::exit(i32::from(main(&[verb, "--help".into()]))); + } + } + + #[test] + fn read_only_stdout_descriptor_makes_added_verbs_fail() { + let path = std::env::temp_dir().join(format!("og-stream-fd-{}", std::process::id())); + std::fs::write(&path, b"previous bytes").unwrap(); + for verb in ["fixture", "evidence", "renovate-check"] { + let result = std::process::Command::new(std::env::current_exe().unwrap()) + .args([ + "--exact", + "stream_tests::stdio_process_control", + "--nocapture", + ]) + .env("OG_DEV_STREAM_CONTROL", verb) + .stdout(std::fs::File::open(&path).unwrap()) + .output() + .unwrap(); + assert!(!result.status.success(), "{verb} silently lost stdout"); + assert!( + String::from_utf8_lossy(&result.stderr).contains("og dev: stdout:"), + "{verb}: {:?}", + result.stderr + ); + assert_eq!(std::fs::read(&path).unwrap(), b"previous bytes"); + } + std::fs::remove_file(path).unwrap(); + } + pub(crate) fn assert_status_errors( verb: fn(&[String], &mut dyn Write, &mut dyn Write) -> u8, args: &[String], From ce558d084b7802e85d888627ac53070d0a0f5be5 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 20:25:17 +0100 Subject: [PATCH 43/45] test(dev): reproduce skipped JSON limits and pin refusal preservation --- rust/crates/og-dev/src/renovate.rs | 227 +++++++++++++++++++++++++++++ 1 file changed, 227 insertions(+) diff --git a/rust/crates/og-dev/src/renovate.rs b/rust/crates/og-dev/src/renovate.rs index cafad1342..e787e50ab 100644 --- a/rust/crates/og-dev/src/renovate.rs +++ b/rust/crates/og-dev/src/renovate.rs @@ -855,6 +855,233 @@ mod tests { let _ = fs::remove_dir_all(&d); } + #[test] + fn malformed_json_lines_are_skipped_before_and_after_the_config() { + let d = planted("syntax-errors"); + let log = fs::read(d.join("renovate.log")).unwrap(); + let bad = b"{broken}\n{\"x\":}\n\"\\uZZZZ\"\n"; + fs::write(d.join("renovate.log"), [bad.as_slice(), &log, bad].concat()).unwrap(); + let (code, out) = run(&d); + assert_eq!(code, 0, "{out}"); + assert!(out.contains("OK: 7 dependencies; every check passed")); + fs::remove_dir_all(d).unwrap(); + } + + #[test] + fn json_reader_limits_never_fall_back_to_an_earlier_config() { + let huge = "340282366920938463463374607431768211456"; + let deep = format!("{}{}", "[".repeat(json::MAX_DEPTH + 1), "]".repeat(json::MAX_DEPTH + 1)); + let records = [ + ("integer-prefix", format!("{huge}\n"), false, "Integer beyond 128 bits"), + ("surrogate-prefix", "\"\\ud800\"\n".into(), false, "Lone surrogate"), + ("final-config", format!("{{\"msg\":\"packageFiles with updates\",\"config\":{{\"regex\":[{{\"packageFile\":\"unused\",\"deps\":null}}]}},\"extra\":{huge}}}\n"), true, "Integer beyond 128 bits"), + ("nesting-prefix", format!("{deep}\n"), false, "nested deeper than 1000 levels"), + ]; + std::thread::Builder::new().stack_size(8 << 20).spawn(move || { + let mut misses = Vec::new(); + for (label, record, append, diagnostic) in records { + let d = planted(label); + let old = fs::read_to_string(d.join("renovate.log")).unwrap(); + let log = if append { old + &record } else { record + &old }; + fs::write(d.join("renovate.log"), log).unwrap(); + let (code, out) = run(&d); + if code != 1 || !out.contains(diagnostic) || out.contains("every check passed") { + misses.push(format!("{label}: exit {code}, {out}")); + } + fs::remove_dir_all(d).unwrap(); + } + assert!(misses.is_empty(), "{}", misses.join("\n")); + }).unwrap().join().unwrap(); + } + + /// The refusal contract applies to every row, including unused input fields. + /// Run each CLI with both a regular output and a symlink to that output. + #[test] + fn refusals_preserve_bytes_modes_and_link_targets() { + // Match the main-thread stack used by og for the nesting-limit control. + std::thread::Builder::new().stack_size(8 << 20).spawn(refusal_table).unwrap().join().unwrap(); + } + + fn refusal_table() { + use std::os::unix::fs::{PermissionsExt, symlink}; + #[derive(Clone, Copy, Debug)] + enum Verb { Gvcf, Clashes, Trim, Renovate, Comphet } + enum Fault { + Replace(&'static str, Vec), + Append(&'static str, Vec), + Prefix(Vec), + Limit(&'static str), + LongName, + ReadOnly, + ParentReadOnly, + Directory, + } + use Fault::*; + use Verb::*; + let bytes = |s: &str| s.as_bytes().to_vec(); + let huge = "340282366920938463463374607431768211456"; + let deep = format!("{}{}", "[".repeat(json::MAX_DEPTH + 1), "]".repeat(json::MAX_DEPTH + 1)); + let final_record = format!(r#"{{"msg":"packageFiles with updates","config":{{"regex":[{{"packageFile":"unused","deps":null}}]}},"extra":{huge}}}"#); + // Add a refusal by adding one row: CLI, input fault, diagnostic. + let mut cases = vec![ + (Renovate, Prefix(bytes(&format!("{huge}\n"))), "Integer beyond 128 bits"), + (Renovate, Prefix(bytes("\"\\ud800\"\n")), "Lone surrogate"), + (Renovate, Append("renovate.log", bytes(&format!("{final_record}\n"))), "Integer beyond 128 bits"), + (Renovate, Prefix(bytes(&format!("{deep}\n"))), "nested deeper than 1000 levels"), + (Renovate, Prefix(bytes("42\n")), "a log record is not an object"), + (Renovate, Prefix(vec![0xff, b'\n']), "not UTF-8"), + (Renovate, Prefix(vec![b'x'; (16 << 20) + 1]), "record exceeds 16777216 bytes"), + (Renovate, Replace(PINS, bytes("dep\tdocs/*.absent\tx\n")), "matches no file"), + (Renovate, Replace(PINS, bytes("dep\tabsent\tx\n")), "is not a file"), + (Gvcf, Append("ref.fasta.fai", bytes("chrUnused\t1\n")), "a row has fewer than 5 columns"), + (Gvcf, Replace("truth.vcf.gz", bytes("chrA\t18446744073709551615\t.\tA\tC\n")), "position span overflows"), + (Gvcf, Replace("ref.fasta.fai", bytes("\n")), "a row has fewer than 5 columns"), + (Gvcf, Append("ref.fasta.fai", bytes("chrUnused\t0\t0\t0\t0\n")), "a row has 0 bases per line"), + (Gvcf, Replace("truth.vcf.gz", bytes("chrA\t3\t.\tA\n")), "a record has fewer than 5 columns"), + (Gvcf, Replace("truth.bed", bytes("\n")), "a row has fewer than 3 columns"), + (Gvcf, Replace("truth.bed", bytes("chrA\u{85}0\u{85}50\n")), "unsupported whitespace"), + (Gvcf, Replace("sites.tsv", bytes("")), "missing header"), + (Gvcf, Append("truth.vcf.gz", bytes("##late=yes\n")), "VCF header after first data record"), + (Clashes, Replace("raw.vcf.gz", bytes("chrA\t3\t.\tA\tC\n")), "missing trailing VCF columns"), + (Trim, Replace("report.json", bytes(&format!(r#"{{"genes":{{}},"drugs":{{}},"title":{huge}}}"#))), "Integer beyond 128 bits"), + (Trim, Replace("report.json", bytes(r#"{"genes":{},"drugs":{},"title":"\ud800"}"#)), "Lone surrogate"), + (Trim, Replace("report.json", bytes(&deep)), "nested deeper than 1000 levels"), + (Trim, Replace("report.json", bytes("{broken}")), "Expecting property name"), + (Trim, Replace("report.json", vec![0xff]), "not UTF-8"), + (Trim, Replace("report.json", bytes("[]")), "the report is not an object"), + (Trim, Replace("report.json", bytes(r#"{"genes":[],"drugs":{}}"#)), "genes is not an object"), + (Trim, Replace("report.json", bytes(r#"{"genes":{},"drugs":[]}"#)), "drugs is not an object"), + (Trim, Replace("report.json", bytes(r#"{"genes":{"G":{"sourceDiplotypes":"x"}},"drugs":{}}"#)), "sourceDiplotypes is not a list"), + (Trim, Replace("report.json", bytes(r#"{"genes":{"G":{"recommendationDiplotypes":{ "x":1 }}},"drugs":{}}"#)), "recommendationDiplotypes is not a list"), + (Trim, Replace("report.json", bytes(r#"{"genes":{"G":{"sourceDiplotypes":[{"allele1":"x"}]}},"drugs":{}}"#)), "allele1 is not an object"), + (Trim, Replace("report.json", bytes(r#"{"genes":{"G":{"sourceDiplotypes":[{"allele2":["x"]}]}},"drugs":{}}"#)), "allele2 is not an object"), + (Trim, Replace("report.json", bytes(r#"{"genes":{},"drugs":{"S":{"D":{"guidelines":[{"annotations":[{"genotypes":[{"diplotypes":{"x":1}}]}]}]}}}}"#)), "diplotypes is not a list"), + ]; + for limit in ["1_0", "٤", "18446744073709551616"] { + cases.push((Trim, Limit(limit), "is not a whole number")); + } + for value in ["3_0", "٣", "-3", "18446744073709551616"] { + cases.push((Gvcf, Replace("truth.vcf.gz", bytes(&format!("chrA\t{value}\t.\tA\tC\n"))), "is not a number")); + } + for row in ["chrA\t50\t18446744073709551598\t1\t2\n", "chrA\t50\t1\t1\t18446744073709551615\n", "chrA\t50\t1000\t50\t51\n"] { + cases.push((Gvcf, Replace("ref.fasta.fai", bytes(row)), "invalid FASTA offset")); + } + for bad in [r#"{"packageFile":"unused","deps":null}"#, r#"{"packageFile":[],"deps":[]}"#, r#"{"packageFile":"unused","deps":[{"updates":{}}]}"#, r#"{"packageFile":"unused","deps":[{"depName":0}]}"#, r#"{"packageFile":"unused","deps":[{"currentValue":0}]}"#, r#"{"packageFile":"unused","deps":[{"updates":[{"newValue":0}]}]}"#] { + cases.push((Renovate, Append("renovate.log", bytes(&format!("{{\"msg\":\"packageFiles with updates\",\"config\":{{\"regex\":[{bad}]}}}}\n"))), "not a map of package file lists")); + } + // A zero-byte gzip file is written directly, without the test compressor. + for (verb, file) in [(Gvcf, "truth.vcf.gz"), (Clashes, "raw.vcf.gz"), (Clashes, "split.vcf.gz"), (Clashes, "chrA.tsv.gz")] { + cases.push((verb, Replace(file, Vec::new()), "wrong compression (expected gzip)")); + } + // Plain inputs reject gzip, and compressed inputs reject plain text. + cases.push((Gvcf, Replace("ref.fasta", vec![0x1f, 0x8b, b'A', b'A']), "wrong compression (expected plain)")); + cases.push((Gvcf, Replace("truth.bed", vec![0x1f, 0x8b]), "wrong compression (expected plain)")); + for verb in [Gvcf, Clashes, Trim, Renovate] { cases.push((verb, LongName, "File name too long")); } + for verb in [Gvcf, Clashes, Trim, Renovate, Comphet] { + cases.push((verb, ReadOnly, "read-only destination")); + cases.push((verb, ParentReadOnly, "parent directory is not writable")); + cases.push((verb, Directory, "destination is not a regular file")); + } + for (index, (verb, fault, message)) in cases.into_iter().enumerate() { + for linked in [false, true] { + let d = planted(&format!("refusal-{index}-{linked}")); + let inputs = d.join("inputs"); + fs::create_dir(&inputs).unwrap(); + if matches!(verb, Gvcf | Clashes) { + let kind = if matches!(verb, Gvcf) { "hg002-gvcf" } else { "clashes" }; + let src = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../tests/parity/inputs").join(format!("dev-evidence-{kind}")); + for entry in fs::read_dir(src).unwrap() { + let entry = entry.unwrap(); + fs::copy(entry.path(), inputs.join(entry.file_name())).unwrap(); + } + // Force the offset controls to read position 13. + if matches!(&fault, Replace("ref.fasta.fai", text) if text.starts_with(b"chrA\t50")) { + fs::write(inputs.join("sites.tsv"), b"id\tchr\tpos\nx\tchrA\t13\n").unwrap(); + fs::write(inputs.join("truth.bed"), b"chrA\t0\t50\n").unwrap(); + } + } + fs::write(inputs.join("report.json"), br#"{"genes":{},"drugs":{}}"#).unwrap(); + let input_root = if matches!(verb, Renovate) { &d } else { &inputs }; + match &fault { + Replace(name, data) | Append(name, data) => { + let path = input_root.join(name); + let mut content = if matches!(fault, Append(..)) { + if name.ends_with(".gz") { + use std::io::Read; + let mut old = Vec::new(); + flate2::read::MultiGzDecoder::new(fs::File::open(&path).unwrap()).read_to_end(&mut old).unwrap(); + old + } else { fs::read(&path).unwrap() } + } else { Vec::new() }; + content.extend_from_slice(data); + if name.ends_with(".gz") && !content.is_empty() { + let mut gz = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default()); + gz.write_all(&content).unwrap(); + content = gz.finish().unwrap(); + } + fs::write(path, content).unwrap(); + } + Prefix(data) => { + let old = fs::read(d.join("renovate.log")).unwrap(); + fs::write(d.join("renovate.log"), [data.as_slice(), &old].concat()).unwrap(); + } + _ => {} + } + let outputs = d.join("outputs"); + let targets = d.join("targets"); + fs::create_dir(&outputs).unwrap(); + fs::create_dir(&targets).unwrap(); + let long = "x".repeat(255); + let names = if matches!(verb, Comphet) { vec!["reads.sam", "vep.vcf", "comphet.vcf"] } else { vec![if matches!(fault, LongName) { &long } else { "out" }] }; + let mut previous = Vec::new(); + for (n, name) in names.iter().enumerate() { + let output = outputs.join(name); + let target = if linked { targets.join(name) } else { output.clone() }; + let directory = matches!(fault, Directory) && n == 0; + if directory { + fs::create_dir(&target).unwrap(); + fs::write(target.join("sentinel"), b"PREVIOUS OUTPUT\n").unwrap(); + } else { fs::write(&target, b"PREVIOUS OUTPUT\n").unwrap(); } + let mode = if matches!(fault, ReadOnly) && n == 0 { 0o444 } else if directory { 0o755 } else { 0o4600 }; + fs::set_permissions(&target, fs::Permissions::from_mode(mode)).unwrap(); + if linked { symlink(&target, &output).unwrap(); } + previous.push((output, target, mode, directory)); + } + let parent = if linked { &targets } else { &outputs }; + if matches!(fault, ParentReadOnly) { fs::set_permissions(parent, fs::Permissions::from_mode(0o555)).unwrap(); } + let p = |n: &str| inputs.join(n).to_str().unwrap().to_owned(); + let dest = previous[0].0.to_str().unwrap().to_owned(); + let (mut stdout, mut stderr) = (Vec::new(), Vec::new()); + let code = match verb { + Gvcf => crate::evidence::cli(&["hg002-gvcf".into(), p("truth.vcf.gz"), p("truth.bed"), p("ref.fasta"), p("sites.tsv"), dest], &mut stdout, &mut stderr), + Clashes => crate::evidence::cli(&["clashes".into(), p("raw.vcf.gz"), p("split.vcf.gz"), inputs.to_str().unwrap().into(), dest], &mut stdout, &mut stderr), + Trim => { + let mut args = vec!["trim-pharmcat".into(), p("report.json"), dest]; + if let Limit(limit) = &fault { args.push((*limit).into()); } + crate::fixture::cli(&args, &mut stdout, &mut stderr) + } + Renovate => cli_with_summary(&[d.join("renovate.log").to_str().unwrap().into(), "--repo".into(), d.to_str().unwrap().into()], &mut stdout, &mut stderr, Some(Path::new(&dest))), + Comphet => crate::fixture::cli(&["comphet".into(), outputs.to_str().unwrap().into()], &mut stdout, &mut stderr), + }; + fs::set_permissions(parent, fs::Permissions::from_mode(0o755)).unwrap(); + let diagnostic = String::from_utf8(stderr).unwrap(); + assert_ne!(code, 0, "row {index} {verb:?}, linked={linked}: false success"); + assert!(diagnostic.contains(message), "row {index} {verb:?}, linked={linked}: want {message:?}, got {diagnostic}"); + for (output, target, mode, directory) in previous { + let meta = fs::symlink_metadata(&output).unwrap(); + assert_eq!(meta.file_type().is_symlink(), linked, "row {index}"); + assert_eq!(fs::read_link(&output).ok(), linked.then_some(target.clone()), "row {index}: link target changed"); + assert_eq!(fs::metadata(&target).unwrap().permissions().mode() & 0o7777, mode, "row {index}: mode changed"); + assert_eq!(fs::read(if directory { target.join("sentinel") } else { target }).unwrap(), b"PREVIOUS OUTPUT\n", "row {index}: bytes changed"); + } + for parent in [&outputs, &targets] { + assert!(!fs::read_dir(parent).unwrap().any(|e| e.unwrap().file_name().to_string_lossy().contains(".tmp-")), "row {index}: temporary sibling leaked"); + } + fs::remove_dir_all(d).unwrap(); + } + } + } + #[test] fn a_pin_renovate_misses_fails() { let d = planted("pin"); From ae9b69412645c82a12725a1e76b40b1d83847f6c Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 20:30:23 +0100 Subject: [PATCH 44/45] fix(dev): refuse JSON reader limits before selecting Renovate config --- CHANGELOG.md | 1 + docs/rust.md | 57 ++- rust/crates/og-dev/src/renovate.rs | 536 ++++++++++++++++++++++++----- 3 files changed, 499 insertions(+), 95 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f66dcea84..cfb63afcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Each release's notes. From 1.0.0 on, a major or minor release has hand-written n - `og dev evidence hg002-gvcf` skips a panel site past the end of its contig. The Python read the byte after the contig, and at the end of the file wrote a reference block with an empty REF. - `og dev renovate-check` stops with exit 2 when a pin's file is missing or its glob matches no file. The Python counted such a pin in no file, so it could never fail. - `og dev fixture trim-pharmcat` names itself in the trimmed report's `_fixture_note`. + - Renovate skips only malformed JSON lines. JSON reader limits (signed 128-bit integers, Unicode scalar strings and 1000 nesting levels) now fail before an earlier config can pass the check. Evidence, fixture and Renovate refusals preserve existing output bytes, modes and symlink targets. [The refusal rule](docs/rust.md#input-and-destination-refusal-rule) states the numeric and size limits, with a shared table of negative CLI controls. - The Rust summary's CHIP reader marks a depth table with NaN unreadable instead of sorting it into a usable sensitivity estimate. Its unindexed APOL1 G2 reader keeps only records overlapping the deletion span and stops past that span on sorted input. - The Rust `og` binary now reads the rest of the summary's own sections: `og summary collect --only c`. It reads 32 sections of summary.json (couple, parascopy, paralogs, hla, prs, ancestry_fine, manta, delly, cnvpytor, sv_consensus, annotsv, expansions and their readings, telomere, roh, haplogroup, y_haplogroup, y_tree, y_str, consumer_export, bloodtype, lpa, vntyper, locityper, mito, traits with the APOL1 G2 call, archaic, ancient, viral, exomiser, chip, mei and mosaic). Nothing calls it yet: the reports still read the summary that `bin/collect_summary.py` writes. The sections are the same as the Python's on 15 new parity cases, compared as JSON with key order. The readers run on a thread pool, one per section. On a synthetic sample folder with every section and a 1,000,000-record VCF it takes 0.33 s and 34 MB, where the Python readers took 0.33 s and 41 to 59 MB. What differs from the Python: diff --git a/docs/rust.md b/docs/rust.md index 4cd9426c3..feda92600 100644 --- a/docs/rust.md +++ b/docs/rust.md @@ -139,10 +139,43 @@ flushed. This includes the evidence status and Renovate table. A configured Renovate summary must also be written and flushed successfully. Guard runs the FASTA offset regression in debug and release profiles. -### Stricter than the replaced Python, on purpose - -Each refusal below has a pinned negative CLI test with exit 1 or 2 and a stable -error class in `og-dev`. The final-LF normalization has a positive byte test. +### Input and destination refusal rule + +These evidence, fixture and Renovate verbs may refuse any input that is +malformed, outside the numeric ranges or size limits below, or not representable +by the JSON reader. They may also refuse any destination beside which they +cannot stage a sibling temporary file. A refusal always exits nonzero with a +specific message. It leaves every existing output byte-identical, with its mode +and link target unchanged. The crash and multi-file publication limits above +still apply. + +- Evidence numeric fields use ASCII unsigned integers from 0 through + 18446744073709551615. A reference lookup needs a position of at least 1. + Spans must fit that range. Calculated FASTA offsets must fit 0 through + 9223372036854775807 and address a byte inside the file. +- Every FAI row needs at least five columns and nonzero bases per line. +- `MAX_ANNOTATIONS` is an ASCII signed integer from -9223372036854775808 + through 9223372036854775807. +- Evidence and Renovate text records have a 16 MiB (16777216-byte) limit. + The PharmCAT reader has no separate file-size limit and materializes JSON. +- The JSON reader accepts signed 128-bit integers (-2^127 through 2^127-1), + Unicode scalar strings and at most 1000 nested arrays or objects. + It cannot represent lone surrogates, including in unused fields. +- Destinations need a writable parent and a regular writable target when one + exists. Resolution follows at most 40 final symlinks. Temporary sibling names + must fit the filesystem's name limit. A 255-byte basename is refused cleanly + before any output write because its temporary sibling name is longer. + +Inputs produced by the real tools are never refused: bcftools/DeepVariant VCF, +samtools faidx, UCSC BED, PharmCAT report JSON and Renovate debug log. + +`refusals_preserve_bytes_modes_and_link_targets` pins the rule through each CLI. +Each table row runs with a regular output and with a symlink. It checks nonzero +exit, the specific diagnostic, unchanged bytes, mode and link target, and no +leftover temporary file. Adding a refusal case needs one row. + +Examples of refusals follow. This is not an exhaustive input grammar. +The final-LF normalization has a positive byte test. - BED fields use spaces and tabs as separators. Other Python whitespace (Unicode White_Space and U+001C through U+001F) fails with `unsupported whitespace`. @@ -176,3 +209,19 @@ error class in `og-dev`. The final-LF normalization has a positive byte test. - FASTA offsets use checked subtraction, division, multiplication, addition and remainder. Overflow, a position beyond the file, or an offset outside the signed file-offset range fails with `invalid FASTA offset`. + +- A nonblank unused FAI row with fewer than five columns fails with + `a row has fewer than 5 columns`. +- A truth position that fits u64 but whose REF span does not fails with + `position span overflows`. +- A destination whose temporary sibling name is too long fails with the + filesystem diagnostic `File name too long`. +- JSON integers outside signed 128-bit range and escaped lone surrogates fail + with `Integer beyond 128 bits` and `Lone surrogate`, even in unused fields. + Excess nesting fails with `nested deeper than 1000 levels`. + +Renovate skips genuine JSON syntax errors only. Reader limitations fail the +command before an earlier matching config or the summary can be used. +`TooDeep` has its own error variant. The reader currently puts integer and +surrogate limitations in `JsonError::Decode`, so the loader distinguishes those +static reasons inside that variant. Other decode errors remain skippable. diff --git a/rust/crates/og-dev/src/renovate.rs b/rust/crates/og-dev/src/renovate.rs index e787e50ab..f279010b1 100644 --- a/rust/crates/og-dev/src/renovate.rs +++ b/rust/crates/og-dev/src/renovate.rs @@ -264,9 +264,21 @@ fn read_pins(repo: &Path, path: &Path) -> Result { fn load(log: &str) -> Result, String> { let mut files = None; crate::evidence::lines(log, |line| { - // The old load() skipped only JSON parse errors, not parsed scalars. - let Ok(value) = json::loads(std::str::from_utf8(line).expect("strict reader")) else { - return Ok(()); + let value = match json::loads(std::str::from_utf8(line).expect("strict reader")) { + Ok(value) => value, + // TooDeep has a distinct kind. The reader still encodes its two + // other representation limits as static Decode reasons. They are + // valid JSON, so neither may hide a record or an earlier config. + Err( + e @ (json::JsonError::TooDeep + | json::JsonError::Decode( + "Integer beyond 128 bits" + | "Lone surrogate (Python keeps it; this port cannot)", + _, + )), + ) => return Err(format!("FAIL: {log}: JSON reader limitation: {e}")), + Err(json::JsonError::Decode(..)) => return Ok(()), + Err(e) => return Err(format!("FAIL: {log}: {e}")), }; let Value::Object(mut rec) = value else { return Err(format!("FAIL: {log}: a log record is not an object")); @@ -870,28 +882,60 @@ mod tests { #[test] fn json_reader_limits_never_fall_back_to_an_earlier_config() { let huge = "340282366920938463463374607431768211456"; - let deep = format!("{}{}", "[".repeat(json::MAX_DEPTH + 1), "]".repeat(json::MAX_DEPTH + 1)); + let deep = format!( + "{}{}", + "[".repeat(json::MAX_DEPTH + 1), + "]".repeat(json::MAX_DEPTH + 1) + ); let records = [ - ("integer-prefix", format!("{huge}\n"), false, "Integer beyond 128 bits"), - ("surrogate-prefix", "\"\\ud800\"\n".into(), false, "Lone surrogate"), - ("final-config", format!("{{\"msg\":\"packageFiles with updates\",\"config\":{{\"regex\":[{{\"packageFile\":\"unused\",\"deps\":null}}]}},\"extra\":{huge}}}\n"), true, "Integer beyond 128 bits"), - ("nesting-prefix", format!("{deep}\n"), false, "nested deeper than 1000 levels"), + ( + "integer-prefix", + format!("{huge}\n"), + false, + "Integer beyond 128 bits", + ), + ( + "surrogate-prefix", + "\"\\ud800\"\n".into(), + false, + "Lone surrogate", + ), + ( + "final-config", + format!( + "{{\"msg\":\"packageFiles with updates\",\"config\":{{\"regex\":[{{\"packageFile\":\"unused\",\"deps\":null}}]}},\"extra\":{huge}}}\n" + ), + true, + "Integer beyond 128 bits", + ), + ( + "nesting-prefix", + format!("{deep}\n"), + false, + "nested deeper than 1000 levels", + ), ]; - std::thread::Builder::new().stack_size(8 << 20).spawn(move || { - let mut misses = Vec::new(); - for (label, record, append, diagnostic) in records { - let d = planted(label); - let old = fs::read_to_string(d.join("renovate.log")).unwrap(); - let log = if append { old + &record } else { record + &old }; - fs::write(d.join("renovate.log"), log).unwrap(); - let (code, out) = run(&d); - if code != 1 || !out.contains(diagnostic) || out.contains("every check passed") { - misses.push(format!("{label}: exit {code}, {out}")); + std::thread::Builder::new() + .stack_size(8 << 20) + .spawn(move || { + let mut misses = Vec::new(); + for (label, record, append, diagnostic) in records { + let d = planted(label); + let old = fs::read_to_string(d.join("renovate.log")).unwrap(); + let log = if append { old + &record } else { record + &old }; + fs::write(d.join("renovate.log"), log).unwrap(); + let (code, out) = run(&d); + if code != 1 || !out.contains(diagnostic) || out.contains("every check passed") + { + misses.push(format!("{label}: exit {code}, {out}")); + } + fs::remove_dir_all(d).unwrap(); } - fs::remove_dir_all(d).unwrap(); - } - assert!(misses.is_empty(), "{}", misses.join("\n")); - }).unwrap().join().unwrap(); + assert!(misses.is_empty(), "{}", misses.join("\n")); + }) + .unwrap() + .join() + .unwrap(); } /// The refusal contract applies to every row, including unused input fields. @@ -899,13 +943,24 @@ mod tests { #[test] fn refusals_preserve_bytes_modes_and_link_targets() { // Match the main-thread stack used by og for the nesting-limit control. - std::thread::Builder::new().stack_size(8 << 20).spawn(refusal_table).unwrap().join().unwrap(); + std::thread::Builder::new() + .stack_size(8 << 20) + .spawn(refusal_table) + .unwrap() + .join() + .unwrap(); } fn refusal_table() { use std::os::unix::fs::{PermissionsExt, symlink}; #[derive(Clone, Copy, Debug)] - enum Verb { Gvcf, Clashes, Trim, Renovate, Comphet } + enum Verb { + Gvcf, + Clashes, + Trim, + Renovate, + Comphet, + } enum Fault { Replace(&'static str, Vec), Append(&'static str, Vec), @@ -920,88 +975,287 @@ mod tests { use Verb::*; let bytes = |s: &str| s.as_bytes().to_vec(); let huge = "340282366920938463463374607431768211456"; - let deep = format!("{}{}", "[".repeat(json::MAX_DEPTH + 1), "]".repeat(json::MAX_DEPTH + 1)); - let final_record = format!(r#"{{"msg":"packageFiles with updates","config":{{"regex":[{{"packageFile":"unused","deps":null}}]}},"extra":{huge}}}"#); + let deep = format!( + "{}{}", + "[".repeat(json::MAX_DEPTH + 1), + "]".repeat(json::MAX_DEPTH + 1) + ); + let final_record = format!( + r#"{{"msg":"packageFiles with updates","config":{{"regex":[{{"packageFile":"unused","deps":null}}]}},"extra":{huge}}}"# + ); // Add a refusal by adding one row: CLI, input fault, diagnostic. let mut cases = vec![ - (Renovate, Prefix(bytes(&format!("{huge}\n"))), "Integer beyond 128 bits"), + ( + Renovate, + Prefix(bytes(&format!("{huge}\n"))), + "Integer beyond 128 bits", + ), (Renovate, Prefix(bytes("\"\\ud800\"\n")), "Lone surrogate"), - (Renovate, Append("renovate.log", bytes(&format!("{final_record}\n"))), "Integer beyond 128 bits"), - (Renovate, Prefix(bytes(&format!("{deep}\n"))), "nested deeper than 1000 levels"), - (Renovate, Prefix(bytes("42\n")), "a log record is not an object"), - (Renovate, Prefix(vec![0xff, b'\n']), "not UTF-8"), - (Renovate, Prefix(vec![b'x'; (16 << 20) + 1]), "record exceeds 16777216 bytes"), - (Renovate, Replace(PINS, bytes("dep\tdocs/*.absent\tx\n")), "matches no file"), - (Renovate, Replace(PINS, bytes("dep\tabsent\tx\n")), "is not a file"), - (Gvcf, Append("ref.fasta.fai", bytes("chrUnused\t1\n")), "a row has fewer than 5 columns"), - (Gvcf, Replace("truth.vcf.gz", bytes("chrA\t18446744073709551615\t.\tA\tC\n")), "position span overflows"), - (Gvcf, Replace("ref.fasta.fai", bytes("\n")), "a row has fewer than 5 columns"), - (Gvcf, Append("ref.fasta.fai", bytes("chrUnused\t0\t0\t0\t0\n")), "a row has 0 bases per line"), - (Gvcf, Replace("truth.vcf.gz", bytes("chrA\t3\t.\tA\n")), "a record has fewer than 5 columns"), - (Gvcf, Replace("truth.bed", bytes("\n")), "a row has fewer than 3 columns"), - (Gvcf, Replace("truth.bed", bytes("chrA\u{85}0\u{85}50\n")), "unsupported whitespace"), + ( + Renovate, + Append("renovate.log", bytes(&format!("{final_record}\n"))), + "Integer beyond 128 bits", + ), + ( + Renovate, + Prefix(bytes(&format!("{deep}\n"))), + "nested deeper than 1000 levels", + ), + ( + Renovate, + Prefix(bytes("42\n")), + "a log record is not an object", + ), + (Renovate, Prefix(vec![0xff, b'\n']), "invalid UTF-8"), + ( + Renovate, + Prefix(vec![b'x'; (16 << 20) + 1]), + "record exceeds 16777216 bytes", + ), + ( + Renovate, + Replace(PINS, bytes("dep\tdocs/*.absent\tx\n")), + "matches no file", + ), + ( + Renovate, + Replace(PINS, bytes("dep\tabsent\tx\n")), + "is not a file", + ), + ( + Gvcf, + Append("ref.fasta.fai", bytes("chrUnused\t1\n")), + "a row has fewer than 5 columns", + ), + ( + Gvcf, + Replace( + "truth.vcf.gz", + bytes("chrA\t18446744073709551615\t.\tA\tC\n"), + ), + "position span overflows", + ), + ( + Gvcf, + Replace("ref.fasta.fai", bytes("\n")), + "a row has fewer than 5 columns", + ), + ( + Gvcf, + Append("ref.fasta.fai", bytes("chrUnused\t0\t0\t0\t0\n")), + "a row has 0 bases per line", + ), + ( + Gvcf, + Replace("truth.vcf.gz", bytes("chrA\t3\t.\tA\n")), + "a record has fewer than 5 columns", + ), + ( + Gvcf, + Replace("truth.bed", bytes("\n")), + "a row has fewer than 3 columns", + ), + ( + Gvcf, + Replace("truth.bed", bytes("chrA\u{85}0\u{85}50\n")), + "unsupported whitespace", + ), (Gvcf, Replace("sites.tsv", bytes("")), "missing header"), - (Gvcf, Append("truth.vcf.gz", bytes("##late=yes\n")), "VCF header after first data record"), - (Clashes, Replace("raw.vcf.gz", bytes("chrA\t3\t.\tA\tC\n")), "missing trailing VCF columns"), - (Trim, Replace("report.json", bytes(&format!(r#"{{"genes":{{}},"drugs":{{}},"title":{huge}}}"#))), "Integer beyond 128 bits"), - (Trim, Replace("report.json", bytes(r#"{"genes":{},"drugs":{},"title":"\ud800"}"#)), "Lone surrogate"), - (Trim, Replace("report.json", bytes(&deep)), "nested deeper than 1000 levels"), - (Trim, Replace("report.json", bytes("{broken}")), "Expecting property name"), + ( + Gvcf, + Append("truth.vcf.gz", bytes("##late=yes\n")), + "VCF header after first data record", + ), + ( + Clashes, + Replace("raw.vcf.gz", bytes("chrA\t3\t.\tA\tC\n")), + "missing trailing VCF columns", + ), + ( + Trim, + Replace( + "report.json", + bytes(&format!(r#"{{"genes":{{}},"drugs":{{}},"title":{huge}}}"#)), + ), + "Integer beyond 128 bits", + ), + ( + Trim, + Replace( + "report.json", + bytes(r#"{"genes":{},"drugs":{},"title":"\ud800"}"#), + ), + "Lone surrogate", + ), + ( + Trim, + Replace("report.json", bytes(&deep)), + "nested deeper than 1000 levels", + ), + ( + Trim, + Replace("report.json", bytes("{broken}")), + "Expecting property name", + ), (Trim, Replace("report.json", vec![0xff]), "not UTF-8"), - (Trim, Replace("report.json", bytes("[]")), "the report is not an object"), - (Trim, Replace("report.json", bytes(r#"{"genes":[],"drugs":{}}"#)), "genes is not an object"), - (Trim, Replace("report.json", bytes(r#"{"genes":{},"drugs":[]}"#)), "drugs is not an object"), - (Trim, Replace("report.json", bytes(r#"{"genes":{"G":{"sourceDiplotypes":"x"}},"drugs":{}}"#)), "sourceDiplotypes is not a list"), - (Trim, Replace("report.json", bytes(r#"{"genes":{"G":{"recommendationDiplotypes":{ "x":1 }}},"drugs":{}}"#)), "recommendationDiplotypes is not a list"), - (Trim, Replace("report.json", bytes(r#"{"genes":{"G":{"sourceDiplotypes":[{"allele1":"x"}]}},"drugs":{}}"#)), "allele1 is not an object"), - (Trim, Replace("report.json", bytes(r#"{"genes":{"G":{"sourceDiplotypes":[{"allele2":["x"]}]}},"drugs":{}}"#)), "allele2 is not an object"), - (Trim, Replace("report.json", bytes(r#"{"genes":{},"drugs":{"S":{"D":{"guidelines":[{"annotations":[{"genotypes":[{"diplotypes":{"x":1}}]}]}]}}}}"#)), "diplotypes is not a list"), + ( + Trim, + Replace("report.json", bytes("[]")), + "the report is not an object", + ), + ( + Trim, + Replace("report.json", bytes(r#"{"genes":[],"drugs":{}}"#)), + "genes is not an object", + ), + ( + Trim, + Replace("report.json", bytes(r#"{"genes":{},"drugs":[]}"#)), + "drugs is not an object", + ), + ( + Trim, + Replace( + "report.json", + bytes(r#"{"genes":{"G":{"sourceDiplotypes":"x"}},"drugs":{}}"#), + ), + "sourceDiplotypes is not a list", + ), + ( + Trim, + Replace( + "report.json", + bytes(r#"{"genes":{"G":{"recommendationDiplotypes":{ "x":1 }}},"drugs":{}}"#), + ), + "recommendationDiplotypes is not a list", + ), + ( + Trim, + Replace( + "report.json", + bytes(r#"{"genes":{"G":{"sourceDiplotypes":[{"allele1":"x"}]}},"drugs":{}}"#), + ), + "allele1 is not an object", + ), + ( + Trim, + Replace( + "report.json", + bytes(r#"{"genes":{"G":{"sourceDiplotypes":[{"allele2":["x"]}]}},"drugs":{}}"#), + ), + "allele2 is not an object", + ), + ( + Trim, + Replace( + "report.json", + bytes( + r#"{"genes":{},"drugs":{"S":{"D":{"guidelines":[{"annotations":[{"genotypes":[{"diplotypes":{"x":1}}]}]}]}}}}"#, + ), + ), + "diplotypes is not a list", + ), ]; for limit in ["1_0", "٤", "18446744073709551616"] { cases.push((Trim, Limit(limit), "is not a whole number")); } for value in ["3_0", "٣", "-3", "18446744073709551616"] { - cases.push((Gvcf, Replace("truth.vcf.gz", bytes(&format!("chrA\t{value}\t.\tA\tC\n"))), "is not a number")); + cases.push(( + Gvcf, + Replace("truth.vcf.gz", bytes(&format!("chrA\t{value}\t.\tA\tC\n"))), + "is not a number", + )); } - for row in ["chrA\t50\t18446744073709551598\t1\t2\n", "chrA\t50\t1\t1\t18446744073709551615\n", "chrA\t50\t1000\t50\t51\n"] { - cases.push((Gvcf, Replace("ref.fasta.fai", bytes(row)), "invalid FASTA offset")); + for row in [ + "chrA\t50\t18446744073709551598\t1\t2\n", + "chrA\t50\t1\t1\t18446744073709551615\n", + "chrA\t50\t1000\t50\t51\n", + ] { + cases.push(( + Gvcf, + Replace("ref.fasta.fai", bytes(row)), + "invalid FASTA offset", + )); } - for bad in [r#"{"packageFile":"unused","deps":null}"#, r#"{"packageFile":[],"deps":[]}"#, r#"{"packageFile":"unused","deps":[{"updates":{}}]}"#, r#"{"packageFile":"unused","deps":[{"depName":0}]}"#, r#"{"packageFile":"unused","deps":[{"currentValue":0}]}"#, r#"{"packageFile":"unused","deps":[{"updates":[{"newValue":0}]}]}"#] { + for bad in [ + r#"{"packageFile":"unused","deps":null}"#, + r#"{"packageFile":[],"deps":[]}"#, + r#"{"packageFile":"unused","deps":[{"updates":{}}]}"#, + r#"{"packageFile":"unused","deps":[{"depName":0}]}"#, + r#"{"packageFile":"unused","deps":[{"currentValue":0}]}"#, + r#"{"packageFile":"unused","deps":[{"updates":[{"newValue":0}]}]}"#, + ] { cases.push((Renovate, Append("renovate.log", bytes(&format!("{{\"msg\":\"packageFiles with updates\",\"config\":{{\"regex\":[{bad}]}}}}\n"))), "not a map of package file lists")); } // A zero-byte gzip file is written directly, without the test compressor. - for (verb, file) in [(Gvcf, "truth.vcf.gz"), (Clashes, "raw.vcf.gz"), (Clashes, "split.vcf.gz"), (Clashes, "chrA.tsv.gz")] { - cases.push((verb, Replace(file, Vec::new()), "wrong compression (expected gzip)")); + for (verb, file) in [ + (Gvcf, "truth.vcf.gz"), + (Clashes, "raw.vcf.gz"), + (Clashes, "split.vcf.gz"), + (Clashes, "chrA.tsv.gz"), + ] { + cases.push(( + verb, + Replace(file, Vec::new()), + "wrong compression (expected gzip)", + )); } // Plain inputs reject gzip, and compressed inputs reject plain text. - cases.push((Gvcf, Replace("ref.fasta", vec![0x1f, 0x8b, b'A', b'A']), "wrong compression (expected plain)")); - cases.push((Gvcf, Replace("truth.bed", vec![0x1f, 0x8b]), "wrong compression (expected plain)")); - for verb in [Gvcf, Clashes, Trim, Renovate] { cases.push((verb, LongName, "File name too long")); } + cases.push(( + Gvcf, + Replace("ref.fasta", vec![0x1f, 0x8b, b'A', b'A']), + "wrong compression (expected plain)", + )); + cases.push(( + Gvcf, + Replace("truth.bed", vec![0x1f, 0x8b]), + "wrong compression (expected plain)", + )); + for verb in [Gvcf, Clashes, Trim, Renovate] { + cases.push((verb, LongName, "File name too long")); + } for verb in [Gvcf, Clashes, Trim, Renovate, Comphet] { cases.push((verb, ReadOnly, "read-only destination")); cases.push((verb, ParentReadOnly, "parent directory is not writable")); cases.push((verb, Directory, "destination is not a regular file")); } + eprintln!( + "refusal contract: {} rows, {} CLI invocations", + cases.len(), + cases.len() * 2 + ); for (index, (verb, fault, message)) in cases.into_iter().enumerate() { for linked in [false, true] { let d = planted(&format!("refusal-{index}-{linked}")); let inputs = d.join("inputs"); fs::create_dir(&inputs).unwrap(); if matches!(verb, Gvcf | Clashes) { - let kind = if matches!(verb, Gvcf) { "hg002-gvcf" } else { "clashes" }; - let src = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../tests/parity/inputs").join(format!("dev-evidence-{kind}")); + let kind = if matches!(verb, Gvcf) { + "hg002-gvcf" + } else { + "clashes" + }; + let src = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../../tests/parity/inputs") + .join(format!("dev-evidence-{kind}")); for entry in fs::read_dir(src).unwrap() { let entry = entry.unwrap(); fs::copy(entry.path(), inputs.join(entry.file_name())).unwrap(); } // Force the offset controls to read position 13. - if matches!(&fault, Replace("ref.fasta.fai", text) if text.starts_with(b"chrA\t50")) { - fs::write(inputs.join("sites.tsv"), b"id\tchr\tpos\nx\tchrA\t13\n").unwrap(); + if matches!(&fault, Replace("ref.fasta.fai", text) if text.starts_with(b"chrA\t50")) + { + fs::write(inputs.join("sites.tsv"), b"id\tchr\tpos\nx\tchrA\t13\n") + .unwrap(); fs::write(inputs.join("truth.bed"), b"chrA\t0\t50\n").unwrap(); } } fs::write(inputs.join("report.json"), br#"{"genes":{},"drugs":{}}"#).unwrap(); - let input_root = if matches!(verb, Renovate) { &d } else { &inputs }; + let input_root = if matches!(verb, Renovate) { + &d + } else { + &inputs + }; match &fault { Replace(name, data) | Append(name, data) => { let path = input_root.join(name); @@ -1009,13 +1263,22 @@ mod tests { if name.ends_with(".gz") { use std::io::Read; let mut old = Vec::new(); - flate2::read::MultiGzDecoder::new(fs::File::open(&path).unwrap()).read_to_end(&mut old).unwrap(); + flate2::read::MultiGzDecoder::new(fs::File::open(&path).unwrap()) + .read_to_end(&mut old) + .unwrap(); old - } else { fs::read(&path).unwrap() } - } else { Vec::new() }; + } else { + fs::read(&path).unwrap() + } + } else { + Vec::new() + }; content.extend_from_slice(data); if name.ends_with(".gz") && !content.is_empty() { - let mut gz = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default()); + let mut gz = flate2::write::GzEncoder::new( + Vec::new(), + flate2::Compression::default(), + ); gz.write_all(&content).unwrap(); content = gz.finish().unwrap(); } @@ -1023,7 +1286,8 @@ mod tests { } Prefix(data) => { let old = fs::read(d.join("renovate.log")).unwrap(); - fs::write(d.join("renovate.log"), [data.as_slice(), &old].concat()).unwrap(); + fs::write(d.join("renovate.log"), [data.as_slice(), &old].concat()) + .unwrap(); } _ => {} } @@ -1032,50 +1296,140 @@ mod tests { fs::create_dir(&outputs).unwrap(); fs::create_dir(&targets).unwrap(); let long = "x".repeat(255); - let names = if matches!(verb, Comphet) { vec!["reads.sam", "vep.vcf", "comphet.vcf"] } else { vec![if matches!(fault, LongName) { &long } else { "out" }] }; + let names = if matches!(verb, Comphet) { + vec!["reads.sam", "vep.vcf", "comphet.vcf"] + } else { + vec![if matches!(fault, LongName) { + &long + } else { + "out" + }] + }; let mut previous = Vec::new(); for (n, name) in names.iter().enumerate() { let output = outputs.join(name); - let target = if linked { targets.join(name) } else { output.clone() }; + let target = if linked { + targets.join(name) + } else { + output.clone() + }; let directory = matches!(fault, Directory) && n == 0; if directory { fs::create_dir(&target).unwrap(); fs::write(target.join("sentinel"), b"PREVIOUS OUTPUT\n").unwrap(); - } else { fs::write(&target, b"PREVIOUS OUTPUT\n").unwrap(); } - let mode = if matches!(fault, ReadOnly) && n == 0 { 0o444 } else if directory { 0o755 } else { 0o4600 }; + } else { + fs::write(&target, b"PREVIOUS OUTPUT\n").unwrap(); + } + let mode = if matches!(fault, ReadOnly) && n == 0 { + 0o444 + } else if directory { + 0o755 + } else { + 0o4600 + }; fs::set_permissions(&target, fs::Permissions::from_mode(mode)).unwrap(); - if linked { symlink(&target, &output).unwrap(); } + if linked { + symlink(&target, &output).unwrap(); + } previous.push((output, target, mode, directory)); } let parent = if linked { &targets } else { &outputs }; - if matches!(fault, ParentReadOnly) { fs::set_permissions(parent, fs::Permissions::from_mode(0o555)).unwrap(); } + if matches!(fault, ParentReadOnly) { + fs::set_permissions(parent, fs::Permissions::from_mode(0o555)).unwrap(); + } let p = |n: &str| inputs.join(n).to_str().unwrap().to_owned(); let dest = previous[0].0.to_str().unwrap().to_owned(); let (mut stdout, mut stderr) = (Vec::new(), Vec::new()); let code = match verb { - Gvcf => crate::evidence::cli(&["hg002-gvcf".into(), p("truth.vcf.gz"), p("truth.bed"), p("ref.fasta"), p("sites.tsv"), dest], &mut stdout, &mut stderr), - Clashes => crate::evidence::cli(&["clashes".into(), p("raw.vcf.gz"), p("split.vcf.gz"), inputs.to_str().unwrap().into(), dest], &mut stdout, &mut stderr), + Gvcf => crate::evidence::cli( + &[ + "hg002-gvcf".into(), + p("truth.vcf.gz"), + p("truth.bed"), + p("ref.fasta"), + p("sites.tsv"), + dest, + ], + &mut stdout, + &mut stderr, + ), + Clashes => crate::evidence::cli( + &[ + "clashes".into(), + p("raw.vcf.gz"), + p("split.vcf.gz"), + inputs.to_str().unwrap().into(), + dest, + ], + &mut stdout, + &mut stderr, + ), Trim => { let mut args = vec!["trim-pharmcat".into(), p("report.json"), dest]; - if let Limit(limit) = &fault { args.push((*limit).into()); } + if let Limit(limit) = &fault { + args.push((*limit).into()); + } crate::fixture::cli(&args, &mut stdout, &mut stderr) } - Renovate => cli_with_summary(&[d.join("renovate.log").to_str().unwrap().into(), "--repo".into(), d.to_str().unwrap().into()], &mut stdout, &mut stderr, Some(Path::new(&dest))), - Comphet => crate::fixture::cli(&["comphet".into(), outputs.to_str().unwrap().into()], &mut stdout, &mut stderr), + Renovate => cli_with_summary( + &[ + d.join("renovate.log").to_str().unwrap().into(), + "--repo".into(), + d.to_str().unwrap().into(), + ], + &mut stdout, + &mut stderr, + Some(Path::new(&dest)), + ), + Comphet => crate::fixture::cli( + &["comphet".into(), outputs.to_str().unwrap().into()], + &mut stdout, + &mut stderr, + ), }; fs::set_permissions(parent, fs::Permissions::from_mode(0o755)).unwrap(); let diagnostic = String::from_utf8(stderr).unwrap(); - assert_ne!(code, 0, "row {index} {verb:?}, linked={linked}: false success"); - assert!(diagnostic.contains(message), "row {index} {verb:?}, linked={linked}: want {message:?}, got {diagnostic}"); + assert_ne!( + code, 0, + "row {index} {verb:?}, linked={linked}: false success" + ); + assert!( + diagnostic.contains(message), + "row {index} {verb:?}, linked={linked}: want {message:?}, got {diagnostic}" + ); for (output, target, mode, directory) in previous { let meta = fs::symlink_metadata(&output).unwrap(); assert_eq!(meta.file_type().is_symlink(), linked, "row {index}"); - assert_eq!(fs::read_link(&output).ok(), linked.then_some(target.clone()), "row {index}: link target changed"); - assert_eq!(fs::metadata(&target).unwrap().permissions().mode() & 0o7777, mode, "row {index}: mode changed"); - assert_eq!(fs::read(if directory { target.join("sentinel") } else { target }).unwrap(), b"PREVIOUS OUTPUT\n", "row {index}: bytes changed"); + assert_eq!( + fs::read_link(&output).ok(), + linked.then_some(target.clone()), + "row {index}: link target changed" + ); + assert_eq!( + fs::metadata(&target).unwrap().permissions().mode() & 0o7777, + mode, + "row {index}: mode changed" + ); + assert_eq!( + fs::read(if directory { + target.join("sentinel") + } else { + target + }) + .unwrap(), + b"PREVIOUS OUTPUT\n", + "row {index}: bytes changed" + ); } for parent in [&outputs, &targets] { - assert!(!fs::read_dir(parent).unwrap().any(|e| e.unwrap().file_name().to_string_lossy().contains(".tmp-")), "row {index}: temporary sibling leaked"); + assert!( + !fs::read_dir(parent).unwrap().any(|e| e + .unwrap() + .file_name() + .to_string_lossy() + .contains(".tmp-")), + "row {index}: temporary sibling leaked" + ); } fs::remove_dir_all(d).unwrap(); } From b5ab8bf8d2456760ec77f460f91c653226d1781f Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 13 Nov 2026 21:15:40 +0100 Subject: [PATCH 45/45] docs(dev): say which BED files the evidence commands read and why deep JSON is accepted --- docs/rust.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/rust.md b/docs/rust.md index feda92600..8bc38d80b 100644 --- a/docs/rust.md +++ b/docs/rust.md @@ -167,7 +167,14 @@ still apply. before any output write because its temporary sibling name is longer. Inputs produced by the real tools are never refused: bcftools/DeepVariant VCF, -samtools faidx, UCSC BED, PharmCAT report JSON and Renovate debug log. +samtools faidx, header-free BED intervals, PharmCAT report JSON and Renovate +debug log. A BED file with a UCSC column header, a `track` line or a `browser` +line is refused, as the Python refused it: remove those lines first. + +Valid JSON nested up to the reader's 1000 levels is accepted. The Python stopped +with a RecursionError at a few hundred levels when it copied or wrote such a +value. That was a limit of the interpreter, not a check on the input, so the +Rust commands do not copy it. `refusals_preserve_bytes_modes_and_link_targets` pins the rule through each CLI. Each table row runs with a regular output and with a symlink. It checks nonzero