From a74e760d52cd6fb32064a12080b77a708b7b156a Mon Sep 17 00:00:00 2001 From: Robert Syme Date: Sun, 16 Nov 2025 00:44:33 +0000 Subject: [PATCH 01/22] Saving in-progress ideas so I can pull locally --- side-quests/smol-nextflow/bin/cat_me.sh | 114 ++++++++ side-quests/smol-nextflow/bin/classify.py | 144 ++++++++++ side-quests/smol-nextflow/bin/download.sh | 47 ++++ .../smol-nextflow/bin/inspect_model.py | 77 ++++++ side-quests/smol-nextflow/environment.yml | 12 + side-quests/smol-nextflow/main.nf | 48 ++++ side-quests/smol-nextflow/outline.md | 249 ++++++++++++++++++ 7 files changed, 691 insertions(+) create mode 100755 side-quests/smol-nextflow/bin/cat_me.sh create mode 100755 side-quests/smol-nextflow/bin/classify.py create mode 100755 side-quests/smol-nextflow/bin/download.sh create mode 100755 side-quests/smol-nextflow/bin/inspect_model.py create mode 100644 side-quests/smol-nextflow/environment.yml create mode 100644 side-quests/smol-nextflow/main.nf create mode 100644 side-quests/smol-nextflow/outline.md diff --git a/side-quests/smol-nextflow/bin/cat_me.sh b/side-quests/smol-nextflow/bin/cat_me.sh new file mode 100755 index 000000000..fd2ef1247 --- /dev/null +++ b/side-quests/smol-nextflow/bin/cat_me.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Show help message +show_help() { + cat << EOF +Usage: $0 [-n|--count NUM] [-p|--prefix PATH] [-h|--help] + +Download random cat images from cataas.com along with their tags. + +Options: + -c, --count NUM Number of cats to download (default: 1) + -p, --prefix PATH Directory to download files to (default: current directory) + -h, --help Show this help message and exit + +Examples: + $0 # Download 1 cat to current directory + $0 -n 5 # Download 5 cats to current directory + $0 -p cats -n 10 # Download 10 cats to ./cats directory + $0 --prefix data/cats # Download 1 cat to ./data/cats directory + +Output: + For each cat, creates two files: + - .jpg The cat image + - .txt The tags (one per line) + +EOF +} + +# Default values +num_downloads=1 +prefix="." + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + show_help + exit 0 + ;; + -c|--count) + num_downloads="$2" + shift 2 + ;; + -p|--prefix) + prefix="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" + echo "Use --help for usage information" + exit 1 + ;; + esac +done + +# Validate number +if ! [[ "$num_downloads" =~ ^[0-9]+$ ]] || [ "$num_downloads" -lt 1 ]; then + echo "Error: Number must be a positive integer" + exit 1 +fi + +# Create output directory if it doesn't exist +mkdir -p "$prefix" +echo "Downloading $num_downloads cat(s) to $prefix/" +echo "" + +# Download loop +for i in $(seq 1 "$num_downloads"); do + echo "[$i/$num_downloads]" + + # Get the JSON metadata + json=$(curl -s 'https://cataas.com/cat?json=true') + + # Extract fields using jq + cat_id=$(echo "$json" | jq -r '.id') + mimetype=$(echo "$json" | jq -r '.mimetype') + url=$(echo "$json" | jq -r '.url') + tags=$(echo "$json" | jq -r '.tags[]') # Extract tags, one per line + + # Map mimetype to extension - only accept jpg, png, gif + case "$mimetype" in + image/jpeg|image/jpg) + ext="jpg" + ;; + image/png) + ext="png" + ;; + image/gif) + ext="gif" + ;; + *) + echo "✗ Skipping unsupported type: $mimetype" + echo "" + continue + ;; + esac + + # Build filenames with prefix + filename="${prefix}/${cat_id}.${ext}" + tagfile="${prefix}/${cat_id}.txt" + + # Download the image + curl -s "$url" -o "$filename" + echo "✓ Saved as $filename" + + # Save tags to text file + echo "$tags" > "$tagfile" + echo "✓ Saved tags to $tagfile" + echo "" +done + +echo "Download complete! Downloaded $num_downloads cat(s) to $prefix/" diff --git a/side-quests/smol-nextflow/bin/classify.py b/side-quests/smol-nextflow/bin/classify.py new file mode 100755 index 000000000..af63518c3 --- /dev/null +++ b/side-quests/smol-nextflow/bin/classify.py @@ -0,0 +1,144 @@ +#!/usr/bin/env -S uv run +# /// script +# dependencies = [ +# "torch>=2.0.0", +# "pillow>=10.0.0", +# "open-clip-torch>=2.20.0", +# ] +# /// +"""Classify images using MetaCLIP with local weights.""" + +from pathlib import Path +from PIL import Image +import argparse +import json +import open_clip +import sys +import torch + +def classify_images(image_dir: Path, labels: list[str], model_path: Path, + json_output: bool = False, architecture: str = None): + """Classify images in a directory using MetaCLIP.""" + # Auto-detect architecture from model filename if not provided + if architecture is None: + filename = model_path.name.lower() + if 'b32' in filename or '32' in filename: + architecture = 'ViT-B-32-quickgelu' + elif 'b16' in filename or '16' in filename: + architecture = 'ViT-B-16-quickgelu' + elif 'l14' in filename: + architecture = 'ViT-L-14-quickgelu' + elif 'h14' in filename: + architecture = 'ViT-H-14-quickgelu' + else: + raise ValueError(f"Cannot infer architecture from {model_path.name}. " + f"Please specify --architecture") + + print(f"Using architecture: {architecture}", file=sys.stderr) + + # Load model and preprocessing + model, _, preprocess = open_clip.create_model_and_transforms( + architecture, + pretrained=str(model_path), + weights_only=False # Trust MetaCLIP checkpoint from Facebook Research + ) + tokenizer = open_clip.get_tokenizer(architecture) + # Prepare text labels + text = tokenizer(labels) + + # Process each image + results = [] + for img_path in image_dir.glob('*'): + if img_path.suffix.lower() in ['.jpg', '.jpeg', '.png', '.gif', '.bmp']: + try: + image = preprocess(Image.open(img_path)).unsqueeze(0) + + with torch.no_grad(): + image_features = model.encode_image(image) + text_features = model.encode_text(text) + + # Normalize and compute similarity + image_features /= image_features.norm(dim=-1, keepdim=True) + text_features /= text_features.norm(dim=-1, keepdim=True) + similarity = (100.0 * image_features @ text_features.T).softmax(dim=-1) + + # Get all confidences for all labels + confidences = {label: float(conf) for label, conf in zip(labels, similarity[0])} + + # Get top prediction + values, indices = similarity[0].topk(1) + prediction = labels[indices[0]] + confidence = values[0].item() + + result = { + 'file': img_path.name, + 'path': str(img_path), + 'prediction': prediction, + 'confidence': confidence, + 'all_confidences': confidences + } + + results.append(result) + + if json_output: + # Print JSONL (one JSON object per line) + print(json.dumps(result)) + else: + # Print human-readable format + print(f"{img_path.name}: {prediction} ({confidence:.2%})") + + except Exception as e: + error_result = { + 'file': img_path.name, + 'path': str(img_path), + 'error': str(e) + } + if json_output: + print(json.dumps(error_result)) + else: + print(f"Error processing {img_path.name}: {e}") + + return results + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Classify images using MetaCLIP") + parser.add_argument( + '--image-dir', + type=Path, + default=Path("data/pics"), + help='Directory containing images to classify (default: data/pics)' + ) + parser.add_argument( + '--model-path', + type=Path, + default=Path("data/models/b32_400m.pt"), + help='Path to MetaCLIP model weights (default: data/models/b32_400m.pt)' + ) + parser.add_argument( + '--labels', + nargs='+', + default=["cute cat", "ugly cat"], + help='Labels for classification (default: ["cute cat", "ugly cat"])' + ) + parser.add_argument( + '--json', + action='store_true', + help='Output results as JSONL (one JSON object per line) to stdout' + ) + parser.add_argument( + '--architecture', + type=str, + choices=['ViT-B-32-quickgelu', 'ViT-B-16-quickgelu', 'ViT-L-14-quickgelu', 'ViT-H-14-quickgelu'], + help='Model architecture (auto-detected from filename if not specified)' + ) + + args = parser.parse_args() + + results = classify_images( + args.image_dir, + args.labels, + args.model_path, + args.json, + args.architecture + ) + diff --git a/side-quests/smol-nextflow/bin/download.sh b/side-quests/smol-nextflow/bin/download.sh new file mode 100755 index 000000000..b4220608c --- /dev/null +++ b/side-quests/smol-nextflow/bin/download.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Check if CSV file is provided +if [ $# -eq 0 ]; then + echo "Usage: $0 " + exit 1 +fi + +csv_file="$1" + +# Check if file exists +if [ ! -f "$csv_file" ]; then + echo "Error: File '$csv_file' not found" + exit 1 +fi + +# Skip header if present and process each line +tail -n +2 "$csv_file" | while IFS=, read -r url attribution; do + # Remove leading/trailing whitespace and quotes + url=$(echo "$url" | xargs) + attribution=$(echo "$attribution" | xargs | sed 's/^"//;s/"$//') + + # Extract photo ID from URL (the part after /photos/) + photo_id=$(echo "$url" | grep -oP '(?<=photos/)[^/]+' || echo "unknown") + + # Default to jpg for Unsplash images + ext="jpg" + + echo "Downloading: $photo_id" + + # Download the image with wget (quiet mode) + if wget -q -O "${photo_id}.${ext}" "$url"; then + echo "✓ Saved image: ${photo_id}.${ext}" + else + echo "✗ Failed to download: $url" + continue + fi + + # Save attribution to text file + echo "$attribution" > "${photo_id}.txt" + echo "✓ Saved attribution: ${photo_id}.txt" + echo "" +done + +echo "Download complete!" diff --git a/side-quests/smol-nextflow/bin/inspect_model.py b/side-quests/smol-nextflow/bin/inspect_model.py new file mode 100755 index 000000000..3a50c728a --- /dev/null +++ b/side-quests/smol-nextflow/bin/inspect_model.py @@ -0,0 +1,77 @@ +#!/usr/bin/env -S uv run +# /// script +# dependencies = [ +# "torch>=2.0.0", +# "numpy>=1.24.0", +# ] +# /// +"""Inspect MetaCLIP model checkpoint metadata.""" + +import argparse +from pathlib import Path +import torch + +def inspect_checkpoint(checkpoint_path: Path): + """Load and inspect a PyTorch checkpoint file.""" + + print(f"Loading checkpoint: {checkpoint_path}") + print(f"File size: {checkpoint_path.stat().st_size / (1024**2):.2f} MB\n") + + # Load the checkpoint + checkpoint = torch.load(checkpoint_path, map_location='cpu', weights_only=False) + + # Check what type of object it is + print(f"Checkpoint type: {type(checkpoint)}") + print() + + # If it's a dict, explore its keys + if isinstance(checkpoint, dict): + print("Top-level keys:") + for key in checkpoint.keys(): + print(f" - {key}") + print() + + # Look for common metadata keys + metadata_keys = ['epoch', 'metadata', 'config', 'args', 'model_config', + 'train_config', 'version', 'architecture'] + + print("Metadata found:") + for key in metadata_keys: + if key in checkpoint: + print(f"\n{key}:") + print(f" {checkpoint[key]}") + + # Check state_dict structure + if 'state_dict' in checkpoint: + state_dict = checkpoint['state_dict'] + print(f"\nstate_dict contains {len(state_dict)} tensors") + print("First 10 tensor names:") + for i, name in enumerate(list(state_dict.keys())[:10]): + shape = state_dict[name].shape + print(f" {name}: {shape}") + + # If checkpoint IS the state_dict directly + elif all(isinstance(v, torch.Tensor) for v in list(checkpoint.values())[:5]): + print(f"\nCheckpoint appears to be a state_dict with {len(checkpoint)} tensors") + print("First 10 tensor names:") + for i, (name, tensor) in enumerate(list(checkpoint.items())[:10]): + print(f" {name}: {tensor.shape}") + + else: + print(f"Checkpoint is not a dict, it's: {checkpoint}") + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Inspect PyTorch checkpoint metadata") + parser.add_argument( + 'checkpoint', + type=Path, + help='Path to .pt checkpoint file' + ) + + args = parser.parse_args() + + if not args.checkpoint.exists(): + print(f"Error: {args.checkpoint} does not exist") + exit(1) + + inspect_checkpoint(args.checkpoint) diff --git a/side-quests/smol-nextflow/environment.yml b/side-quests/smol-nextflow/environment.yml new file mode 100644 index 000000000..c7a564fdb --- /dev/null +++ b/side-quests/smol-nextflow/environment.yml @@ -0,0 +1,12 @@ +name: metaclip +channels: + - pytorch + - conda-forge + - defaults +dependencies: + - python=3.11 + - pytorch + - pillow + - pip + - pip: + - open-clip-torch>=2.20.0 diff --git a/side-quests/smol-nextflow/main.nf b/side-quests/smol-nextflow/main.nf new file mode 100644 index 000000000..b0a510fba --- /dev/null +++ b/side-quests/smol-nextflow/main.nf @@ -0,0 +1,48 @@ +// params.models = "https://dl.fbaipublicfiles.com/MMPT/metaclip/b32_400m.pt" +params.models = "data/models/b32_400m.pt" +params.width = 400 + +workflow { + images = channel.fromPath("data/pics/*.{png,gif,jpg}") + | map { img -> [[id: img.baseName], img] } + + models = channel.fromPath("data/models/b32_400m.pt") + | map { model -> [[modelId: model.baseName], model] } + + images + | combine(models) + | map { imgMeta, img, modelMeta, model -> [imgMeta + modelMeta, model, img]} + | Classify + | view + + return + + Resize(images, params.width) + | view + +} + +process Resize { + input: + tuple val(meta), path(img) + val(width) + output: tuple val(meta), path("resized.png") + script: "convert $img -resize ${width}x resized.png" +} + +process Classify { + memory '8G' + input: tuple val(meta), path(model), path(pic) + output: tuple val(meta), path('summary.json') + script: "classify.py --model-path $model --image-dir . --json > summary.json" +} + +process Measure { + input: path(img) + output: tuple val(img.baseName), path('dimensions.txt') + script: + """ + identify -format "%wx%h" "$img" > dimensions.txt + """ +} + diff --git a/side-quests/smol-nextflow/outline.md b/side-quests/smol-nextflow/outline.md new file mode 100644 index 000000000..8a128d390 --- /dev/null +++ b/side-quests/smol-nextflow/outline.md @@ -0,0 +1,249 @@ +# Outline + +What are the Nextflow concepts being introduced at each stage? + +## Introduction + +Nextflow offers you a way to iterate over a collection of files, so let's grab some files to iterate over. We're going to write a workflow which produces a gallery of cute and not-so-cute cats. First things we're going to need are some cats. +I've created a small script that pull from the Cat-As-A-Service API to give us some random cats. + +```bash +bin/cat_me.sh --help +``` + +To start, lets grab 4 cats. By default, the script will save the images to `./data/pics`: + +```bash +bin/cat_me.sh --count 4 --prefix data/pics +``` + +Now lets iterate over those images in Nextflow. To start, we'll just create a channel of those images. We're not gong to do anything with them, but to make sure that everything is working, we connect the channel to the `view` operator which takes the things in the channel (files in our case) and prints a String representation of those things to the command line: + +```nextflow +workflow { + channel.fromPath("data/pics/*.{png,gif,jpg}") + | view +} +``` + +## Channels + +Let's try actually doing something with those images. We'll start with something simple - resizing images. We'll need to download some software to do so. Eventually, we'll talk about containers and reproducibility, but just to start, let's download the software to our machine: + +```bash +sudo apt update +sudo apt-get install -Y imagemagick +``` + +We'll create a new "process" for our resize operation. You can think of these processes as templates, or a classes. We'll connect the process to a channel and fire of a new "task" for each thing in the channel. + +In our Resize process definition (indeed in most process definitions), there are three blocks - input, output, and script. We connect these processes by channels and these blocks describe what we expect to get, what we expect to emit, and the work we want to do in-between. + +```nextflow +workflow { + images = channel.fromPath("data/pics/*.{png,gif,jpg}") + + images + | Resize + | view +} + +process Resize { + input: path(img) + output: path("resized-*") + script: "convert ${img} -resize 400x resized-${img}" +} +``` + +### Input +The input block describes what we expect to take from the input channel. The "things" in the channel can be have a type. The most common "types" are +- `val` (values Strings, integers, Maps, complex obects), +- `path` (paths like directories, files), or +- `tuple` (a collection of values and paths). + +We'll see more of those later, but for the moment, the channel we created in the `workflow` block is a channel of files, so in our process definition, we'll say "I'm gong to supply a channel of paths to this process, and as our process takes thinsg from that channel to spawn a new process, we'll call the thing in channel `img`. + +### Output +The output block describes what we want to emit into the process' output channel. Again, we can describe the "type" of thing emitted - `val`, `path`, `tuple` and others. For now, we'll promise to produce a file (or directory) that matches the glob pattern `reseized-*`. + +### Script +The script block describes what work we want to do on each of the things in the channel - how we're going to transform each of the things we pull from the input channel into the files or values we promised to emit into the outupt channel. + +By default, the script block will be rendered into a bash script, but you can use any interpreted language that makes sese to you - python, ruby, R, zsh, closure, whatever. In this introductory workshop, we'll stick with the default bash. + +We run the "convert" command from imagemagick which performs many types of manipulation. In our case, we'll use the `-resize` argument to resize the image to a width of 400 pixels. We also supply an output filename. You'll notice that we use the `${img}` varliable twice in our script block. This `${img}` is the varibale we defined in the input block. For each iteration of our process (each task), the variable will be the path to our individual image. + +For example, if the "thing" in the channel is the image `kitten.jpg`, then when Nextflow creates a new Resize task for this file, it will "render" our script block into bash, replacing the `${img}` variables with the path to produce this valid bash: + +```bash +convert kitten.jpg -resize 400x resized-kitten.jpg +``` + +Now let's run our workflow! We'll iterate over all of the images in `data/pics` (relative to our current location) and produce a channel of resized pictures that we then pipe into the `view` operator to print the channel contents to stdout. + +## Investigate .command files + +TODO: Explain that each tasks is run in a separate directory.This is to ensure independence of each of the tasks - they can't interfere with each other. + +TODO: Show the contents of one of the task work directories. + +## Harmonization +One of the nice features of the `convert` utility is that it will also do file format conversion for us. It will infer the format from the extension of the final argument. For example, if we execute + +```bash +convert kitten.jpg -resize 400x resized-kitten.png +``` + +The `convert` utility will both resize the image and convert the jpg to png format. Let's say we want to ensure that downstream in our workflow, we'd like to ensure all images are in the png format. How might we modify our `script` block to replace the extension or pull out the file basename so that we can append the `.png` extension? + +If you're a bash wizard, you might know that if you have a variable `$myFile` with the path to our file, you can replace the extension with this arcane incantation: + +```bash +file=kitten.jpg +convert "$file" -resize 400x "${file%.*}.png" +``` + +Or perhaps you use the `basename` utility: + +```bash +convert "$file" -resize 400x "$(basename "$file" .${file##*.}).png" +``` + +I can never remember these syntax. Forgunately for me, when inside the script block the `img` variable is not a bash variable - it's a Nextflow variable, and Nextflow provides some convenience methods for operating on those path objects. +We can simply call `${img.baseName}` to retun the file base name. For example: + +```nextflow +process Resize { + input: path(img) + output: path("resized-*") + script: "convert ${img} -resize 400x resized-${img.baseName}.png" +} +``` + +## Parameters +What if we want to make our workflow a little more flexible. Let's pull out the width and expose it as a parameter to the user. + +```nextflow +process Resize { + input: path(img) + output: path("resized-*") + script: "convert $img -resize ${width}x resized-${img.baseName}.png" +} +``` + +Now we can run wth +```bash +nextflow run . --width 300 +``` + +This is great, but it's considered best practice (and we'll see why in a bit) to make the inputs to a process explicit. We can do this by adding a second channel as input: + +```nextflow +workflow { + images = channel.fromPath("data/pics/*.{png,gif,jpg}") + + Resize(images, params.width) + | view +} + +process Resize { + input: + path(img) + val(width) + output: path("resized-*") + script: "convert $img -resize ${width}x resized-${img.baseName}.png" +} +``` + +The params object still works in the same way: + +```bash +nextflow run . --width 500 +``` + +## Extracting an ID +Great, but I'd like a way of retaining the original IDs. + +TODO: Explain the map operator and explain closures + +```nextflow +workflow { + images = channel.fromPath("data/pics/*.{png,gif,jpg}") + | map { img -> [img.baseName, img] } + | view +} +``` + +Better - we have the id extracted as a String. What if we want to add other metadata later? Let's turn it into a Map + +```nextflow +workflow { + images = channel.fromPath("data/pics/*.{png,gif,jpg}") + | map { img -> [[id: img.baseName], img] } +} +``` + +Now we've change the "shape" of the items in the channel, so we'll update the downstream process: + +```nextflow +process Resize { + input: + tuple val(meta), path(img) + val(width) + output: path("resized/*") + script: "convert $img -resize ${width}x resized-${img.baseName}.png" +} +``` + +This is better, but now that we have that critical metadata being passed through the channels in our "meta" object, we can stop encoding the id in the filenames. We can also just pass the "meta" object through to the output channel so that the newly resized image stays locked with the meta. + +```nextflow +process Resize { + input: + tuple val(meta), path(img) + val(width) + output: tuple val(meta), path("resized.png") + script: "convert $img -resize ${width}x resized.png" +} +``` + +Run the workflow and view the output: + +```bash +nextflow run . +``` + +## Classification + +We have a little classification script - bin/classify.py, which you can run with: + +```bash +bin/classify.py --help +``` + +``` +usage: classify.py [-h] [--image-dir IMAGE_DIR] [--model-path MODEL_PATH] [--labels LABELS [LABELS ...]] [--json] [--architecture {ViT-B-32-quickgelu,ViT-B-16-quickgelu,ViT-L-14-quickgelu,ViT-H-14-quickgelu}] + +Classify images using MetaCLIP + +options: + -h, --help show this help message and exit + --image-dir IMAGE_DIR + Directory containing images to classify (default: data/pics) + --model-path MODEL_PATH + Path to MetaCLIP model weights (default: data/models/b32_400m.pt) + --labels LABELS [LABELS ...] + Labels for classification (default: ["cute cat", "ugly cat"]) + --json Output results as JSONL (one JSON object per line) to stdout + --architecture {ViT-B-32-quickgelu,ViT-B-16-quickgelu,ViT-L-14-quickgelu,ViT-H-14-quickgelu} + Model architecture (auto-detected from filename if not specified) +``` + +The script takes images, a model, and a set of labels and classifies each of the images according to the labels. To run the script outside of Nextflow, we'll need to download one of the models. Do so with: + +```bash +cd data/models +wget https://dl.fbaipublicfiles.com/MMPT/metaclip/b32_400m.pt +cd ../../ +``` + From 02018f960b7418cc128ca1dc9f0466afd3e0c616 Mon Sep 17 00:00:00 2001 From: Rob Syme Date: Tue, 18 Nov 2025 05:36:10 +0800 Subject: [PATCH 02/22] First draft of Small Nextflow --- docs/index.md | 12 + docs/small_nextflow/00_orientation.md | 73 +++ docs/small_nextflow/01_fundamentals.md | 438 ++++++++++++++++ docs/small_nextflow/02_data_transformation.md | 470 ++++++++++++++++++ .../03_publishing_portability.md | 381 ++++++++++++++ docs/small_nextflow/04_advanced.md | 194 ++++++++ docs/small_nextflow/index.md | 57 +++ docs/small_nextflow/next_steps.md | 74 +++ docs/small_nextflow/survey.md | 7 + mkdocs.yml | 9 + small_nextflow/.stuff/cat_me.sh | 114 +++++ small_nextflow/.stuff/classify.py | 137 +++++ small_nextflow/.stuff/pyproject.toml | 9 + 13 files changed, 1975 insertions(+) create mode 100644 docs/small_nextflow/00_orientation.md create mode 100644 docs/small_nextflow/01_fundamentals.md create mode 100644 docs/small_nextflow/02_data_transformation.md create mode 100644 docs/small_nextflow/03_publishing_portability.md create mode 100644 docs/small_nextflow/04_advanced.md create mode 100644 docs/small_nextflow/index.md create mode 100644 docs/small_nextflow/next_steps.md create mode 100644 docs/small_nextflow/survey.md create mode 100755 small_nextflow/.stuff/cat_me.sh create mode 100755 small_nextflow/.stuff/classify.py create mode 100644 small_nextflow/.stuff/pyproject.toml diff --git a/docs/index.md b/docs/index.md index 21cc4ba42..d10668065 100644 --- a/docs/index.md +++ b/docs/index.md @@ -64,6 +64,18 @@ These are foundational, domain-agnostic courses intended for those who are compl [Start the Nextflow Run training :material-arrow-right:](nextflow_run/index.md){ .md-button .md-button--primary } +!!! exercise "Small Nextflow" + + !!! tip inline end "" + + :material-cat:{.nextflow-primary} Build a complete workflow from scratch. + + This is a hands-on workshop where you build a real-world image classification workflow from the ground up. You'll learn Nextflow fundamentals by creating channels, defining processes, working with operators, and making your workflow reproducible and portable. Perfect for learners who prefer building something concrete while learning core concepts. + + The course is calibrated to take a half day to cover in group trainings. + + [Start the Small Nextflow workshop :material-arrow-right:](small_nextflow/index.md){ .md-button .md-button--primary } + !!! exercise "Hello nf-core" !!! tip inline end "" diff --git a/docs/small_nextflow/00_orientation.md b/docs/small_nextflow/00_orientation.md new file mode 100644 index 000000000..c35b4e7f3 --- /dev/null +++ b/docs/small_nextflow/00_orientation.md @@ -0,0 +1,73 @@ +# Orientation + +This orientation assumes you have already opened the training environment by clicking on the "Open in GitHub Codespaces" button. +If not, please do so now, ideally in a second browser window or tab so you can refer back to these instructions. + +[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/nextflow-io/training?quickstart=1&ref=master) + +## GitHub Codespaces + +The GitHub Codespaces environment contains all the software, code and data necessary to work through this training course, so you don't need to install anything yourself. +However, you do need a (free) GitHub account to log in, and you should take a few minutes to familiarize yourself with the interface. + +If you have not yet done so, please go through the [Environment Setup](../../envsetup/) mini-course before going any further. + +## Working directory + +Throughout this training course, we'll be working in the `small_nextflow/` directory. + +Change directory now by running this command in the terminal: + +```bash +cd small_nextflow/ +``` + +!!!tip + + If for whatever reason you move out of this directory, you can always use the full path to return to it, assuming you're running this within the GitHub Codespaces training environment: + + ```bash + cd /workspaces/training/small_nextflow + ``` + +Now let's have a look at the contents of this directory. + +## Materials provided + +You can explore the contents of this directory by using the file explorer on the left-hand side of the training workspace. +Alternatively, you can use the `ls` command. + +Here we list the contents of the directory: + +```bash +ls -la +``` + +If you run this inside `small_nextflow`, you should see a minimal directory structure: + +```console title="Directory contents" +. +├── .stuff/ +│ ├── cat_me.sh +│ ├── classify.py +│ └── pyproject.toml +└── main.nf +``` + +**Here's a summary of what you should know to get started:** + +- **The `.stuff/` directory** contains helper scripts and configuration files we'll use throughout the workshop. + You can think of this as a toolbox we'll pull from as we build our workflow. + +- **The file `main.nf`** is where we'll write our Nextflow workflow. + It starts nearly empty, and we'll build it up step by step. + +- **The `cat_me.sh` script** fetches random cat images from an API for our workflow to process. + +- **The `classify.py` script** is a Python program that uses machine learning to classify images. + +- **The `pyproject.toml` file** describes the Python dependencies needed for the classification script. + +Throughout this workshop, we'll start with this minimal setup and progressively build a complete image classification workflow. + +**Now, to begin the course, click on the arrow in the bottom right corner of this page.** diff --git a/docs/small_nextflow/01_fundamentals.md b/docs/small_nextflow/01_fundamentals.md new file mode 100644 index 000000000..2c608821d --- /dev/null +++ b/docs/small_nextflow/01_fundamentals.md @@ -0,0 +1,438 @@ +# Part 1: Fundamentals + +In this first part, we'll learn the building blocks of Nextflow by creating channels, defining our first process, and working with parameters and metadata. +We're going to build a workflow that produces a gallery of classified cat images, starting from the very beginning. + +--- + +## 1. Introduction + +Nextflow offers you a way to iterate over a collection of files, so let's grab some files to iterate over. +We're going to write a workflow which produces a gallery of good and bad cats. +First things we're going to need are some cats. + +We're starting with a (nearly) empty directory. +There is a `.stuff` directory that contains some bits and pieces to help us along during the workshop, but you can imagine that we're essentially starting from scratch. + +### 1.1. Fetch some cat images + +The first thing we're going to need is some data. +I've created a small script that pulls from the Cat-As-A-Service API to give us some random cats. + +Let's see what the script can do: + +```bash +.stuff/cat_me.sh --help +``` + +To start, let's grab 4 cats. +By default, the script will save the images to `./data/pics`: + +```bash +.stuff/cat_me.sh --count 4 --prefix data/pics +``` + +This will generate some example data. +It will look something like this: + +```console title="Directory structure" +data +└── pics + ├── 5n4MTAC6ld0bVeCe.jpg + ├── 5n4MTAC6ld0bVeCe.txt + ├── IOSNx33kkgkiPfaP.jpg + ├── IOSNx33kkgkiPfaP.txt + ├── IRuDgdPJZFA39dyf.jpg + ├── IRuDgdPJZFA39dyf.txt + ├── uq5KqqiF0qpgTQVA.jpg + └── uq5KqqiF0qpgTQVA.txt +``` + +### 1.2. Create a channel of images + +Now let's iterate over those images in Nextflow. +To start, we'll just create a channel of those images. +We're not going to do anything with them, but to make sure that everything is working, we connect the channel to the `view` operator which takes the things in the channel (files in our case) and prints a String representation of those things to the command line. + +Open `main.nf` and add the following: + +```groovy title="main.nf" linenums="1" +#!/usr/bin/env nextflow + +workflow { + channel.fromPath("data/pics/*.{png,gif,jpg}") + | view +} +``` + +Now run the workflow: + +```bash +nextflow run main.nf +``` + +You should see output showing the paths to your cat images. + +!!! tip "Glob patterns" + + The `{png,gif,jpg}` syntax is called brace expansion. + It's a glob pattern that matches any file ending in `.png`, `.gif`, or `.jpg`. + This is equivalent to writing three separate patterns: `*.png`, `*.gif`, and `*.jpg`. + Using brace expansion keeps our code concise when we need to match multiple file extensions. + +### Takeaway + +You now know how to create a channel from files using glob patterns and view its contents. + +### What's next? + +Let's actually do something with those images by creating our first process. + +--- + +## 2. Channels and processes + +Let's try actually doing something with those images. +We'll start with something simple - resizing images. +We'll need to download some software to do so. +Eventually, we'll talk about containers and reproducibility, but just to start, let's download the software to our machine: + +```bash +sudo apt update +sudo apt-get install -y imagemagick +``` + +### 2.1. Understanding process structure + +We'll create a new "process" for our resize operation. +You can think of these processes as templates, or classes. +We'll connect the process to a channel and fire off a new "task" for each thing in the channel. + +In our Resize process definition (indeed in most process definitions), there are three blocks - `input`, `output`, and `script`. +We connect these processes by channels and these blocks describe what we expect to get, what we expect to emit, and the work we want to do in-between. + +### 2.2. Create the Resize process + +Update your `main.nf` to add a Resize process: + +```groovy title="main.nf" linenums="1" +#!/usr/bin/env nextflow + +workflow { + images = channel.fromPath("data/pics/*.{png,gif,jpg}") + + images + | Resize + | view +} + +process Resize { + input: path(img) + output: path("resized-*") + script: "magick ${img} -resize 400x resized-${img.baseName}.png" +} +``` + +### 2.3. Understanding the input block + +The input block describes what we expect to take from the input channel. +The "things" in the channel can have a type. +The most common "types" are: + +- `val` (values like Strings, integers, Maps, complex objects), +- `path` (paths like directories or files), or +- `tuple` (a collection of values and paths). + +We'll see more of those later, but for the moment, the channel we created in the `workflow` block is a channel of files, so in our process definition, we'll say "I'm going to supply a channel of paths to this process, and as our process takes things from that channel to spawn a new task, we'll call the thing in the channel `img`." + +### 2.4. Understanding the output block + +The output block describes what we want to emit into the process' output channel. +Again, we can describe the "type" of thing emitted - `val`, `path`, `tuple` and others. +For now, we'll promise to produce a file (or directory) that matches the glob pattern `resized-*`. + +### 2.5. Understanding the script block + +The script block describes what work we want to do on each of the things in the channel - how we're going to transform each of the things we pull from the input channel into the files or values we promised to emit into the output channel. + +By default, the script block will be rendered into a bash script, but you can use any interpreted language that makes sense to you - python, ruby, R, zsh, closure, whatever. +In this introductory workshop, we'll stick with the default bash. + +We run the `magick` command from imagemagick which performs many types of manipulation. +In our case, we'll use the `-resize` argument to resize the image to a width of 400 pixels. +We also supply an output filename. +You'll notice that we use the `${img}` variable twice in our script block. +This `${img}` is the variable we defined in the input block. +For each iteration of our process (each task), the variable will be the path to our individual image. + +For example, if the "thing" in the channel is the image `kitten.jpg`, then when Nextflow creates a new Resize task for this file, it will "render" our script block into bash, replacing the `${img}` variables with the path to produce this valid bash: + +```bash +magick kitten.jpg -resize 400x resized-kitten.jpg +``` + +### 2.6. Run the workflow + +Now let's run our workflow! +We'll iterate over all of the images in `data/pics` (relative to our current location) and produce a channel of resized pictures that we then pipe into the `view` operator to print the channel contents to stdout. + +```bash +nextflow run main.nf +``` + +### Takeaway + +You now understand the three-part structure of a Nextflow process: input, output, and script blocks work together to transform data flowing through channels. + +### What's next? + +Let's explore how Nextflow executes each task in isolation. + +--- + +## 3. Investigate task execution + +TODO: Explain that each task is run in a separate directory. This is to ensure independence of each of the tasks - they can't interfere with each other. + +TODO: Show the contents of one of the task work directories. + +### Takeaway + +Each task runs in its own isolated directory to prevent interference between parallel executions. + +### What's next? + +Let's learn some convenient methods for working with file paths. + +--- + +## 4. Harmonization + +One of the nice features of the `magick` utility is that it will also do file format conversion for us. +It will infer the format from the extension of the final argument. +For example, if we execute: + +```bash +magick kitten.jpg -resize 400x resized-kitten.png +``` + +The `magick` utility will both resize the image and convert the jpg to png format. +Let's say we want to ensure that downstream in our workflow, we'd like to ensure all images are in the png format. +How might we modify our `script` block to replace the extension or pull out the file basename so that we can append the `.png` extension? + +### 4.1. The bash way (harder) + +If you're a bash wizard, you might know that if you have a variable `$myFile` with the path to our file, you can replace the extension with this arcane incantation: + +```bash +file=kitten.jpg +magick "$file" -resize 400x "${file%.*}.png" +``` + +Or perhaps you use the `basename` utility: + +```bash +magick "$file" -resize 400x "$(basename "$file" .${file##*.}).png" +``` + +I love bash, but it's easy to forget this syntax or mistype it. + +### 4.2. The Nextflow way (easier) + +Fortunately for us, when inside the script block the `img` variable is not a bash variable - it's a Nextflow variable, and Nextflow provides some convenience methods for operating on those path objects. +The full list is available in the [Nextflow stdlib documentation](https://www.nextflow.io/docs/latest/reference/stdlib-types.html#stdlib-types-path), but one handy method is `baseName`. + +We can simply call `${img.baseName}` to return the file base name. +For example: + +```groovy title="main.nf" hl_lines="14" linenums="1" +#!/usr/bin/env nextflow + +workflow { + images = channel.fromPath("data/pics/*.{png,gif,jpg}") + + images + | Resize + | view +} + +process Resize { + input: path(img) + output: path("resized-*") + script: "magick ${img} -resize 400x resized-${img.baseName}.png" +} +``` + +Update your workflow with this change and run it again: + +```bash +nextflow run main.nf +``` + +### Takeaway + +Nextflow path objects provide convenient methods like `baseName` that make file manipulation easier than bash string operations. + +### What's next? + +Let's make our workflow more flexible by adding parameters. + +--- + +## 5. Parameters + +What if we want to make our workflow a little more flexible? +Let's pull out the width and expose it as a parameter to the user. + +### 5.1. Using parameters directly in the script + +We could reference a parameter directly in the script block: + +```groovy title="main.nf" hl_lines="14" linenums="1" +#!/usr/bin/env nextflow + +workflow { + images = channel.fromPath("data/pics/*.{png,gif,jpg}") + + images + | Resize + | view +} + +process Resize { + input: path(img) + output: path("resized-*") + script: "magick $img -resize ${params.width}x resized-${img.baseName}.png" +} +``` + +Now we can run with: + +```bash +nextflow run main.nf --width 300 +``` + +### 5.2. Making inputs explicit (best practice) + +This works, but it's considered best practice (and we'll see why in a bit) to make the inputs to a process explicit. +We can do this by adding a second input channel: + +```groovy title="main.nf" hl_lines="8 16-17" linenums="1" +#!/usr/bin/env nextflow + +workflow { + images = channel.fromPath("data/pics/*.{png,gif,jpg}") + + Resize(images, params.width) + | view +} + +process Resize { + input: + path(img) + val(width) + output: path("resized-*") + script: "magick $img -resize ${width}x resized-${img.baseName}.png" +} +``` + +The params object still works in the same way: + +```bash +nextflow run main.nf --width 500 +``` + +### Takeaway + +Exposing parameters as explicit process inputs makes workflows more flexible and clearer about their dependencies. + +### What's next? + +Let's learn how to attach metadata to our files using tuples and maps. + +--- + +## 6. Extracting an ID + +Great, but I'd like a way of retaining the original IDs. + +### 6.1. Understanding the map operator + +The `map` operator is one of the most powerful tools in Nextflow. +It takes a collection of items in a channel and transforms them into a new collection of items. +The transformation is defined by a closure - a small piece of code that is evaluated "later" - during workflow execution. +Each item in the new channel is the result of applying the closure to the corresponding item in the original channel. + +A closure is written as `{ input -> output }` where you define how to transform the input into the output. + +Let's use `map` to extract the ID from each filename: + +```groovy title="main.nf" hl_lines="5-6" linenums="1" +#!/usr/bin/env nextflow + +workflow { + images = channel.fromPath("data/pics/*.{png,gif,jpg}") + | map { img -> [img.baseName, img] } + | view +} +``` + +Run this to see the structure: + +```bash +nextflow run main.nf +``` + +### 6.2. Using a metadata map + +Better - we have the id extracted as a String. +What if we want to add other metadata later? +Let's turn it into a Map: + +```groovy title="main.nf" hl_lines="5" linenums="1" +#!/usr/bin/env nextflow + +workflow { + images = channel.fromPath("data/pics/*.{png,gif,jpg}") + | map { img -> [[id: img.baseName], img] } + | view +} +``` + +### 6.3. Update the process to handle tuples + +Now we've changed the "shape" of the items in the channel, so we'll update the downstream process: + +```groovy title="main.nf" hl_lines="8 13" linenums="1" +#!/usr/bin/env nextflow + +workflow { + images = channel.fromPath("data/pics/*.{png,gif,jpg}") + | map { img -> [[id: img.baseName], img] } + + Resize(images, params.width) + | view +} + +process Resize { + input: + tuple val(meta), path(img) + val(width) + output: path("resized-*") + script: "magick $img -resize ${width}x resized-${img.baseName}.png" +} +``` + +Run the workflow and view the output: + +```bash +nextflow run main.nf +``` + +### Takeaway + +Using tuples with metadata maps allows you to carry important information alongside your files as they flow through the workflow. + +### What's next? + +Now that we understand the fundamentals, let's build a more complex workflow with classification and grouping operations. diff --git a/docs/small_nextflow/02_data_transformation.md b/docs/small_nextflow/02_data_transformation.md new file mode 100644 index 000000000..0c71c2b38 --- /dev/null +++ b/docs/small_nextflow/02_data_transformation.md @@ -0,0 +1,470 @@ +# Part 2: Data Transformation & Analysis + +In this part, we'll build a multi-step workflow that classifies images using machine learning, manages computational resources, and groups results intelligently. + +--- + +## 1. Classification + +Let's get to the fun part - the cat sorting! +We have a little classification script - `classify.py` that I've provided in the `.stuff` directory. +In your research sometimes you have small accessory scripts that are useful for your pipelines. +We're using a python script here in this workshop example, but this pattern will hold for scripts written in perl, ruby, R, python, closurescript, or any of the other interpreted languages. + +### 7.1. Set up the classification script + +Let's pull the file out into a new `bin` directory: + +```bash +mkdir -p bin +cp .stuff/classify.py bin/ +``` + +The script requires some dependencies. +Again, we'll do this the slow/painful way one time before we demonstrate how to use containers to encapsulate the software dependencies. + +We'll grab one more file from our `.stuff` directory - a `pyproject.toml` file which is a way of describing software dependencies for Python projects. +This is unrelated to Nextflow, but an example of one of the (many) ways in which different languages and frameworks might install software. + +You can install the dependencies and activate the environment with: + +```bash +cp .stuff/pyproject.toml . +uv sync +source .venv/bin/activate +``` + +which you can run with: + +```bash +bin/classify.py --help +``` + +```console title="Output" +usage: classify.py [-h] [--model-path MODEL_PATH] [--labels LABELS [LABELS ...]] [--json] image + +Classify a single image using MetaCLIP + +positional arguments: + image Path to the image file to classify + +options: + -h, --help show this help message and exit + --model-path MODEL_PATH + Path to MetaCLIP model weights (default: data/models/b32_400m.pt) + --labels LABELS [LABELS ...] + Labels for classification (default: ["good cat", "bad cat"]) + --json Output result as JSON to stdout + --architecture {ViT-B-32-quickgelu,ViT-B-16-quickgelu,ViT-L-14-quickgelu,ViT-H-14-quickgelu} + Model architecture (auto-detected from filename if not specified) +``` + +### 7.2. Download the classification model + +The script takes images, a model, and a set of labels and classifies each of the images according to the labels. +To run the script outside of Nextflow, we'll need to download one of the models. +Do so with: + +```bash +mkdir -p data/models +(cd data/models && wget https://dl.fbaipublicfiles.com/MMPT/metaclip/b32_400m.pt) +``` + +### 7.3. Create the Classify process + +Now let's create a `Classify` process that will take two channels - one channel of images and one channel that supplies the model: + +```groovy title="Process definition" linenums="1" +process Classify { + input: + tuple val(meta), path(img) + path(model) + output: tuple val(meta), stdout + script: "classify.py --model-path $model ${img}" +} +``` + +Note here that we're calling the `classify.py` script directly, even though we can't do that from the command line (we had to provide the relative or absolute path). +This is because Nextflow automatically adds the `bin` directory (relative to the main.nf) to the `$PATH` for all Nextflow tasks. +This is a very convenient way to bundle accessory scripts and snippets with your workflow. + +### 7.4. Understanding queue vs. value channels + +Processes can have multiple channels as input or as output. +A process will continue to emit tasks as long as it can pull an item from each of the input channels. +We could create a new channel for the model, and define a sensible default: + +```groovy title="Workflow with model channel" linenums="1" +params.model = "${projectDir}/data/models/b32_400m.pt" + +workflow { + images = channel.fromPath("data/pics/*.{png,gif,jpg}") + | map { img -> [[id: img.baseName], img] } + + model_channel = channel.fromPath(params.model) + Classify(images, model_channel) +} +``` + +What happens when you run the workflow? +Given what we know about channels, what might be happening? + +**Answer:** The Classify process only spawns a single task. +This is because after pulling the model path from the second input channel on the first iteration, the channel is empty, so no more Classify tasks can be submitted for execution. + +There are two types of channel in Nextflow - **queue channels** and **value channels**. +Queue channels are exhaustible - they have a set number of items in the channel and each process can only take each item in the channel once. +The second type of channel is a value channel, which is a channel of only a single item. +This item is emitted without exhaustion. + +### 7.5. Using value channels + +There are some operators which will always return a value channel. +Examples are `first`, `collect`, `count`, etc. + +We could also create a value channel using the `channel.value` factory: + +```groovy title="Using channel.value" linenums="1" +params.model = "${projectDir}/data/models/b32_400m.pt" + +workflow { + images = channel.fromPath("data/pics/*.{png,gif,jpg}") + | map { img -> [[id: img.baseName], img] } + + model_channel = channel.value(file(params.model)) + Classify(images, model_channel) +} +``` + +Note here that we're wrapping the params.model value (a String) in the `file()` function, which turns an ordinary String into an object that Nextflow can use as a path. +We've not needed to use this until now because the `channel.fromPath` factory necessarily returns paths, so it automatically does this conversion for us. + +### 7.6. Implicit value channels + +An even simpler solution is to provide the path object directly when calling the process. +Any non-channel object will automatically be converted into a value channel for you: + +```groovy title="main.nf" hl_lines="8" linenums="1" +#!/usr/bin/env nextflow + +params.model = "${projectDir}/data/models/b32_400m.pt" + +workflow { + images = channel.fromPath("data/pics/*.{png,gif,jpg}") + | map { img -> [[id: img.baseName], img] } + + Resize(images, params.width) + + Classify(images, file(params.model)) + | view +} +``` + +Add the Classify process definition to your workflow and run it: + +```bash +nextflow run main.nf +``` + +You might find that the process errors out with a 137 exit code. +This generally means that we've run out of RAM because we're running too many of these classification jobs at the same time. +Let's talk about how we tell Nextflow that a particular process requires more resources. + +### Takeaway + +Understanding queue channels vs. value channels is crucial for controlling how data flows through multi-input processes. + +### What's next? + +Let's learn how to manage computational resources for our processes. + +--- + +## 8. Resources + +Our processes are currently composed of the `input:`, `output:`, and `script:` blocks. +In addition to these blocks, processes can use "process directives" which are optional annotations which modify the behaviour of the processes. +There are many directives ([documentation](https://www.nextflow.io/docs/latest/reference/process.html#directives)), but we can introduce the concept with two important process directives - `memory` and `cpus`. + +### 8.1. Understanding executors + +So far, we've been using the local executor to run Nextflow - running on the local machine. +There are many other executors targeting different backends, from HPC executors like SLURM and PBS to cloud executors like AWS Batch, Google Batch, and Azure Batch. +There are more than a dozen supported executors ([documentation](https://www.nextflow.io/docs/latest/executor.html)). + +Each of these have a concept of the resources a particular task will require - resources such as cpus, memory, gpus, disk, etc. + +### 8.2. Resource defaults and management + +If not otherwise specified, the defaults are to request 1 cpu, 1 GB of RAM and 0 GPUs for each task. + +When using the local executor, Nextflow scans the machine it is running on and determines how many cpus and how much RAM the system has. +It will ensure that (given the resources specified or defaults applied) the running tasks never exceed the available limits. +If the system has 16 GB of RAM, for example, and a particular process requires 6 GB of ram, Nextflow will ensure that _at most_ 2 of those tasks are running at any one time. +As a task finishes, Nextflow begins the next task in line. + +### 8.3. Add resource directives + +Update your Classify process to request more memory: + +```groovy title="Process with memory directive" hl_lines="2" linenums="1" +process Classify { + memory '13 GB' + + input: + tuple val(meta), path(img) + path(model) + output: tuple val(meta), stdout + script: "classify.py --model-path $model ${img}" +} +``` + +Now run the workflow again: + +```bash +nextflow run main.nf +``` + +### Takeaway + +Process directives like `memory` and `cpus` communicate resource requirements to Nextflow executors, enabling proper scheduling and preventing resource exhaustion. + +### What's next? + +Let's learn how to combine related data using the join and groupTuple operators. + +--- + +## 9. Grouping + +Now we want to combine our classification results with our resized images. +We can use the `join` operator, which finds pairs of items (one from each channel) that share a key. +By default, the `join` operator will use the first element of each item in the channel as the key. +In our case, that first item was the image metadata, which occupies the first position in both the Classify process output and the Resize process output. + +### 9.1. Join classification results with images + +Update your workflow to join the channels: + +```groovy title="Workflow with join" hl_lines="12-14" linenums="1" +#!/usr/bin/env nextflow + +params.model = "${projectDir}/data/models/b32_400m.pt" + +workflow { + images = channel.fromPath("data/pics/*.{png,gif,jpg}") + | map { img -> [[id: img.baseName], img] } + + Resize(images, params.width) + + Classify(images, file(params.model)) + | join(Resize.out) + | view +} +``` + +This produces a channel like: + +``` +[metadata, label, img] +[metadata, label, img] +[metadata, label, img] +[metadata, label, img] +``` + +### 9.2. Group items by label + +In order to make a picture of just the good cats and a second picture of just the bad cats, we'll need to group the items in the channel based on the label. +We can do this with the `groupTuple` operator. +Normally the groupTuple expects that the grouping key will be the first element in each item in the channel. +In our case, it is the second item, i.e. index "1" if the first item is index "0". +To ask Nextflow to group on the item with index 1, we add a `by: 1` argument to the operator: + +```groovy title="Workflow with grouping" hl_lines="13-15" linenums="1" +#!/usr/bin/env nextflow + +params.model = "${projectDir}/data/models/b32_400m.pt" + +workflow { + images = channel.fromPath("data/pics/*.{png,gif,jpg}") + | map { img -> [[id: img.baseName], img] } + + Resize(images, params.width) + + Classify(images, file(params.model)) + | join(Resize.out) + | groupTuple(by: 1) + | view +} +``` + +This produces a channel of the form: + +``` +[metadatas, label, images] +[metadatas, label, images] +``` + +### Takeaway + +The `join` and `groupTuple` operators allow you to match related items and collect them by common attributes. + +### What's next? + +Let's create visual collages for each group of classified images. + +--- + +## 10. Collage + +Let's create a `Collage` process that takes this channel and produces a collage of all of the images for each label. +The script block here is a little involved, but it uses ImageMagick's montage command to arrange images into a grid. + +### 10.1. Create the Collage process + +```groovy title="Collage process" linenums="1" +process Collage { + input: tuple val(metadatas), val(label), path("inputs/*.png") + output: tuple val(label), path("collage.png") + script: + """ + magick montage inputs/* \\ + -geometry +10+10 \\ + -background black \\ + +polaroid \\ + -background '#ffbe76' \\ + collage_nolabel.png + magick montage \\ + -pointsize 48 \\ + -label '$label' \\ + -geometry +0+0 \\ + -background "#f0932b" \\ + collage_nolabel.png collage.png + """ +} +``` + +### 10.2. Connect to the workflow + +We can then hook this into our channel chain: + +```groovy title="Workflow with Collage" hl_lines="13-15" linenums="1" +#!/usr/bin/env nextflow + +params.model = "${projectDir}/data/models/b32_400m.pt" + +workflow { + images = channel.fromPath("data/pics/*.{png,gif,jpg}") + | map { img -> [[id: img.baseName], img] } + + Resize(images, params.width) + + Classify(images, file(params.model)) + | join(Resize.out) + | groupTuple(by: 1) + | Collage + | view +} +``` + +### 10.3. Optimize with resized images + +Those collage tasks are taking a little too long, but that might be because we're collaging the original full-sized images and not our resized images. +Because the `images` channel and the output channel from the `Resize` process both have the same shape, we can simply replace them in the workflow: + +```groovy title="Optimized workflow" hl_lines="12" linenums="1" +#!/usr/bin/env nextflow + +params.model = "${projectDir}/data/models/b32_400m.pt" + +workflow { + images = channel.fromPath("data/pics/*.{png,gif,jpg}") + | map { img -> [[id: img.baseName], img] } + + Resize(images, params.width) + + Classify(images, file(params.model)) + | join(Resize.out) + | groupTuple(by: 1) + | Collage + | view +} +``` + +### 10.4. Combine all collages + +For our final process, let's combine these two collages together into a single final image. +We'll create a process that takes a collection of images (we don't care what they are called) and produces a final `collage_all.png` image: + +```groovy title="CombineImages process" linenums="1" +process CombineImages { + input: path "in.*.png" + output: path "collage_all.png" + script: + """ + magick montage \\ + -geometry +10+10 \\ + -quality 05 \\ + -background '#ffbe76' \\ + -border 5 \\ + -bordercolor '#f0932b' \\ + in.*.png \\ + collage_all.png + """ +} +``` + +### 10.5. Transform the channel + +The channel coming from the Collage process looks like: + +``` +[label, collageImage] +[label, collageImage] +``` + +but we need it to look like: + +``` +[collageImage, collageImage] +``` + +So we'll drop the labels and collect all images: + +```groovy title="Final workflow" hl_lines="14-18" linenums="1" +#!/usr/bin/env nextflow + +params.model = "${projectDir}/data/models/b32_400m.pt" + +workflow { + images = channel.fromPath("data/pics/*.{png,gif,jpg}") + | map { img -> [[id: img.baseName], img] } + + Resize(images, params.width) + + Classify(images, file(params.model)) + | join(Resize.out) + | groupTuple(by: 1) + | Collage + | map { _label, img -> img } + | collect + | CombineImages + | view +} +``` + +The `collect` operator takes all the items in a channel and then emits them as a single "wide" collection. + +Run the complete workflow: + +```bash +nextflow run main.nf +``` + +### Takeaway + +You can chain together multiple processes and operators to build sophisticated multi-step workflows that transform and aggregate data. + +### What's next? + +Now that we have a working workflow, let's learn how to publish the results in an organized way. diff --git a/docs/small_nextflow/03_publishing_portability.md b/docs/small_nextflow/03_publishing_portability.md new file mode 100644 index 000000000..d43a0ce91 --- /dev/null +++ b/docs/small_nextflow/03_publishing_portability.md @@ -0,0 +1,381 @@ +# Part 3: Publishing & Portability + +In this part, we'll make our workflow production-ready by publishing organized outputs, supporting multiple filesystems, and containerizing dependencies for reproducibility. + +--- + +## 1. Workflow outputs + +Great! We have a workflow that (arguably cruelly) collects our cats into "good" and "bad" groupings! +Unfortunately, the final output file is still deep in the work directory in a hostile-looking hash-addressed directory. +We'd like to define some final workflow outputs that should be published somewhere safe, outside of the work directory. + +### 11.1. Understanding workflow output blocks + +To define the workflow outputs, we'll need to define a `publish:` block in the workflow. +We'll also need to put the existing workflow in a `main:` block as shown below: + +```groovy title="Workflow with output blocks" linenums="1" +workflow { + main: + images = channel.fromPath("data/pics/*.{png,gif,jpg}") + | map { img -> [[id: img.baseName], img] } + + Resize(images, params.width) + + Classify(images, file(params.model)) + + Classify.out + | join(Resize.out) + | groupTuple(by: 1) + | Collage + | map { _label, img -> img } + | collect + | CombineImages + + publish: + // Something to go here +} +``` + +### 11.2. Publishing the collage + +In the `publish:` block, we define channels that we'd like to publish: + +```groovy title="Publishing output" hl_lines="19-20 23-27" linenums="1" +workflow { + main: + images = channel.fromPath("data/pics/*.{png,gif,jpg}") + | map { img -> [[id: img.baseName], img] } + + Resize(images, params.width) + + Classify(images, file(params.model)) + + Classify.out + | join(Resize.out) + | groupTuple(by: 1) + | Collage + | map { _label, img -> img } + | collect + | CombineImages + + publish: + collage = CombineImages.out +} + +output { + collage { + mode 'copy' + } +} +``` + +Now when we run, the final collage will be copied into `results/collage_all.png`. + +We can control the publication mechanism by adding arguments. +The `mode 'copy'` directive tells Nextflow to copy the output file rather than create a symlink (the default). + +### 11.3. Publishing the classifications + +The more interesting outputs might be those with more metadata associated with them. +For example, we might want to record the classification for each image ID. +To publish metadata-rich outputs, we'll first create a channel that is composed of Maps: + +```groovy title="Publishing with metadata" hl_lines="11-14 20 25-28" linenums="1" +workflow { + main: + images = channel.fromPath("data/pics/*.{png,gif,jpg}") + | map { img -> [[id: img.baseName], img] } + + Resize(images, params.width) + + Classify(images, file(params.model)) + + Classify.out + | join(Resize.out) + | map { meta, label, image -> meta + [label:label, image:image] } + | set { classifiedMaps } + + Classify.out + | join(Resize.out) + | groupTuple(by: 1) + | Collage + | map { _label, img -> img } + | collect + | CombineImages + + publish: + collage = CombineImages.out + classification = classifiedMaps +} + +output { + collage { + mode 'copy' + } + classified { + mode 'copy' + } +} +``` + +This will cause the resized images to also be published in the `results` directory, but it's looking a bit cluttered now: + +```console title="Results directory" +results +├── collage_all.png +├── resized-4skdDxHm4yDsSJIr.png +├── resized-4y6Hyu0uzVZcEx89.png +├── resized-6Nb0ipGrHDHqCEmZ.png +└── resized-wfMCf1lHc9YPw455.png +``` + +### 11.4. Organizing outputs with path directives + +Let's bring a little bit of order by organizing images into subdirectories by their label: + +```groovy title="Organized output" hl_lines="7-9" linenums="1" +output { + collage { + mode 'copy' + } + classified { + mode 'copy' + path { sample -> "images/${sample.label.replaceAll(/\s+/, '_')}" } + } +} +``` + +Now our results are organized: + +```console title="Organized results" +results +├── collage_all.png +└── images + ├── good_cat + │ ├── resized-4skdDxHm4yDsSJIr.png + │ └── resized-wfDuHvt6VIn2tM8T.png + └── bad_cat + ├── resized-6Nb0ipGrHDHqCEmZ.png + └── resized-wfMCf1lHc9YPw455.png +``` + +### 11.5. Creating index files + +Now we sanitized the label names so that they'd be in more sensibly named directories (no spaces, etc), but this risks corrupting that metadata. +Let's ask Nextflow to publish a more digestible samplesheet or "index" of the published outputs, that includes the real, unsanitized labels: + +```groovy title="Output with index" hl_lines="8-11" linenums="1" +output { + collage { + mode 'copy' + } + classified { + mode 'copy' + path { sample -> "images/${sample.label.replaceAll(/\s+/, '_')}" } + index { + header true + path 'images/cats.csv' + } + } +} +``` + +This produces a CSV at `results/images/cats.csv`. + +For more structured data, you can also choose YAML or JSON: + +```groovy title="JSON index" hl_lines="10" linenums="1" +output { + collage { + mode 'copy' + } + classified { + mode 'copy' + path { sample -> "images/${sample.label.replaceAll(/\s+/, '_')}" } + index { + header true + path 'images/cats.json' + } + } +} +``` + +Run the workflow to see the organized outputs: + +```bash +nextflow run main.nf +``` + +### Takeaway + +Nextflow's output publishing system allows you to organize results with custom paths and generate index files that preserve important metadata. + +### What's next? + +Let's make our workflow portable across different storage systems. + +--- + +## 12. Filesystem independence + +Nextflow speaks many different communication protocols, allowing you to seamlessly move from using data on a local or shared filesystem, to `http://`/`https://`, to object storage protocols like `s3://`, `az://`, `gs://` or even older `ftp://` protocols. +You can provide support for new protocols yourself via Nextflow's plugin system. + +### 12.1. Using remote files + +For example, our current workflow uses a local model file. +But we can easily switch to using a remote model from the web: + +```bash +nextflow run main.nf --model https://dl.fbaipublicfiles.com/MMPT/metaclip/b32_400m.pt +``` + +Nextflow will automatically download the file and make it available to the process. +This works for input files too - you could provide image URLs instead of local paths! + +### 12.2. Cloud storage support + +Similarly, if you're working in the cloud, you can use cloud storage URLs: + +```bash +# AWS S3 +nextflow run main.nf --model s3://my-bucket/models/b32_400m.pt + +# Google Cloud Storage +nextflow run main.nf --model gs://my-bucket/models/b32_400m.pt + +# Azure Blob Storage +nextflow run main.nf --model az://my-container/models/b32_400m.pt +``` + +!!! tip + + To use cloud storage protocols, you'll need to configure appropriate credentials for your cloud provider. + See the [Nextflow documentation](https://www.nextflow.io/docs/latest/amazons3.html) for details. + +### Takeaway + +Nextflow's protocol flexibility makes workflows portable across local, web, and cloud storage systems without code changes. + +### What's next? + +Let's containerize our workflow to ensure it runs reliably anywhere. + +--- + +## 13. Containerization + +All of our Nextflow tasks are currently using the software installed on the host operating system. +This practice can quickly become a problem for you for a number of reasons: + +- As the workflow grows, and the number of software dependency stacks will also likely grow, and it becomes increasingly likely that the installation of one piece of software accidentally updates a dependency of another piece of software. Similarly, you may end up with incompatible software dependency stacks. +- The analysis becomes tied to a very specific machine or infrastructure, difficult to reproduce in exactly the same way elsewhere (by yourself or by a colleague). +- Managing software is a thankless and boring task. + +### 13.1. Understanding containerization + +Containers are lightweight, standalone packages that include everything needed to run a piece of software: code, runtime, system tools, and libraries. +Docker is the most popular container technology, and it works by packaging your software and dependencies into an "image" that can run consistently anywhere Docker is installed. + +When you run a containerized task, Docker creates an isolated environment with exactly the software versions specified in the container image, completely independent of what's installed on the host system. +This ensures that your workflow produces identical results whether you run it on your laptop, an HPC cluster, or in the cloud. + +### 13.2. Container technologies in Nextflow + +Nextflow provides the opportunity to run each task in an isolated software environment, and can do so via a variety of technologies, including: + +- conda +- containers (docker, apptainer/singularity, charliecloud, sarus, shifter, and podman) +- spack + +Let's improve the reproducibility and portability of our workflow. + +### 13.3. Containerizing processes + +You'll remember that we manually installed software two different ways: + +- imagemagick (via `apt-get install`), and +- python packages (via `uv sync`) + +We could use a single container for all of the steps in the workflow, but this might limit the reusability of the containers, and upgrading one piece of software for one task would mean changing the container for all of the tasks. +Most researchers prefer (and Nextflow supports) defining a container per-process. + +To replace the imagemagick we installed via apt-get, we'll use the public container `minidocks/imagemagick:7`. + +We've already talked about the `memory` and `cpus` process directives, but another useful directive is the `container` directive. +We'll use this to add the container to our `Resize`, `Collage`, and `CombineImages` processes: + +```groovy title="Processes with containers" hl_lines="2 9 16" linenums="1" +process Resize { + container 'minidocks/imagemagick:7' + + input: + tuple val(meta), path(img) + val(width) + output: tuple val(meta), path("resized-*") + script: "magick ${img} -resize ${width}x resized-${img.baseName}.png" +} + +process Collage { + container 'minidocks/imagemagick:7' + // ... rest of process +} + +process CombineImages { + container 'minidocks/imagemagick:7' + // ... rest of process +} +``` + +### 13.4. Building custom containers + +Our `classify.py` process includes three specific python packages (torch, pillow, and openclip-torch) at specific versions. +It's unlikely that there is an existing container that provides these specific packages. +We could opt to build our own. + +There are a number of ways of building containers, but we'll use the [Seqera Containers](https://seqera.io/containers/) web interface. +You can add multiple packages and it will build a container for you. + +![Creating a new container using Seqera Containers](../assets/img/seqera-container-python-00.png) + +Once you have your container image, add it to the Classify process: + +```groovy title="Classify with custom container" hl_lines="2" linenums="1" +process Classify { + container 'your-registry/your-classify-container:latest' + memory '4 GB' + + input: + tuple val(meta), path(img) + path(model) + output: tuple val(meta), stdout + script: "classify.py --model-path $model ${img}" +} +``` + +### 13.5. Enable container execution + +To actually use containers, you need to enable Docker (or another container engine) in your Nextflow configuration. +Create or update `nextflow.config`: + +```groovy title="nextflow.config" linenums="1" +docker { + enabled = true +} +``` + +Now run your fully containerized workflow: + +```bash +nextflow run main.nf +``` + +### Takeaway + +Containerization ensures your workflow runs identically across different computing environments by packaging all software dependencies. + +### What's next? + +Let's explore advanced topics like version control integration and cloud execution. diff --git a/docs/small_nextflow/04_advanced.md b/docs/small_nextflow/04_advanced.md new file mode 100644 index 000000000..47dc6c4ed --- /dev/null +++ b/docs/small_nextflow/04_advanced.md @@ -0,0 +1,194 @@ +# Part 4: Advanced Topics + +In this final part, we'll explore version control integration, cloud execution, and extension exercises to deepen your Nextflow skills. + +--- + +## 1. Version control + +TODO: Create git repository at project root + +TODO: Commit current state, create git tag, and create branch, and then change and re-commit. + +TODO: Change directories and then run using revision argument, pointing to branch, tag, and then specific commit. + +### Takeaway + +Nextflow's git integration allows you to version your workflows and run specific commits, branches, or tags from anywhere. + +### What's next? + +Let's explore running workflows on cloud infrastructure. + +--- + +## 15. Cloud executors + +Nextflow supports running workflows on cloud infrastructure through various executors including AWS Batch, Google Cloud Batch, and Azure Batch. + +### 15.1. Benefits of cloud execution + +Running workflows in the cloud offers several advantages: + +- **Scalability**: Automatically scale to hundreds or thousands of parallel tasks +- **Cost efficiency**: Pay only for the compute resources you use +- **No local infrastructure**: No need to maintain local HPC clusters +- **Global accessibility**: Run workflows from anywhere with internet access + +### 15.2. Cloud executor configuration + +Each cloud provider has its own executor configuration. +Here's a basic example for AWS Batch: + +```groovy title="nextflow.config for AWS Batch" linenums="1" +process { + executor = 'awsbatch' + queue = 'my-batch-queue' + container = 'my-default-container' +} + +aws { + region = 'us-east-1' + batch { + cliPath = '/home/ec2-user/miniconda/bin/aws' + } +} +``` + +### 15.3. Data staging with cloud storage + +When running in the cloud, you'll typically stage your data in cloud storage: + +```bash +# Upload model to S3 +aws s3 cp data/models/b32_400m.pt s3://my-bucket/models/ + +# Run workflow with cloud-based data +nextflow run main.nf --model s3://my-bucket/models/b32_400m.pt +``` + +!!! tip + + For detailed cloud setup instructions, see the Nextflow documentation for [AWS](https://www.nextflow.io/docs/latest/aws.html), [Google Cloud](https://www.nextflow.io/docs/latest/google.html), or [Azure](https://www.nextflow.io/docs/latest/azure.html). + +### Takeaway + +Nextflow's cloud executors enable scalable, cost-effective workflow execution without managing local infrastructure. + +### What's next? + +Try the extension exercises to practice your new skills! + +--- + +## 16. Extension Exercise 1: Find extreme scores + +Our team is interested in which cat is the cutest cat and which cat is the ugliest cat. +Can you extend the workflow to identify (for each label) which picture scores the highest? + +### 16.1. Hints + +**Hint 1:** You can use the `--json` flag on the `classify.py` script to output structured data instead of plain text. + +**Hint 2:** You can parse a JSON file in a closure by using the JsonSlurper class, part of the standard library: + +```groovy +| map { meta, jsonFile -> new groovy.json.JsonSlurper().parseText(jsonFile.text) } +``` + +**Hint 3:** You can use the `min` and `max` operators to return a channel containing the minimum or maximum item, and you can pass a closure to those operators to describe how the elements in the channel should be compared ([docs](https://www.nextflow.io/docs/latest/reference/operator.html#min)). + +### 16.2. Exercise goals + +- Modify the Classify process to output JSON +- Parse the JSON to extract scores +- Use operators to find the highest and lowest scoring images +- Publish these special images separately + +### Takeaway + +This exercise demonstrates how to work with structured data and use comparison operators to filter channel contents. + +### What's next? + +Try making the classification labels configurable! + +--- + +## 17. Extension Exercise 2: Configurable labels + +We've decided that "bad" and "good" are too cruel a classification system for the cats. +Can you modify the workflow to add a `--labels` parameter? +The parameter should take a comma-separated list of labels and use those labels in preference to the default "good cat" and "bad cat". + +### 17.1. Example usage + +```bash +nextflow run main.nf --labels 'red cat','orange cat','black cat' +``` + +### 17.2. Hints + +**Hint 1:** You'll need to modify how the labels are passed to the `classify.py` script. + +**Hint 2:** The classify.py script accepts multiple labels via the `--labels` argument: + +```bash +classify.py --labels "label1" "label2" "label3" image.jpg +``` + +**Hint 3:** You can split a comma-separated string in Groovy: + +```groovy +params.labels = 'good cat,bad cat' +labels = params.labels.split(',') +``` + +### 17.3. Exercise goals + +- Add a `--labels` parameter to the workflow +- Parse the comma-separated labels +- Pass them to the classification script +- Ensure the grouping and collage steps still work with custom labels + +### Takeaway + +This exercise shows how to make workflows more flexible by parameterizing key values. + +### What's next? + +Congratulations! You've completed the Small Nextflow workshop. + +--- + +## 18. Final notes + +### 18.1. Additional topics to explore + +TODO: explain difference between `path(img)` and `path("inputs/*.png")` + +TODO: Add in resources directive memory, cpus, etc. (note: this is partially covered in section 8) + +### 18.2. What you've learned + +Congratulations on completing the Small Nextflow workshop! +You've built a complete image classification workflow from scratch and learned: + +- **Fundamentals**: Channels, processes, parameters, and metadata +- **Data transformation**: Operators, resource management, and grouping +- **Publishing**: Organized outputs with indexes and custom paths +- **Portability**: Filesystem independence and containerization +- **Advanced patterns**: Version control and cloud execution + +### 18.3. Where to go from here + +Now that you understand the basics, you can: + +- Explore the [Hello Nextflow](../../hello_nextflow/) course for more detailed explanations +- Learn about [nf-core](../../hello_nf-core/) for production-ready pipeline templates +- Try the [Side Quests](../../side_quests/) for advanced topics +- Build your own scientific workflows! + +### Takeaway + +You now have the foundational knowledge to build, run, and share reproducible scientific workflows with Nextflow. diff --git a/docs/small_nextflow/index.md b/docs/small_nextflow/index.md new file mode 100644 index 000000000..1e098434b --- /dev/null +++ b/docs/small_nextflow/index.md @@ -0,0 +1,57 @@ +--- +title: Small Nextflow +hide: + - toc +--- + +# Small Nextflow + +Hello! Welcome to Small Nextflow, a hands-on workshop where you'll build a real-world image classification workflow from scratch. + +In this workshop, you'll create a workflow that fetches cat images, classifies them using machine learning, and produces visual collages. +Along the way, you'll learn the core concepts that make Nextflow powerful for scientific computing: channels, processes, operators, and reproducible execution. + +By starting with an empty directory and building up piece by piece, you'll gain an intuitive understanding of how Nextflow workflows are structured and how data flows through them. + +Let's get started! + +[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/nextflow-io/training?quickstart=1&ref=master) + +## Learning objectives + +In this workshop, you will learn foundational Nextflow concepts by building a complete workflow from scratch. + +By the end of this workshop you will be able to: + +- Create channels from files and understand channel types (queue vs. value channels) +- Define processes with input, output, and script blocks +- Use operators to transform and combine channel data (`map`, `join`, `groupTuple`, `collect`) +- Work with metadata using tuples and maps +- Configure workflows with parameters +- Manage resources and direct process execution +- Publish workflow outputs in organized structures +- Containerize workflows for reproducibility +- Make workflows portable across filesystems + +## Audience & prerequisites + +This workshop is designed for those who want to learn Nextflow by building a complete workflow from the ground up. +Some basic familiarity with the command line and programming concepts is helpful but not required. + +**Prerequisites** + +- A GitHub account +- Basic familiarity with command line +- Curiosity about machine learning (no ML expertise needed!) + +## Workshop structure + +The workshop is organized into four chapters: + +**Chapter 1: Fundamentals** - Learn the building blocks of Nextflow by creating channels, defining processes, and working with parameters and metadata. + +**Chapter 2: Data Transformation & Analysis** - Build a multi-step workflow that classifies images, manages resources, and groups results intelligently. + +**Chapter 3: Publishing & Portability** - Make your workflow production-ready by publishing organized outputs, supporting multiple filesystems, and containerizing dependencies. + +**Chapter 4: Advanced Topics** - Explore version control integration, cloud execution, and extension exercises to deepen your skills. diff --git a/docs/small_nextflow/next_steps.md b/docs/small_nextflow/next_steps.md new file mode 100644 index 000000000..e4c8d896e --- /dev/null +++ b/docs/small_nextflow/next_steps.md @@ -0,0 +1,74 @@ +# Next Steps + +Congrats again on completing the Small Nextflow workshop and thank you for completing our survey! + +--- + +## 1. Top 3 ways to level up your Nextflow skills + +Here are our top three recommendations for what to do next based on the workshop you just completed. + +### 1.1. Master the fundamentals with Hello Nextflow + +**[Hello Nextflow](../../hello_nextflow/index.md)** is a comprehensive hands-on course that provides deeper explanations of Nextflow concepts. + +If the Small Nextflow workshop gave you a taste of what's possible and you want to understand the details more thoroughly, Hello Nextflow is the perfect next step. +It covers channels, processes, modules, containers, and configuration in greater depth with more exercises and examples. + +### 1.2. Get started with nf-core + +**[nf-core](https://nf-co.re/)** is a worldwide collaborative effort to develop standardized open-source pipelines for a wide range of scientific research applications. +The project includes [over 100 pipelines](https://nf-co.re/pipelines/) that are available for use out of the box and [well over 1400 process modules](https://nf-co.re/modules/) that can be integrated into your own projects, as well as a rich set of developer tools. + +The **[Hello nf-core](../../hello_nf-core/index.md)** training course is a hands-on introduction to the nf-core project and its rich set of community-curated pipelines. +Part 1 covers how to find and use existing nf-core pipelines, which can save you a lot of time and effort. + +The rest of the training covers how to adopt nf-core standards and conventions in your own work, work with nf-core modules, and contribute back to the project. + +### 1.3. Explore advanced topics with Side Quests + +If you're ready to dive deeper into specific Nextflow topics, check out our growing **collection of [Side Quests](../../side_quests/index.md)**. + +These are short standalone courses that go deep into specific topics like: + +- Metadata handling +- Working with files +- Splitting and grouping data +- Debugging workflows +- Testing with nf-test +- And more! + +Each Side Quest focuses on a particular skill or pattern you can apply to your own workflows. + +--- + +## 2. Apply Nextflow to your domain + +**Check out the [Nextflow for Science](../../nf4_science/index.md) page** for a list of short standalone courses that demonstrate how to apply Nextflow concepts to common scientific analysis use cases: + +- **Genomics**: Variant calling workflows +- **RNAseq**: Gene expression analysis +- **Imaging**: Image processing pipelines + +These courses show you how to apply what you learned in Small Nextflow to real scientific problems in your field. + +If you don't see your domain represented, let us know in the [Community forum](https://community.seqera.io/) or by [creating an issue in the training repository](https://github.com/nextflow-io/training/issues/new/choose) so we can add it to our to-do list. + +--- + +## 3. Check out Seqera Platform + +**[Seqera Platform](https://seqera.io/) is the best way to run Nextflow in practice.** + +It is a cloud-based platform developed by the creators of Nextflow that you can connect to your own compute infrastructure (whether local, HPC or cloud) to make it much easier to launch and manage your workflows, as well as manage your data and run analyses interactively in a cloud environment. + +The Free Tier is available for free use by everyone (with usage quotas). +Qualifying academics can get free Pro-level access (no usage limitations) through the [Academic Program](https://seqera.io/academic/program/). + +Have a look at the [Seqera Platform tutorials](https://docs.seqera.io/platform/latest/getting-started/quickstart-demo/comm-showcase) to see if this might be useful to you. + +--- + +### That's it for now! + +**Good luck in your Nextflow journey and don't hesitate to let us know in the [Community forum](https://community.seqera.io/) what else we could do to help.** diff --git a/docs/small_nextflow/survey.md b/docs/small_nextflow/survey.md new file mode 100644 index 000000000..5d09521d2 --- /dev/null +++ b/docs/small_nextflow/survey.md @@ -0,0 +1,7 @@ +# Feedback survey + +Before you move on, please complete this short 5-question survey to rate the training, share any feedback you may have about your experience, and let us know what else we could do to help you in your Nextflow journey. + +This should take you less than a minute to complete. Thank you for helping us improve our training materials for everyone! + +
diff --git a/mkdocs.yml b/mkdocs.yml index 3f27c7c0c..b44ccb018 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -27,6 +27,15 @@ nav: - hello_nextflow/06_hello_config.md - hello_nextflow/survey.md - hello_nextflow/next_steps.md + - Small Nextflow: + - small_nextflow/index.md + - small_nextflow/00_orientation.md + - small_nextflow/01_fundamentals.md + - small_nextflow/02_data_transformation.md + - small_nextflow/03_publishing_portability.md + - small_nextflow/04_advanced.md + - small_nextflow/survey.md + - small_nextflow/next_steps.md - Hello nf-core: - hello_nf-core/index.md - hello_nf-core/00_orientation.md diff --git a/small_nextflow/.stuff/cat_me.sh b/small_nextflow/.stuff/cat_me.sh new file mode 100755 index 000000000..fd2ef1247 --- /dev/null +++ b/small_nextflow/.stuff/cat_me.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Show help message +show_help() { + cat << EOF +Usage: $0 [-n|--count NUM] [-p|--prefix PATH] [-h|--help] + +Download random cat images from cataas.com along with their tags. + +Options: + -c, --count NUM Number of cats to download (default: 1) + -p, --prefix PATH Directory to download files to (default: current directory) + -h, --help Show this help message and exit + +Examples: + $0 # Download 1 cat to current directory + $0 -n 5 # Download 5 cats to current directory + $0 -p cats -n 10 # Download 10 cats to ./cats directory + $0 --prefix data/cats # Download 1 cat to ./data/cats directory + +Output: + For each cat, creates two files: + - .jpg The cat image + - .txt The tags (one per line) + +EOF +} + +# Default values +num_downloads=1 +prefix="." + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + show_help + exit 0 + ;; + -c|--count) + num_downloads="$2" + shift 2 + ;; + -p|--prefix) + prefix="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" + echo "Use --help for usage information" + exit 1 + ;; + esac +done + +# Validate number +if ! [[ "$num_downloads" =~ ^[0-9]+$ ]] || [ "$num_downloads" -lt 1 ]; then + echo "Error: Number must be a positive integer" + exit 1 +fi + +# Create output directory if it doesn't exist +mkdir -p "$prefix" +echo "Downloading $num_downloads cat(s) to $prefix/" +echo "" + +# Download loop +for i in $(seq 1 "$num_downloads"); do + echo "[$i/$num_downloads]" + + # Get the JSON metadata + json=$(curl -s 'https://cataas.com/cat?json=true') + + # Extract fields using jq + cat_id=$(echo "$json" | jq -r '.id') + mimetype=$(echo "$json" | jq -r '.mimetype') + url=$(echo "$json" | jq -r '.url') + tags=$(echo "$json" | jq -r '.tags[]') # Extract tags, one per line + + # Map mimetype to extension - only accept jpg, png, gif + case "$mimetype" in + image/jpeg|image/jpg) + ext="jpg" + ;; + image/png) + ext="png" + ;; + image/gif) + ext="gif" + ;; + *) + echo "✗ Skipping unsupported type: $mimetype" + echo "" + continue + ;; + esac + + # Build filenames with prefix + filename="${prefix}/${cat_id}.${ext}" + tagfile="${prefix}/${cat_id}.txt" + + # Download the image + curl -s "$url" -o "$filename" + echo "✓ Saved as $filename" + + # Save tags to text file + echo "$tags" > "$tagfile" + echo "✓ Saved tags to $tagfile" + echo "" +done + +echo "Download complete! Downloaded $num_downloads cat(s) to $prefix/" diff --git a/small_nextflow/.stuff/classify.py b/small_nextflow/.stuff/classify.py new file mode 100755 index 000000000..b7edec541 --- /dev/null +++ b/small_nextflow/.stuff/classify.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python +"""Classify a single image using MetaCLIP with local weights.""" + +from pathlib import Path +from PIL import Image +import argparse +import json +import open_clip +import sys +import torch + +def classify_image(image_path: Path, labels: list[str], model_path: Path, + json_output: bool = False, architecture: str = None): + """Classify a single image using MetaCLIP.""" + # Auto-detect architecture from model filename if not provided + if architecture is None: + filename = model_path.name.lower() + if 'b32' in filename or '32' in filename: + architecture = 'ViT-B-32-quickgelu' + elif 'b16' in filename or '16' in filename: + architecture = 'ViT-B-16-quickgelu' + elif 'l14' in filename: + architecture = 'ViT-L-14-quickgelu' + elif 'h14' in filename: + architecture = 'ViT-H-14-quickgelu' + else: + raise ValueError(f"Cannot infer architecture from {model_path.name}. " + f"Please specify --architecture") + + print(f"Using architecture: {architecture}", file=sys.stderr) + + # Load model and preprocessing + model, _, preprocess = open_clip.create_model_and_transforms( + architecture, + pretrained=str(model_path), + weights_only=False # Trust MetaCLIP checkpoint from Facebook Research + ) + tokenizer = open_clip.get_tokenizer(architecture) + + # Prepare text labels + text = tokenizer(labels) + + # Process the image + try: + image = preprocess(Image.open(image_path)).unsqueeze(0) + + with torch.no_grad(): + image_features = model.encode_image(image) + text_features = model.encode_text(text) + + # Normalize and compute similarity + image_features /= image_features.norm(dim=-1, keepdim=True) + text_features /= text_features.norm(dim=-1, keepdim=True) + similarity = (100.0 * image_features @ text_features.T).softmax(dim=-1) + + # Get all confidences for all labels + confidences = {label: float(conf) for label, conf in zip(labels, similarity[0])} + + # Get top prediction + values, indices = similarity[0].topk(1) + prediction = labels[indices[0]] + confidence = values[0].item() + + result = { + 'file': image_path.name, + 'path': str(image_path), + 'prediction': prediction, + 'confidence': confidence, + 'all_confidences': confidences + } + + if json_output: + print(json.dumps(result)) + else: + # Print human-readable format to stderr + print(f"\nImage: {image_path.name}", file=sys.stderr) + print(f"Prediction: {prediction} ({confidence:.2%})", file=sys.stderr) + print("\nAll confidences:", file=sys.stderr) + for label, conf in confidences.items(): + print(f" {label}: {conf:.2%}", file=sys.stderr) + # Print only the top label to stdout (no newline) + print(prediction, end='') + + return result + + except Exception as e: + error_result = { + 'file': image_path.name, + 'path': str(image_path), + 'error': str(e) + } + if json_output: + print(json.dumps(error_result)) + else: + print(f"Error processing {image_path.name}: {e}", file=sys.stderr) + sys.exit(1) + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Classify a single image using MetaCLIP") + parser.add_argument( + 'image', + type=Path, + help='Path to the image file to classify' + ) + parser.add_argument( + '--model-path', + type=Path, + default=Path("data/models/b32_400m.pt"), + help='Path to MetaCLIP model weights (default: data/models/b32_400m.pt)' + ) + parser.add_argument( + '--labels', + nargs='+', + default=["good cat", "bad cat"], + help='Labels for classification (default: ["good cat", "bad cat"])' + ) + parser.add_argument( + '--json', + action='store_true', + help='Output result as JSON to stdout' + ) + parser.add_argument( + '--architecture', + type=str, + choices=['ViT-B-32-quickgelu', 'ViT-B-16-quickgelu', 'ViT-L-14-quickgelu', 'ViT-H-14-quickgelu'], + help='Model architecture (auto-detected from filename if not specified)' + ) + + args = parser.parse_args() + + result = classify_image( + args.image, + args.labels, + args.model_path, + args.json, + args.architecture + ) \ No newline at end of file diff --git a/small_nextflow/.stuff/pyproject.toml b/small_nextflow/.stuff/pyproject.toml new file mode 100644 index 000000000..f5946848e --- /dev/null +++ b/small_nextflow/.stuff/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "my-classifier" +version = "0.1.0" +requires-python = ">=3.10" +dependencies = [ + "torch>=2.0.0", + "pillow>=10.0.0", + "open-clip-torch>=2.20.0", +] \ No newline at end of file From 5d3d6d195bdb648409668d0b47ef1dc0ff520b61 Mon Sep 17 00:00:00 2001 From: Rob Syme Date: Tue, 18 Nov 2025 05:45:54 +0800 Subject: [PATCH 03/22] Formatting fixes --- docs/small_nextflow/02_data_transformation.md | 6 +++--- docs/small_nextflow/03_publishing_portability.md | 4 ++-- docs/small_nextflow/04_advanced.md | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/small_nextflow/02_data_transformation.md b/docs/small_nextflow/02_data_transformation.md index 0c71c2b38..4958f3e9d 100644 --- a/docs/small_nextflow/02_data_transformation.md +++ b/docs/small_nextflow/02_data_transformation.md @@ -180,7 +180,7 @@ Let's learn how to manage computational resources for our processes. --- -## 8. Resources +## 2. Resources Our processes are currently composed of the `input:`, `output:`, and `script:` blocks. In addition to these blocks, processes can use "process directives" which are optional annotations which modify the behaviour of the processes. @@ -235,7 +235,7 @@ Let's learn how to combine related data using the join and groupTuple operators. --- -## 9. Grouping +## 3. Grouping Now we want to combine our classification results with our resized images. We can use the `join` operator, which finds pairs of items (one from each channel) that share a key. @@ -315,7 +315,7 @@ Let's create visual collages for each group of classified images. --- -## 10. Collage +## 4. Collage Let's create a `Collage` process that takes this channel and produces a collage of all of the images for each label. The script block here is a little involved, but it uses ImageMagick's montage command to arrange images into a grid. diff --git a/docs/small_nextflow/03_publishing_portability.md b/docs/small_nextflow/03_publishing_portability.md index d43a0ce91..0eafe84c7 100644 --- a/docs/small_nextflow/03_publishing_portability.md +++ b/docs/small_nextflow/03_publishing_portability.md @@ -218,7 +218,7 @@ Let's make our workflow portable across different storage systems. --- -## 12. Filesystem independence +## 2. Filesystem independence Nextflow speaks many different communication protocols, allowing you to seamlessly move from using data on a local or shared filesystem, to `http://`/`https://`, to object storage protocols like `s3://`, `az://`, `gs://` or even older `ftp://` protocols. You can provide support for new protocols yourself via Nextflow's plugin system. @@ -265,7 +265,7 @@ Let's containerize our workflow to ensure it runs reliably anywhere. --- -## 13. Containerization +## 3. Containerization All of our Nextflow tasks are currently using the software installed on the host operating system. This practice can quickly become a problem for you for a number of reasons: diff --git a/docs/small_nextflow/04_advanced.md b/docs/small_nextflow/04_advanced.md index 47dc6c4ed..9513319fa 100644 --- a/docs/small_nextflow/04_advanced.md +++ b/docs/small_nextflow/04_advanced.md @@ -22,7 +22,7 @@ Let's explore running workflows on cloud infrastructure. --- -## 15. Cloud executors +## 2. Cloud executors Nextflow supports running workflows on cloud infrastructure through various executors including AWS Batch, Google Cloud Batch, and Azure Batch. @@ -81,7 +81,7 @@ Try the extension exercises to practice your new skills! --- -## 16. Extension Exercise 1: Find extreme scores +## 3. Extension Exercise 1: Find extreme scores Our team is interested in which cat is the cutest cat and which cat is the ugliest cat. Can you extend the workflow to identify (for each label) which picture scores the highest? @@ -115,7 +115,7 @@ Try making the classification labels configurable! --- -## 17. Extension Exercise 2: Configurable labels +## 4. Extension Exercise 2: Configurable labels We've decided that "bad" and "good" are too cruel a classification system for the cats. Can you modify the workflow to add a `--labels` parameter? @@ -161,7 +161,7 @@ Congratulations! You've completed the Small Nextflow workshop. --- -## 18. Final notes +## 5. Final notes ### 18.1. Additional topics to explore From a7c1b8628e397c503953752f6fd0874fc65e19e7 Mon Sep 17 00:00:00 2001 From: Rob Syme Date: Tue, 18 Nov 2025 05:49:37 +0800 Subject: [PATCH 04/22] Formatting fixes --- small_nextflow/.stuff/classify.py | 4 ++-- small_nextflow/.stuff/pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/small_nextflow/.stuff/classify.py b/small_nextflow/.stuff/classify.py index b7edec541..d24a6493c 100755 --- a/small_nextflow/.stuff/classify.py +++ b/small_nextflow/.stuff/classify.py @@ -36,7 +36,7 @@ def classify_image(image_path: Path, labels: list[str], model_path: Path, weights_only=False # Trust MetaCLIP checkpoint from Facebook Research ) tokenizer = open_clip.get_tokenizer(architecture) - + # Prepare text labels text = tokenizer(labels) @@ -134,4 +134,4 @@ def classify_image(image_path: Path, labels: list[str], model_path: Path, args.model_path, args.json, args.architecture - ) \ No newline at end of file + ) diff --git a/small_nextflow/.stuff/pyproject.toml b/small_nextflow/.stuff/pyproject.toml index f5946848e..6bd10d516 100644 --- a/small_nextflow/.stuff/pyproject.toml +++ b/small_nextflow/.stuff/pyproject.toml @@ -6,4 +6,4 @@ dependencies = [ "torch>=2.0.0", "pillow>=10.0.0", "open-clip-torch>=2.20.0", -] \ No newline at end of file +] From 3ecc0522a833ae2ab890fa684b716346138657c8 Mon Sep 17 00:00:00 2001 From: Rob Syme Date: Tue, 18 Nov 2025 05:57:52 +0800 Subject: [PATCH 05/22] Cleanup --- .../assets/img/seqera-container-python-00.png | Bin 0 -> 289263 bytes side-quests/smol-nextflow/bin/classify.py | 144 ------------------ side-quests/smol-nextflow/environment.yml | 12 -- 3 files changed, 156 deletions(-) create mode 100644 docs/assets/img/seqera-container-python-00.png delete mode 100755 side-quests/smol-nextflow/bin/classify.py delete mode 100644 side-quests/smol-nextflow/environment.yml diff --git a/docs/assets/img/seqera-container-python-00.png b/docs/assets/img/seqera-container-python-00.png new file mode 100644 index 0000000000000000000000000000000000000000..edc329f438ea0dce41a3658d03732766ddefd3c6 GIT binary patch literal 289263 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 Df`R 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Ɍok#%qHwuxw+ZUapxvWaMOU_{`X(BH!&D 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%0AJ~x17AK0mlxcPWM^-D$zmNcXo~3_z!ut1O0o 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ଝ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ℑu>|$!_S2re9)hZ-m{5FBtU2pNO)}nqN2LR!1Vfo340J9x$0ChPlWXoisO{On%?@; zUFrlL=6U$k7528|`g9Ast|6=W?WW(9ZMNBlcTDVyz6@}9h))c(^{wK#4ZPuVNgnk6 zql(J>5CC{e=Gkbf0(yHYPiaCsqW+ErB(IT={Y0b|i=XtoJ0!9)naX_}QNU-1qV;_sxwaF=*kAe!&9n-6clTChDCKsVk^Bj zcB6ODYJp>>j(!W@x`Hm8Gtz&y*;&D9wV%u6F>yR+$61mNW7oH$0_yDL2Ry(|F^eX% zZa?7e3<*6JL0p%)R`adgcfyyFmJObuMst98bS|s|#4F+>S^6M5eihN1)<%PJkXDHW z!;hDgM&1W)z+Do{^i4pWu?05ORQi)Sauz5ovGM>;zYS9tyKEb(|ooIA&Bw! zV15(O{rRb0vYb-BM9>5H`jh!(s0sEW`eMJYJ>`XWr&kqoqBJc*IUaVUpVXp}FO3;u zu?>OCIG^fC+{4$%+qX*5Sc150l`b)NDV3Zy06`=5YTwhLCfCS12$WPMEvy%_nI5r` z(k*PXU0RWD)EyrERM)Pz1Cb`pbwpCd8-upmPH3w{uk=a@^yC$hCoMm8u5JbF0>E96 z)gFIM*Zln;zq605?Qf|x8!O+NRn1kCo?;V8EgXM|jw#OVxYSuN+Z0_*d-+-TSInXo zW-koP>Q8_(HIzmd*G zl1U-f)Ro zr`rRZEn&f|I+c|R6R{imjv>7=0wbM^_xU!xi8mZ>WlD86Hvh13YI0zb1t?STzOLt|6Aat1O#3n0@tGaDub!}^M)O>?4oaD ztw0Gu_;{6ZBVk_e-vF>bWSpN&*;Ro4C7|&Ir0D21p-_+s(eL@+*JaOOH$ziP$q-i@ zhTgSc=(;9)NCsL-%AH2W!n#X>n+M!?N>%Hyd-gt*6&x>0R5B0%q|5#BPrEA}Hhm|g zm9JZcIE7Ec>Ii;g7umNR_)BE80kHa~BPi$rD5WOz0kry(^hxYluCAd>n@YZs+00#E zo+gcXLe#pQG)xI;neGI|63}xNAXo{FFT>s+RuYZzTA%Te>Hg`UmKKdT0 zOI(V-2^>^{pKmdq0jb01aB3Z@pmD{^$367`E4N>l(wP_srg-E@eDP!yNfck1ubB1Z zCp%*R4$mD+zXvbF8o`Je@`#N)QxK?2!duSc8}oyVqw0^tOW+7RSqFD)?XMIiKDnP- zXaMWKq?x8=;!-eS*tKZ;IkbHpb-E`-C~J%r=37LBri}SSViN9gO-oP0LHMC^usm~z zoO{X&?qVc-&vwhr2|rF-9iB ziZno#v3C%Eb)Y>LK3Z9eBmW40!n&;_ptgA=7~RUfDodNc;Gz}YHtV+WYI2wJB6F@y z2uWEFgcF0DqGh6{UvaA-dCh3nkf~b^WEkiX$c}UVW|9O(b&UpP4e|o_IN}t|>`yiS zB1?*J7;aJ1r|WJe&&`^j?)~yDI;D8stVRh4a-u%UL@EjRyaS;?AA6dH=NG3hoyj>X zv4RM-b|-i1k6giSh3YN!ji?n?2=N45dzwmu6Jj-I|3HpOaU;qnu$Lwx5fnd>2}6z8 zL~;x380&J!S5}~T-Njg?H?IwGZP{;{iODJSjDckoX zQn6X3s%rZRn&)#K+*nA;)>qE$-mY1Sn-6!ESE-Ucbh-`chctqMh1vnh%?VGo-vAy) z*WWl=q$Rz5^=2gf8R%ppd5OF+ChW2}kv2#}EZINh%JVALV*Qk>&n;L3VtB=f!~k$` z6I8Pm(WhKHr((Am#_czbg^_ZuM_eH@rn6-29 z_>q^DbOZi-tL8^T2a6L9`w)IlxB3dxkV5r9z8x2r94;4blIOXcgyfbvp~3s>eFdK| zA{?aFAySS**j$gJ2ZVV9d3cYDow?`?y~Gh-l$OL{V2&;akv%`w`!Pn5Wp!RS^}yET zT=1x6MeAAM*{`T4+wBjo9e(Ez^-?DjuQG3;DS&%k9V;qbhSAhV@}`QE{mq-1^K*vLZim_y(?)&_^F0_eKT5F%L*^ z;4Zt1GZ7@GEkk5Cr9+W#3wp4MzQp7D?ui@;wLSnu03`wb<#E}zy@)o&98hc8dVf&!63wCq8p2S| zzj+LKHmMD5GYwA|Iy+eGz7$D~Ddb2o364rR^*Qi!ZoR}RaK7@~)MxW0lfgyV(yY*pw(H4|N!{(P@L?kDc%i3;Th^0~8#bZw*w$*Uqc6%F z4nq3{>@W5Mhi$&Le2hX`82Eb(`wIWSfjxQArW&NtLmZsGk|iQd@-0$lqa0sf43BjC z>C=iBY@Bj(j5ZT=VN6>?2~^^ez1HK{I$aT|pveU(2kEpY2j7=ZxS35P3Cc-g>Aaqt zhc2B+!D)j*dAZGxP>&^Hb=#4ynA;SPED(@xwC_0ouwH;n;0hIzFZDB?xT-LXuAFKT zBPbx%>eM!Hf(1N7>wU2od0RZy{pm+|pkaeicp#u_;@f?ewLkU78@$9$#!n~F6V##w zH;Gh-Y)s=wo~7_>uUaW*K-bQIGW0OEz=Ul;kcQ$>_qs#%b7{ugSlxOnyRv?((Crc3 zI%0##=D{!Z`r9b#Xog_HO;D0vWx$C7tFW7CSn#quxn#?$OWR zKH3d65@eNb_ML)NP=>0buXRZ>lxIrfHcFU14&I@c9`Vg1N4l$>=v+Oo8c6d_b={== zcxlK)#OF2c52O!YQ+K#mCfP8+WajFIo!WrgdKsQ;ova~9Q^0NEWUFo&XZz(iY5+yh zQ&+WE($#0BC#~zI_wm5fbom?x)z8fhHaAfLh#cev3i-e~L)4j1wJHjSdz;)GX6t8M zxw70ASS+%=L4FzVa7hO8ce?r zn@mPA&ebximR3qLLeqY<^_==Hwr;t~5pMQF%_6Hj8%}?hVRC?n=1^P4zakP4iq~3j zEIhqzA2fTYcc1gYVxCHg-5x#gB7rymr$c@N^l z#wh{3IWIX|`hEXV=De;)>vh_TRb;ByczfFrG!y#WwdnRrt`pebW0#A3fV~3&y9F-4LpgVFON*Yn;qnCbKJSd?UsBcB;J* zL;rU>8^2-Z+c=u)HX+BhfAGGVd;5#a$a*hmcb`2(3=EJgyn2LJ@KxlrW# zD={%d;%k63>pW(*+hK>|w*JaJN-nvJC@_sG`5;0gGRzo&(FLj}QuwLE?J7ay$_RwB&`LxcGnm)i0{m{H?;a7@SB6?*lT z^QkXO_ZcC$-2)90-4D#Q4?I*kPIY4NuRuRLA5GY@Y_fM>Qg`T>z6w>)Js);_t2*3| z63&JB!EKMuJ@{Ll^`s8y(cTB-LQ7AbkkkU8x?00UxDfDBy>X{7kv%GrRHCRt<|>ox znl{i^mVoaPzMq$|D{-#s&m{XpQk2x%16flL85(KYU#1AA9ZH~|C<^r@pSbQ4_r;8LY#x(RDttE}RAbpLPM3=nw5;mHU zheT?isAxc)a$ZoTRdt7ctvXa4HzW~`oD9>M?{87(TkA83aurLB32Sxmk6d~kL@DlN zrc@ahT(Wme^n{8-e*Z9(x(&NYI5h;8vJz+`bCg!f<66O~vsD{_+qLM0c4;vwD;^5^8QQM!0ap}Jwrs0u8N+(0M@ z>S|+$@x_wc2?9Fk_GNWsvR;ykU8bYR+A$S)dQOjb+^0B>JPzGPDR72ZjNDY2OBYes zuQXok9oI&&_vp`h?lr`O?GY$~pl;|FfC526N^`c31Q~}9?|aJmT%W^(6!2DAo`+Yd-Nq#2*Wp zqY(onB~5^$tNrL5|a+;)~*SF;>ryGSg%W`JT^**k&SlG zYLL)L2Gt7FHF=di;hs}qq^Ut~``ChHR7;kF1onps6)K6Z%PjGTYQ@C^g7bZ4{RWpd zYER~mmNks+S!mC)fV|K_fJU9Jrn{?F+@AH$Dtv2oi$u%Aha-?M5VkYjQTig(qKJ{6 zcrkN($`+1TPip==P&a31P`X~-Ch>@N%Y{q_1bHbQ^sHL>tjmgE^@WrfaE-jGCgvR<*A)NPC?GW?ikAY218wUIy#@`7ta-287eQ-GNaYyKRF(Th)X!6mF&nj~s! z+A_dZ@!5)JK!y*v8t>2L6R8y_WrFB0Z02a`lDZvL&kmS|tSy8QIUoes16BWmfJ1H-QAW9%bS-{?r3BU?yH z?(Rs3X?PJ)pU~`fL#!$IaVryH1^FJ|JjPVN!a}X>lAxV`l2My`#2<_>>@)x=9fK9_ zllDZpXq+?gYRN`%bDAVx%K)VWIm@2pZu;{@54>aR6$v9!h$TBZ9vK@J5V3?F>}0SGAcbShgW6}Bo*B$>_FGrt8u zsIAM2`$lJy-hoITKAU+MOCu>f`fc?U*@5eUiotDjutr_d?$a*BYof;!>7rh@>TK<< z@u8DGA+&E)x;;qaODr;+<1Fbp_yURMZE|-(ByX)Iih~6d2hNwTRcye6tMqxfJ|?I9 z2|6T_#m$L5*Xj&#hssrKd32UMtBg^;>d&aRqe?(xoP8rZwPx3oU=>~J^;1k~f~MKM zQN|(%Oo^Sn4vPU#*D_M!fdm$7c4Cvn$0Q{rux#HL1U5X}Erw0`<0OV2JUHI$`;Zlj zHmjM6CPC~y6g0ACG={6*&jv`K=~|{Mh>TAhB5@^wnJj_Tg$Di9mCJWRlXM#ny0u5M z#pfbAJ=V2-khm@?>UpC7dpKg|+u1A1$TtV77F8}m1Z}cpLuXh;%%~h9o#d01X>@7N zt?O&9z2&uN0sSnE{rNL?;BZ^APjto8&y`d>&Id$fVfbJ?|B;Z5SDuyuyfpfGmpZ{O z6%iEbgp^aCS!>#UR4@xK_f7#~EbYK{Nk+)->)#_(A)cP9Fj5J+I*AI`yt}6W3NhoT zNhg)aNGz+WG?Q2( zV)x0(uG}n4xE_$POh4Tt2TOhC#r1Bz-@|?ICw&mOoj7L&m{D^^d+eE!Y6uk0&Hp|G zNTbP`f2lVB`t`iK@*x?7!~v;2^op9er1w3;*#_E9zSXGVHsc|wlEH@NWqfp0;-A6A zX#UvoXL>!{1^$Zj*5J~saSY<2$I^AXi^wvssNcl^iNiBZSiM$>Adwn-Ju|Po|Yw zTt{0{LgT?ig&RESY)S!WE$P?Ra^1GmARz?+b}ShY`ljo{sV-!)i*@O-&}{nfo=~Y7 zXi*{tbB(|AkWj&(b#~hXHeP&ef)2vU#&L@H^1g z1>K(PZTh>*mwZ>9G~QTRov+>UHybIjk+H8jUpTFnKd3>%06gDWppBrozptpQq z_k@@;58K?aJ@qA|CQdLPb-T&r82;KO=!j>(8QJj1#0j@dz@PeAwAkrE-SEd%QkCK^ z1@e;e_xp=mf0639^SDIc-0^;CV;8}ub1#GchsXhh03I3K{hyG5!yPI0x}otlcoz} zAb@;JEuXr6eh`u^{cak1K|RS|gW^Vt6(oMo4z#j`U(N(T`za+u!MV?g{5}z`hE4+g zmO$UkG~5+QwG(P?Q%#K{w>Elti-!>&e-{)5^sIY`0m|I)lQnxSLgzQH^&)zOhJ+sZ zK3y^Vg2~@OL=+@xg_8M}2H?S)w^U7T5(Cs-QMR9V0+aru^2X}}n&wCLrVLufGGlp` z4jBu~2b4Tk8364Kz>mHz=G&J4qmupK3lgIO6b=rtz=JzlY>C{Zd3W;1#{Xv`v^>{+ z$W%qL+i0ED6rg3iE{_(fS)!GGcnF?g&|>~qQe83P{dc5}()wUl*2A|~y)43iisJBvq5S_>jHp;~G#ib;d1s^o;x&yxY)j%*u1?UDDxCp&;P=A`3~_%InfTi?3I1Kd z#0FzJM&3&u^6BT7-sU}}nloWSUKx%8X{)s9Fke34g-fqWO8I;WwtTS2XiKLVLQuHpYh0MAJP zHi^0$wYZtacO~f;q%+T5|Ef|HaqE<8`>Pz_)|d@SvRnLtfBtiR{wY|%j6`$U`N9RH z?B7VY8MHWaASc*Qj>7>B5v#WW@@d5v?ySWN#5|fdsr1Umhk2IfgGpuP$#}L(>jR1O zH_uD|t||UEYfHdGN$0cWQ!h~DCuP%mMlDpwNsVt{tdhzUv`U7wFD~juQxl+ko1EfG z%C3e}`T@IPS*PY_%0$UzTKQ#I@2;4ldRgKX2G}AkVFhDG%SO_s0`UBqfS4#gs zpL0kErd!>^Q@PjeL!)(j-)+@~vlzAOv$$>Ly7#y>X`oNdH}_;Fa0;xRlHZy}jOSXG z1~jsLZ8u5PZBb?)%atArXha|UyX@{i*ZZFjn4zC{02U>{^tPRT6=eTrAjwaL&Y7H? zDd}hCKF#Ewivl2|p)r1153r~66>sKtrejTK0rFPyR3X{14D|Z{Vk}ZbkQSFrKxV3* zMXx?$bEGP>NcDqcsMohHpTZP^tqTpaaV@F#={>03BUdUW7>9Xiv2(t3aU3&fjzJ+Q zH5i9%(3s60h%J*Cml^U5&ME$*2{43wuD6{Vsx2BD0*JecYnhKz-{Wp4XN0hksjO~OW_(U1$0j}Dh!8!-UVG36C9xEzP^7>)&KFq zUzXS}2~}YnKo6u_O_YQsf7i-gm%4}z(_a7mZe-r0=}v_QP~KVpF#HeQok%X$*V&M^ z{nJ`X7t)tQX%kA}Nkcv0R+~_dSE3L96d(B8HUjKWD)Ms_wb0i)=6)LQcB$)km)*H( z+wrWAjZ@meBT}brO}wt$PcO4xhL5iAB<@O>FE5L+Va_Ev33UNvN~C7IX9qDq$Utf5^6%V(9LWZcrFHhHi%L`d#?!XYc!d_Ko}dfBAnm zA7!pt*SXHM&OQz!YGl33^~Sy=uJKENu~_N^ss;YPca6>y34jvgaG2JGI+@qD30?i- zctcsK5b+iiUaQDg+aH!*-(J!TXZ%NZhD8y z|8y7s*I~srM*4SJOAz-4J!tpY|NRm-`si;WBXTg5%--I4ZSzjsDq)KCh1D>1(R#u3 z^2bBa(-l5+Xj=(boBsdtX>1Q#d(vkfFT}v=qMg2og2*rt|C@T}za5&teh`nj^DHs0 zi&IeD1GfG8-_E-4f&1U{4!0^c#dl;ibg(*nC4%Qp+(Yxd8!8`1$r$a$zYi$gVxWJE zV)b!R&6`x7U{uF>$i(GX_1L|dV^gV3T1+t={2mQ}6^Lbu`VZg#%YV_0Bj;%_gujs( zsbbw7P@v>YMQ!|ll|$4WwbzkS3*7zDt^W(@r|SRl_dl}x>m|HgkaGB!=UNx>f1k`7 z!G6`k>BON|n@?(H;FRJT@v%?D@5O!Xyy&C@$@~;L4*bbgT0OtL-MsX|ohkNq*LS>P zy`q6jLd_#I=a<55G`i2HtVJ1+S<&Jt8vt_ILy{<-`DSrJU*wSkmvD%0TJhCcwdDhB z3X!*W?jj-o?+;FR+4ly~N?Gp@nLY=XKjUl~dwlEP0^g3v7vX0kMP&4sk>=X5MSK{l z;E|8u`;hVepP&A(-}7gAP-Z%~8=Tm*W>kj2frNRjQQo$mn{QCa`9BHBOiCX~c;nau zYrj+(GMG?gQH$Ukmtg(Jm-zQmyjX|I;IXbL-R9CPIZ!TPE`pU2enw7oyD24h6~?L-5C~Iz@l@?q|ER ztYlBryka$Zd{V_(NRx?8(PK_^dkIif5%nKMaSy34#-&QOd|X|9Osh#JRP#~%QGW8v ze^}c8Xnv>e*Zx;Q;Fo;e%SMgvEM$(4)^}c$$LnZ7Xoz+HD3lUH=~t^KOI|@4>ZoxbAZB^Vf2duj+Rot2|GvXuAtdsW4^D;Y*Y{c#jXom#-uyCQh2`DrB5H zhNrpJDi7n_D(6i3+15{{b1-;Z^VC!meUkQQCdWh3T!AvsW{&c!^@6cIk6-=$idxzp zL>j3AkJ{!J3|IqBPftd%(wGAVh=i{mKTC54W!@&UA1>&4n)fdk!pqk!nJY#|M@=Ph zO(cgf!9=FxR#?M*E-B&Zyv8Bc^;S5eZ}GI zC%0<02n&+EK_i;5;2CvZVSdm(REy?}9>VV2pk?-Jl4ag1z6~Q%P_PtjpO=jdglY?2 zg?7z+rn0&kA2Cx+9=sJv6*4zAR$xec zf9&WKLfWJIc9EGeE1U6RA%q|1<`%YmYa3|EkRvpSiw4UKah$Rf=C}MZG5J)1us!~) z&>*k4pejKB4)!9Yj$cC~^+QLa0R6S++6)kuX&9nt56lpqS7H&&F^GFbzi`OG)lLa z;Ro2OIX`kWbCxyt}BKJ?9JsmY}Eebi|HDNCwt&bN>p-WCiK zAN;g6uzSC)@$!i2w_fodtJ8v1kZ)glE)o{`6-lbF7f!>;y!*-eWn0H* z#mRZLSL;tSQb~oc9B6H9Y(7|zvIYScnLr4e^{tEjA%qKDj#!oU(-_Xqs4b)11&Gr1 z=vV0TYyNbL`zzA_%5YyiIL4o$vAv zWSyf(zW_OAj6UO5m;~Kldpkxcov*fMck*t@=c+gLHrH^h5y>ev+zo-syUO;6R89td z%nJ_IshDRUHra@*e#@%0O#AZy&bZT#Qx@CHBmr1So{nz zASZ>P0^f@1%VyQ$%IX$(qoMyfTI<-jYQMfly>sXK z`q1@6_}{?Cn<=P9`07XPN{=7ZbFbACM$)vsLqGa}QmnaIP*Z@Yxzg0%m~gfI`p&3n z#*0uqSQ>S0&}*ABV$rYR+=+I7xzEc(#-KZ5yCx~2`nbQ|&DRa_yq9|=86vOHjOUJ2 zYSGU*>R`5VCY zm-MkwO!z$+vP5-U??`dR4v*Edx|kiGQk9{C9}0Tm;mQo(J1!n#o38 ztwewQc!uX*x>Y=`B7J!G=?^BBqC1L2LCK6ruK?1YgY%egB`cFN%|!pRvOf6V2YpQkPU9^fU@nfBmROin9A9oVtSMwY0)iE1tQW zo{@x1nh)W9e+o3HVeZ?riy+(#P+N(U=;$)LZ4;4pwXs*&m#FBd#SE|hSQ>Edl2$za zt)cSo1P~@UPzQJ{7;hK*vPX6}Wyxiw#0cD-Li#OY{VnT z-hy+zo2~Yn>4kVVmq=fMf zbjlf2>hCir$Q|_k2>Ex7?msgb4_*RAj87>-i~*80+kNXDQ0xt@^!&kcdlTBsY@G!p zMuxr3%AyCzGSZh>=+GfGX$2L{`q1!f2F6i)!d8V)TCjDx=z1u%lyqNbXT&F58se8* zla-M`rgHh?+OMG0$d#+}V3*Y~4ax9xfsV5v)4+1;(VqHaf#(2-OsuW0m|ePV1&GEn z#X0BVGOaSnYulmD&fxaM5lO*~%2-(B3xO(qfhvLdw#P=odiSY?Tdi=$x`I?fT;9do{qpQo~2vqHelh4@1q2+#DUIA-hwBo5z<#`+_+ zzKzcVAmfh$TINs*$L82V>ON>82d76QMn=-ZuNykG(hxK8%jnXi8?xdS7uAZ`lY^|U zq=~a_sVxGz1C(5mP#!IV=pZR7X^Mw;YtCJdJ8Wmzom1r<%bsKp(TPDp1_P0D4+;@i3z z>mG3WtTRQA)|qoab;?U@;FzweW=Fl%%S2wsYjb+mV_j%GRhMyIOVgFYT*YQ9tf_c3 zF^VYyV*NZz&+ym8NPP~*H=Oj~GY$0skH>42QL7T8tF*xieMa6IOHHVLRvd+2#}TPY zgQ$nq*v<|v!pE5H?_C-HbN#$U4!#SVbF|ceH%K{MZutMo(SFF0xh(oJ@$7Vd@cR5l>r4LjJJRzUde z*j4Y;<(dy5>W#OWIX4B5uOyUSHaz~_w+H7H(!pkEyIOVI2qquy%=hdhvTSZ*EH2(8OW$y0B@T z&oeF?!+YCOi1MsJTUNenqNn@&?$K_WyY-*HKJ*6~&i!z-rbPLDeOGavezo0e?svw3 z-eVQg6uO$lSo7)q<)juL)-DSS>^Hv9>yA-~ed_l)lz}~nlW3*sZuyL@R#rOH=elt_ zPS>7%Gx@Z+v}F-M3XbFG=hKd3)~75w6Wr?c-D}qsOW_-P`obn=%1cT5i`{fZ;K2r5 zli4kLDQLT+^Tz5)gk&!3X&tCYKN9P>{Su;6zOny-+;R`V2g;VTN~8elK5oUl+hanl z-6Y&Fx?<$u=m@^W1^8l!<36sJzjppK@9GoY>Ma%--6LW6kt*mQcJNW*K68&kCID%p z2++DM?%U7A0SMtd3Ktt`;a!YkhVd#Ha$PYA!g7~q;1u@FDUV6v&^r^E`f{h>`B7u@ zqpy~uSCYvg7EeEUo_>l9-k;NX!eW!JZX+rA;bFrW=IP%4_Ghn|YCpvo0M`3^PIP-% z0tF#v)da~k{GMBvA)>B{ezM%UQjguC=;CVfB~-UCOg>?^6cQ%`?w$r%ebX+s8<%pd zSzNnUE?yX(k@^)BE7u=iBVzckU87aw;3?|4BiVu5XqgFF!?MTdQ>sQT2C%49#($fnLoO^JYO0Nv5Rmt z_2W3ChwzP6iQVd228sfw;P%c#xM+q*zkJv8`~m9+iykkPNzBE9~K&gcBG&=M1Hh>U;n zIm$j?882PVc|ueo?z8TP)fKB5RB$g8xVh46T%)%AFtvwj_@89kMfp7yeAWNdA@QN# z7O&guhNCQ{B<8GC7k{gT;nB2cV2Z2{7zol~EHLmG@7vqb6i(pDSG5dvJtMj5PI7|7 z$zS*}$tNDA1IXuN^evd;v$)}c9gD94&2IUFW;@(?0tAj#0_$6h@-lraFdKB}I1f^N z2MWRJ+QRGtg>rHLEM(hHsRHpZoR+AAx;0NFUDm^6XTo=|M6m|K>?y)NQcU10I7Yka|C(RN3@M9rQb;V7$WbeIS znobK~zgPpGnjTBTdULHQ&mqCRrrI=_rEYxl-R?C-?omVMAn&6t?$;~0_+DUs?Kma_ zfcFTK6Yx{wNNEJd1ib zbc&+u34UH;m}IiyyjL-cW>!O0aV~c1nhbG_xaD@TvQaN{RMq)73#nvKHM*Pd#i;jx z)MOu0L(5_W_sUn79OJYo%MC86%$BDNeFi+|i&qvCXoEU&z}7vRr{CPaBZ5boi$TNP z?3Q!l9Vjd&9u@ZEa;~7w;cEeKqaMcz`>~br`6-SHF}Ky5Da;_wY}6A8?JOPZXhECB zQYVB*7QZ@|?G)3NZeYN0yy0bGvRhMvVuIJoUWk3fb|WcO2Z>ji_udj+RwpwA(_=}d zdgr@y3VZGT;=(}BkWw8Tvo5EqX6<;f3v2zVC+_>l&*UVaIAS&i|q(+3iY% zQ@-tvkr&|!MBsZ1tZ8SdTuvvtd*QX@QHzVuwabkQVl+0?-6k4rgIwMWNeyB`KIs-+ z94r(reX!n+VK-L|{h^Z~YyMxt=XbGTOz-d^8zI9qL4yKx3Y1yN?hU*v(X4Q_Z)KLP zr=3Lv4Hmifh&U_3Est?}HXy5rYx8gMYm zy;h>-@vBHu1)ak_doJADZ8s6TPG(fdQ!U(>Q?w0_Jlx>w4z1Lq#u7YJve#T5)1g$A zK5T1|ztu;lW4ker0VAv5$LHyE!1IvAOJ(hB?qgrNjtg-vhJumS%CxaW0LN#80nDPmWQA&c2WUy|O9(}d1ud|?uE%;X8vrq0egbv-} zuZVoH(6B?=Ulb3IxIXVl2OLMHT<-p1TUqh?r2BAca=(*|WQV~QdsiJ)3e}>l<>1-&6Nl_14lngmoY^E2A&nw|hg>~mg9XJ{pfP&jjdQ1beK}v}*{=wv# z?ZpL@0^i9pYii-kJ?$@@(~aAAnzjQ}75QID)}IFtrGO{B0^7ZPbON;+&GNPBA!omg z$!-n#@NVdwP&!5m&C_E+{y5q#n9bIz(vMbf#;90|ekhWnlF|M)Sx3EuC2xi;;E{$? z*V5H{kQyhWCV2UjkdII9GCfDNsGN-LsYq%+IL#~&20#mTPDwOYha_K0k$Xy}o^Lba zu$ZKv*KqhwJ?GudWBfe4?gFMt%3}!)2l*vKvMjUZrdA2kDnbFoS%5QxK0ncgW4iHV z50~BH=Nu7}pHHIQizXHi0HVLn`1d zt;4xxv99MkEV0*HiZ%JH*v?Cd_fq`nVOa2m&|0r5YS*I7&{L}6phu;D4JZEg`6~qg zq9%ik4a41Le-#Ch3gB`X0Zs>EhZ_H?E33Y4cw?z>~rX1 z@vw!3IJIljO69IE>iToD7Wn*<-v6L)Hd-+4v~N4w{T@qf2UDpklZEuWJ8^iUl~IzQ zJi>D;)-RG(7^_Xc{wkAkDY9c5gZRQ8H>;C3%Mi3@;Aj0Y+1J7^>jKdzL}CBgTk|G~ zV48KskRcnW`(zv5XlOcl#AWj)a@}YF^LC9wze&NgBvVH(SHC(w1!#3X7rUo)qG;4D zzfYGDxX~uXghrjK?O1Ye*1fe!#dt3IZMm}}`i7;`9{DJk29})DDb1<1tFD+FswIW3}E7Z%<4aN}pP!jU9OGAg+DtA2rVysY!@1S)qShW>asutZ=m#B%(hCY|PyysIx$s5i6UfbT!{HjmM zX~IQ8Pl(9=W{`O?Sq9n!ig>S$De^RQx!vCJjM9eARtfR z7sk_kabr*!Qk%Gi4-=U5YdPlu$odLJPgQo;{xG^8dr8^hRF#^p{8QWZu(9lq5`DRTyF|x@Tw{RV+LJ}v}XQ8uMjOx>~#)L~^ z`)XM-&x16a&3^t3Cve9_t&xEt*{&fk6z;fu)_x(NSWtWBYD$^xN)cM%L)H1>{p}_( z?q(iiDTz7a?BKm<(r-E`<}YvUtA8K~`IGpD-9$IRO$Azw`UsC*-SGx5!?7_8Noj?s zn4IYhmvmzBsGfks_|bcekP4gO-j9~KnuW1c{B99C>+^2wF;7c8hBWq@`WKXDSVtc= zFm>1!)NB%*F6WXf)B}Lpk~7!tW??I1=tG(PQA6j3Zvqq+)pHHh88i#bf>D$GJ`~%P zgNyrsIW#Ur|6T&WrT>&u%QQPcQ7!fM zCMlBa>M87D*SVmQDeuxmYL~Qq-PmD1f6~>yS7B8u>GT=B{Uad;*>>YTG{LQk)8Y?} z-gG24AxD^Vl|0dSxGvA5Sc?^9>Ci>|P7;&eRP7%R{#ATtDB8-u2_XDjS# zeDbJs+iqa%QLo}8<#}#YeGPZ(5Qq=_L$B{ADnJVynOfeZTUh_AF&`AZInwyM zF&`-&ux3^C3vZoeW{YrCoU81#(lWWOy)eu73^!>Z%jN2=Tbnbd&$a78`Fegsv3s2h z$4lk(shR=E*b5bM9Si1{HKBV4rC2@qn)+%1>BghCE=0Kmn2eX%^gi!N=Nh$ZIq&W5 zg*kv8L?uG6sh|Fe6$SC6{~!Yb4A*s@9Ue520#T3(G&oAFA;WAlF@0e9eS4_=&j1)< zDkiW(GN=MH-whJ9T1Y%O~2v@M90Yi=iz!0uf3 zNuBuhr>D2BOauw#CYtv*I<$p#XXhm4IOG&AmsTj6>%QWkgYc=nt8e+Y$;W1*JfsJ2 z@iVT33=%=}$s-q?6rTI+D>*qFZZJSxl|x{DtzbJ}hNaPGhXBfZG!R>NefhPJGL{pc zi?YB0>grVUM*AJR?H{_yz;VMPXM8-Rhb%(PfqXCftAz~TycedCWbUhE)l00S`0$X~ z&cFF5f9?I9WqW?JvC%|>uS#J*AH=@Q@-0zUE6uXV8DmFDzC)<>gEpdNAo8a0BW67l z2c#JKb`zDq$xmiIhsx{Azz^z4^f+Xr7~SGv7JeZcQx|I80J!JL`sC8SQfcAtg}-G_ z2&*6^J$-e^uY@$~+3ITA9U!{t;nbX3_A&_!URv_^gHr(YP~2+7TiT z#O_huoc&H)Ni0x>YlbfMnnia5Uq<;e+Z?7Z*I6Wv;>^5<2fo#IcAH<&N^Hhup$7Fi zB-5oyuwSlU-n}vca)f)6t@I@(r?TTf<1s8xTTG|ijs+Orhy$F}`wP!_WN!@~czOYy zrC&vR^eKJ(vDN04jV(H~m=`5827`%BwatI|M!0HvCd&=6>p91qU1R%}dYZ{GDz9aS z>qS&R%LcH|x%V{waNPjcHC@F`6$k`kA4cbtxptPx>@O%?SYy%=GKF}Gpd3eFFjYBs zM0Q9~yb;Z*&-q;6^W6E^n^SRk>)Z6RH7_-D1wt8mS{eGK;e2}^nS!74rs-t#QOO{s zh5yZm{a5ArihM&CxD!pv60g(5nF}ux73@@NY&;=1g%Dh{;z?rZ83n#d@WG?@|B4Qk z==$vQhl&m00U^J?30(W4ftd(yVwyM$!vCtV_T9%{6#RE&1<)AoDw~krD(xHNnSBCDiG`@iJ)6~KDSE3!5tK#+Jd*dB3Cf=z z`S#|KtDl^6A=(-_2&SwX(qL7@=FjrN7W!F!zy&i!!pgAh-mE^Sx<8cT@f4~>$@@&W z@oDX`S?70Z)MN`4Ia-;Z3iFhODa7-d`l%!GT%kaimW!6;t?9v=y9WD)e*7_y*EruB zjr4+trNn&gO3%w*^~sJOZju44xk)!pOmGMCm5^=W)OBn7$EJr3b%}Zo8nGWQE-ZQz z8VwPiM)gDT`HA#pM2@>a)cy{-oKlgxTg1QAT7@b6YR4?Vsci2M3-JZAQ$sj0s(NIUmNhuRTM-Bxe? zm#@K-d~>b$+UWSe>@IcsSbApmM>wd@fL4D4_Pc^CRv4StWN->qi%pKw;md5Nz0P`Y zu=ny2d1p+>{lb{b^WwT^L%G+vtiaq!7*nT|;`hJ-@yhscxey+}Bp^SwxYfP>?P`B9t9~6K_;=Ov>A$L$3|1IEH`eJuTjs#=Bs_(ykAS)!;>2V;8 z&gjdZ=}fWRk*F0`=%dh_p_$+GHSgay!|Edt2GHavNHB$ECy*G!MAsXyN?*|^VQF{G zr-CN3%b0XMepc77lf9NT@N4jQPX3r8j*<4sjumJ0&E(ezkj#Ie)%~Yga;UQ7S_FDD$eGdP1tfD8b7Wapco1ImeE`fE;Ik8&4(e%nkkU5!3mEIcm6a6UYrN(W~s z^stY|I*s*oVxQV}8)z3TzO|Wgl3pKWW@hG`0j*#zLF5yjcjfOStR6W%@f8iBhtS~B z68~9~=p=v3cy=`%Et>|ezPicBA9h5PlbG;{8OTV*l%Z%$1;7gZrl<$7;PX=w3dHeo z@o71%0GOCeyW-TsVw#tydG%V(dcR{{%3A4hS81V5`{2dkc?}aKrfD*61Le>OOy2fO`yM{Du zHcU%y!}Y0D-$W9nsC||FFS+Ss<*9D`yw@-2Jp47sY1c#Dym{Me>6;}N-|28pW+y)@ zH;~7q?Quih)kq^Mkr^EF2~nV#sPS=l#Kp^Xd+wukL(!8ymhVxw8VFTa=cj8=L(dgn-!Bc8DU$zP79%_ z5gc;Fxd3?Ik$;X;-x{LdoF|cQ*f%QXFQ%?d!y!{1n-}(V?zuT>0A%>5lB$hcAD3#r zW_0pCKq-A9kFSVIZR?K@q#NH4xOIN4tmMU=u9L~35Xw=w_VA7V@eG!E<-^zww=1y1 zqN)hb;h#0sN4$MKx96|E)O!^R0mb4`jrHqS8K!zWmf9%49!|LkfQeVGAPU)z=^+F( zwnzU*|IU}++V{ogm*!0!K=7}10L?)HEyu?y_BEFIO!}$IQ^J~tDhl^gblZ4SdCUth zCYO9Gy-%Ne__+(}8kfg)$Xio4<)>-uwQ94%B0%A%8sGes zazQP68(#%?I(5hcXH!|48EJJh>ekrw#x?t@{2=CKO4iq}7CCEHDsgMjSgFgiV7h&? zF)?(g?*Y=6-;2ai(ahs}GqqD=pD>zQ$a)w?w`G<=+UvAki`BytgE+Tl<}I*90aoIt z`fHeUAJ6*oa_$k(@r0KZ=C!i+EorI4(3M}?fRgjq7B4A&6J{Xn+1q}WKMxh62a3oH zd7zW!#ER+*o|-jj;A6f`Nwf3&LHF96z+C<=twR+P+XKihL_!Xe$5Av_O)*ZRucy^H zQNYPQrE0xRckS@FqN_G-(;)}od}R2;-|T}@3*vX_pz^R^y%5~N)@r`+nJrXzUiy91)4s!~jC zP`Wxyd*rkd%DD)`Wdty8*_}eXcc)osw;>A*odBYHJQE&a$3|O&3Q(D2rOHoRq zNQ|=l!Pio`Kf6Dolnzy^0)a@37PQI|>ST}LJeC0^>39=5I>`x;VvSvL{c|0LrWrC8 z3?}IUpL}VnK$viRW>@~T7V)8E&B{ok)1V`M8#+|Vm5xzq#!p=9vQl`0#oo7Qy32a{ znR==p)1Z1$KAY#1oyUA&-;r{iDCqm7Gath{QowV(lcN9Ud0{+s{}zcAdOR&&mna}@ zsLbeDeB*cg;zh}UK$zJc6x_HD&M?4rKnwRt99qoE3ej^~kR5NJYAKc`-L0rux)hjX zF02equZ@cix-#g%QHh4-Zcnmu#t;pwB zRWWZ4xyZOh4qnc0o^&YjLIZa{W~l^BUND6K>Im?p58v1s5G8n>DOBO?4IP)&WlaXe zwNJ&V;}NI%WxI4`!x4gBU3PKE-nDz;Fvx6e?`5>lCI0Zz1&5fOe`K+{cH-3Du|w{N z6e6DrJEVS1*)!8|!+KJ*`uBd%yue$25eRFB^flZX68`q^(W8H#`)KB~ZUSi^Pswiz zWW%w*@1CE7W4CuuwT z1y$g38wa$K;b9Z-7{_6TiUg-In7WXYA-E2}+j%@qHo+7uPFYTuU8?kF1#G5J9&|~4 zXF|mC#e#HPzBY7KK+Py*i(r@zBKZT|LJ}~D67-PF4D;?EO^oJnA*KxF8bk@mX>MG7 z(-^j829~;D1pGhGzenh}=Z)XqiTB$k&ON@diN=GE;$^73Trsk~4Q`U4LnG^u z5>x>*^BNu6s5n6te&wb*=CijPabG1X?hMgK*+jZ6@m{kFF+GmHnn9=*%XnV{+%JAH zth=qhQFRSQ_jXkj`u6>Tjb^fWc;FdueV;x|S1ti8E4qcN=pVxeW#r$6ujr%tgYu@= zrJS$By$p4YDzB)%N30J}J5=B7Wk$r=b{2PcYnNAw7X%!l=5bZ*a9pSOZJ?in)fN+1 zc!LyhLcJPZY4!E#4lOlwjP&fqqPFZ!qxyuguxdWap?SZpw_pY!fYQkW7fMiI6>yHTRn3A)iT?-)Op2Nan(qd}MBnzZ{P`RN2i%;4U}PtaEDcHZ zX|+JrScTGx{x3EteE@%e81)$cjR<2Sbnx7|zKJE2*tJlWGYCEhY%^&*#+=pyAd=!U zSu-G@5Fa$t{j;~%_UZSTi}I01?Nbq>QX5S6?ALhRmzoGQ>A`bA0)8m=IVI<8rA6}x z0D{DrZXEm14%!Q<-y&`SW{SRJ9Y+qUi6KD=n%xTRyM;mspbc-|#cK}|JPCx&PN$!e z0=L4&4SiZvY0Rz2j5)pEu~86!5I3 z0qrV15XxwtZwvY%h3e(iyx-?YogG}&CxQT_(T!MBIhru+9+c~## zv!P)hwfz6+a#4J}5u59L9yZZh19vWYmT2CBybgrMRVTQBljVk`CKV&-4q$fXdJX=( zo9Jmk3aFK>@+^D$tUOUau6`xoQuYAo+^si5^6W(+`1xs}Xn_X6MY2=Nlm6q)hyA-` z&tv@h@K@)%&g30O<-ZrDfuxV)I}^@7E9#G%{MG~2WfcXn+Jc8J_J0Z6tA1bxX)kiN zJ_8m*KpOYwd2Zx{o6J^!DiDk-0KvHD*Z&H}t9}H}K^&yfsDX!k<8ijJIYtPUcdK_iv&=9Ap;QzfcBR{D&_S^ zy|1@5j(!%o>MzBv8oz7b1VQW67-n5bKGN}-r{~KGx{E+FVqAADV;9Q3fyl*0vn-}J z{HA^h6pc!WG%JPl0p1{Z9XNY%;G$jW`aVymT(ZJ;nx%XL z7UOxiAzSY$wYctyMauVF*jrdqMMVVyR0#*mv={VJ($c!+2w0<;bt_~rQqtEsa}qK! zv(DL=nH5s~T))5y^i>P0K+3+#_@~vMYTyo(jB$lgj*V9|`3F}BZ#7gI1qEiC=P~(+CPzJ2Tt64VRbdaOB<8ju5C~FL zm6N0PnHhnq$^lH=O9gQem-kRHzgUVVu$x2d>iN*DN_k$st8O6yaJ_ZbxwCaiKHz~L z5|7x&_?#cfrBH$70LJX}5z|+Q2b*v|kx@9;c!h1y>9o4K@RuBodeApvIVbM*@459> zLl)zuUWKqhxCA~!3~P;lk>%ev&A%n}M$;9FW5u@!m?wQ|LuZm|B{PB6Z$79L-J9NLR*l)MqMn|rm zL{teyuxbdqZ*yn^s62Yv?<&%Y&~TQ4GD19fgG*dqVX`8B@qptzDZU3MQ0S?Ki{$H| zFx#aAj3Fy9R3bkkAAH2EPpzJVS1^1sAbgpl(HvwFwc+bRxc~V;GBx*vE=Q#;I7)?q+QhrmvM=`V>{wLl(jAlh6Wf0tSdmx2S&ybG(f$`6 ztj9WV@ue2wH%0?z6rSuL{)rkJnG)Zm>%_59&b+)}9cMNkQG`vrcaht!vKH0DBE2b2 zpkiGNWb)xaZQn$XklSubd7{jkUOiVWQdqrAm*-k(ywt7mB?u;RU*E|fEPcAlv9#{= z%quD~(s-Q!mGaP`8td*s7I{bafmP|^_gD9jHYT_ukBPr{UyXB@I5e4TCaR5zG6ej5 zAsnt+{4J&oC`DsZndoLI+S04iOsNnMw5!q#DEAZyw!b54wyJhof;GED|JcJ`8dhrs zn*Xyu9xVg?`oNS2S6tR1F)W%0T2iS9Fxcnvo|XJ59(V zUS(J!bP$?UUfusFZBN_hh?GNE7Er?xTtd_@2q z>XGU{4}J#p9VcRi(d;6>qkzy{<%DEPXz?@;?tU@#lHC>(47R{N-Q93R~MMD7@Ti<<`z`*4i}du=uOFy=kqef>_ecML+#)cZw9H)`0&mAhO%ZyiT2 z9_ZHgW6^mQJIr2bS!lP5N!#IhW1`h&@c}VmI_gZkI#r)OG9?9Xd71?u27gXFjt6^o2rL%$28jwHw081TX*uZUnjgz;pWTeu5AuUahWVSV{R>~iAm>fK06 zZHZ1T$Kg%Rs>12ytk2kTBT1N~vYHsBeVI(DJZ9MXKMf}Q-*=bPI1By?P-7!Lo0r~S z`UQ04Z}fnhPvGXupM@a+HJa1BOovC%DoKmr8i1V5>Bx}8ZT}3;P#H!#*=1#U;Jl7^ z5SC^Qe{=k;Pf12*5rhD<4g{ShLQ#C+cgg zSL{5jR;8^b$|CjLe^nz8$v(Zpt=sipM+w#+EkfTEHu-F6y$4`|Us@LdO)nM-M!g8W zD)KoEJQmZf2{7L%=|M`SHTJI6WjAowk*eXCwdUG)A@pmPoI(lDp)~>@vON|QG;`lR zsg9dkkY5zalPVU%X567G*9+k`F7SQI(d6Cbc7f*Oo#urdj8pogfJ3*^YVGACjG9|`n>T249^XDjYXA6#?4vC@4iVT9^C;4Ch^1Z%n|st6$EIfiycNQe zs73pRxKXJ{V?pK#?#SbIl;bAf#>Tp>G0wyS>jIMu^!~#RDoEnh!PB>2s?9mWa&*5Q zAK?x-3XIy0r`Kz}2rgX_Xghvtyq)qeMNS~-Vwu!u>}tw6bfX=PGaI;KquAx^@OG?Q z;h=h5p{mjM+BOHWAsCGHc*l!B%dD_a4LaC=>0f@QN24}u%69guAp>R(7Hyt#+lp|zM zj0ETGxoQ4xN$&z!oL~DFsG}-PFNk}u_gU;5EFzrCwj6NoXcTWyz~=Bh@%`+v z>(&jWO|+>y@!J}%T#9HIYn<;6frRN4R1d8WTQ8+l3bC`Y71krzeX6y7>DwF`lAR^u zys@wKacGs(*YbJwCO}R%NfyfUnXvRyQAlXM4O)L+k6(P)utQOKjsuUl*z+*wW@4xB zaQkPSedDFfNP&T6dsxMs!ExEpxrA2Vn6L4-qsajJ60ZTW=s?1s%(`40FJy%;7p>t> zpnlmR51UGXsjR`{R#O|*>KV~R;@x$+eT>tIrg#hkWkNzq&IzA4`x{VBiaguKYu7R+ zjX88tw8!F|3rakaI^`g;_LkI@P_)g0my3a#eHimMJ#TetlHAa4g*h=m^+&`eLmlG1K;~}2=wKk zA@w_U9LKSMmX;Kci1xf1zxDkhig#IuC1A4V4SmN}(WHwG#JGz2P$RTk{Ho6C(L6io z^H4j(Y}v?)gx#P$=q^T&Sz$&_?W3t4xx;IP!}R+CsPhUZbjWt^cVFEVmxZX|XH2-g zu}$qNHi3w~K@9<0kMbkx9fhWFEA57!&70ypXgl$H|KTu0|6Ez#6eiV7e)7fifpwqG z%UFUlKQ~eNsbU84mc*R^+;C3OJbZDSmhxJmq2)e(qtw12H33nEEOM^d@rNNEZY3)c zA(6o={HccyO*XjmmRuk;L!DM5j7XoZ`6Y&!t&*$+I3;$4V_{XBLDoA&o?f*MK&JhT zoH}6sYMz`Am zza@PNxhllI*3ilqmiK?f>Q}I_~;Tqb$chD9(4o4}GIPdy-XdBhfi9i)V zeauN%A^dk{;EL_|U!b#{O6mIU6Hn_!P*80X)ejVc zI^Knk&Qh}u*ec@YtSBpMz#@?j!_dz zG1};z$o20b14Hre#x)3zh|Wly%jB%06rd$hOvY*;_Z++Hkqxs%!7yS z&}tnN_0NQ?Ev%Y=C-uq<)eMD!WC6uOOc3kE443`Pv(T3a;&rhd|F@H6+NNHKREY5} zN$yS6c3b!NztAq(giT(|lRV`in`RSvMJusX~ z1N#6yjf>ooKG#}L;DJ(=6a$nK7x@T?xe^F_SJvuk6UO)JwyHZ8Xd&~Sk`GleWbtwC z=SV;&kK`Mz6_rkZ_|<&ASx*BP&=(u7$lsCR*|x zr}6YEku^2xeqrO4MZbUB2+$@Ps{XkBk*nReuHdfIdbopqOX$nIs={Ab6h-ZiDIq48 zlBIf;`?J%cJgpP&M1sJm%jaN4w)i6Hu(-U*p9Rz5?7Iq-&$nRCFVc7jzZ;glwA-|o zOkT?$z|rzM&AHud0+J2a)-bt><0su$+Agk%ER6D zH%e310%=CE4eHEsjsuh<&zZ&?3UJ_H@|CItt;$a9^RSIcew&ZFoI*>mt|5g~LqF?- z1Rl5k3pP0;#)3Ja4|M3(zTVjWE9*zL{JqZ1G7@A0=@ie#%?9>yRVr;}K=Y3m&M4>> z1hlY4J0(E@Rf7x%9?|$?k@f@c1F4v5IAW;_ph^9LObydr=$oA_f^SzIBIt3YG*`st zh}yB&r$*meJ{S=wC`_!-&j6X%&-j!81clC+W9J@%4#!(F13f?oe@(FvEk&{yu6P}s zMv-d(>0U8_hVNlzYen~W3`Y=iL(a4AD=O7;*k`beWHs* zQ1$3w8%^MzDYbX!OV?g^=d=kd)ty)9tv?3OC?8a1B{D(){122q`1KVkq5_$zMjLzW_=eqt$7VgHs_(rv?@`5V~h+xf4_iH<*O*c7W> zh_mZ{$Ojmu_KdggHsOtsSf+x5O;3?GL>}-z8qQgrm+O}r{umHnD}2eLktgzoaW-Q` zx)pOc{=>+-fw)%>o+{Y34QJb8z=8I1)Q#U`Ou#)0z;IP6Yjbgp70xfx2ohqRZ z*w#ajL(3uY7{~=!GncRj`GncZ>jbM5JoU#%9Zt%VQhVr7U=k#d>Gj+OVB*mXzBbno z*xj>jmeXwgaHtb9?qoyAtd~a+3YYf5-=R=jnAoF&G@Ea@!LMxcEO-7t%Dy@*%B_1_ z5s;7)0qGKymX<~u>FxofyQE7{xhVG%e`5w+&zt{5~&-MK?7ml!> z{p`K=s=d~IXW*zhL*0p1cUiVtRucjB7q)1`QKVS>%Fi9~46&qx?z@%`*7}o{MU?q+ z0MS3MPF};aq{w`uA68h}+&RDmUxn2nj$hu%Q@h)(>)}<0yy?}iTjF3+NVPM}Vlkm@ zFvx2A^W$raoW9HUOFSG@vUpHFT#?Kvf$bBi4IezTz%nw2NuTGO62*$e7Coy3li0f% zR`xx^ey@DrDL+2W&OF)Q(LdtNWvaa|7SZ-e*UVD= zqkdr|I393uUbR;zSDiN@r`4-470aWX%`sy_yX;|RKBJ>phVJN}o{S<@xC{wq*I@2B zC8Kc7ieeR5M-8o|VAF||ON-Udy7Ky!rkt(!N0$W2ul z_-@!6u7`Ki;(ujjVF?SYG(1qGd_n@01uFR;mbXfOd1Q`&j*decD;_Hu{yJ|MJFCUq zaqIR~7f81e-D<$opIcSCgCCxZm#3&vc{a{ro&NPK*`emafM*IopUg^Rfdt1|1e+y6 z+h4>eI@4=Iuk8;zX(>lGa*6fTa<6{fSvf+@3sZ0sOs&NHu=sR_I?zYyf28R`XXiv>hr~%(_nxRaecZ^^txLq~ zaP*y|C1-1h(==|c>c`^riCj#4=u(BF0h-Uj21tjPoXN6ee6+FxL1R@#D)I z4$<|ha5rSroQZzaFhMJr(Ci~`b<4~8>)MkhExOlBq+ib?drNd@QPq#me?CB-M3K^@ zlNQoT%Ci1^@T81XZK~#on{-IMioEpdJx~CT^I6Wudt0bV6>pA&wl zXT~(Qe3Fo@$RuRou~gZY9@g%i@&_Kq-)A%t=9(HPow*l(=@|L0FADeri?X>R@2aJa zIH#v;3WqgM441w>xknQm9lu_xE42oGS`ReBVx?&(pL$Qn593Z!| z3B1W3qRD<^p534WMeNwzU3jTdxhjDdr4K@Q>Ec~Qp~K*4yC_Y{xBJ`B+{Xp}RQ(9w ztQV1cY?@9>u(HUM-!}o!2pkh}nDk^t)`|)Us^KO{p6@=pt0!R;k_3LlL?;7l(4_&A zueu<1B|&p3bk(g@5=2nRrc8G{wntL#o2GVNb@Ap%G0Yo=%M%%3;l5a}FDBivT*}GK z)U@L|cWN6h0|eV&v|vzdIqnJdESaR&8r>0zhDC^Sd^;~7B2&0RP#Cw`jU8cpO3R|Bk?Xqb<2H9N8Cgp)ceGdEo8Fq9P&*T=_e~tCyG*XOAp~0oGL@Wyre3ETZTfstL(iW1ePMtlv@9hxXZfRJPO3Tz7A|R! zy8f)$>mRaSCGOXyF#~){FTb~a-St|9V}=GdpU$;4hrY?8$riF3g9>-dKeC0ua8q`Y z(m`@Je2Ih>T1lTU3XKbRuDKn}c~n=}Rc3*RAw$z}j~Rw{7E(^68Z}{~ZQ7Gq^j%#G zvg4s#7l-Qc3K8#GV%MVFN^m29%9POY>WSJd$EqX$w)*~}?Bs5;mBWxzR0A=n#nR7? zbMLg_Bgz3vzAfqe;JR?`_!ccmNj0o6a0b++oVcy+G!cPh-pEfO$mO-}>socbQiJxZ zhyL!K0U#*BCNG0F!rvyTTXpxH>=?= z%J&sZ+DK$~?rwQO-kUgz`m2F>(-N3DvI^wGRwoE*$pUw56QKyciE7fq(weq=*~|yw zeEeFkhaw__>wEYXWY^856>N$CS%lVlH~^0K!TKqW%{~skf9qk69lmRFw`Va(7;0`_ z0cQa6Q$|vy1SjBA;M2>`dp>4%>wjLTXL%EigV8)O?Bxh`wH*g2PD&G9Jgk|)pigQW zA5tw~g3lUvPGL|{!&IKKO2-tIDU*D#|zH@ec0Y}bv!xsa2fdiYOfai}q?tpHVh9qr*v(qj) z_`>Me#j*UGae?qFQEl;BsuVuFGvtXTtrr0}N6I(*`s+iHvjXKR0$c6I!R z5Wa=O{te`{jwf~4{;PVPj*U=Iv0T(b4xU9j4?0~*Tf#m7xdHRUt1x!d4Z*p*+z5E7 zT2{Ue@T+M7TtcAonHn>{bam@kE1gCJ;j-gktJVTX#@GRF7g9)swvF4rPx48_oRm{ThL3}H%R%`<^3|f zn#LSz7Rf+H1wwO(-IpW;?UR(?%Uhy>r*HZwYWZ3ng?#Q6sNsqV3Du@BQNI%})R-pl zNRQY*J}MB^Ki)g=q*_Opf?^SYx=6|@*b(hH-e=_|!GOpunefQ1rwmwQ2Y&1Rgch0i z*&p{{2TO#vrIDA{l^p{i?t#9T;qNZgA9-G#$Q|~O9%YqkB6a-er`~rp0gF>JCY;Rz z#;UBu?`yG)%ZOXQ)#N1#YE3xw@}T4bLOtGT01m%pG zJgqt*-IoMc5+j|9@@BhULK1$j zw!#RxG#MxtyRnya_6*$4`2&g?XJgIV=Bmsx%fb(MKjCl{kwlUGn6i0DzU3qjNVOIe z&Pe@H%BPDX+;3Msu;4MBK@=v?!?Erlzed$Ms}m^>YN~Fh*@Na}TFl4#RKew`wxf1p z(x67C<^hG;nF$t*4Azt}j_?AU4_bL;69YI$)4>*?x8O}_=OQXwInQv_62-n}7s&yR zte3fVVuCw_!g-a}W^IHALCwf;6tlg%BU8O)icsJNF~}yw?VK3CU*rXnb8FtqPf7HF z#d&MEigx@d^{_95PdneK2UB)4XgiBv5u4Dud##)`*o6fyhP=u2l-pPi^m8J1Ss&0` zUuv+JIE}Dqu-Wr67<|&Q5W2!?k|Gu4YAT>CNafM_@y*VhVJX5y-agp^4uA8BouyxU z_@%S3P6Smwj^aW27YM~nje+P!&SfpeQU&Fz3IF>r1S>7n=>&p8VrzdW4TyYvk9!22 z|Mj;##v*;Q+4ZSy46gmAge+_C#SDT25B;yf5nuL`?0e*#BK)p?DNdkD1g=8ihXgDo zPHwP@6j>dyFD_h55E528M(`IvTCN;RNn z%9RQ_#2I}N%X#lTB}b}y+V*OuA{=Hf^&-(q%}QqZM0N`byeChPA(#Rn(u)$y2^Zr} zjQmXPR%TwGm8o&vId>!1W90K8sCAyRoa5 zeUUI~htnfCog_2@6}EiwOH24uS5J=7=$^~dw^%BY& z{UV1E^d%W#L9Tr7`PVRr1}j?%%s-6Un4;T8=Ue+BN%{L?3De~~*@QRa>dS7$g3DNw zOE`+fqRD5PP~NguX`QsYd}gOaQ*D@PaUxGwJP*= z1z;4@{RBeC_MY7xqfgJi%9Pc7mYB*gI(ofUGut@0=a|Wdbgorv?Xdee)M}dvJ5>D% zR&x=kRpxZAyr;aznzQ0yz_F-iqC}w_B!KA~@2vO?W|SmgcWsD+;B4Wc>L^}Rvgh!w zytBUvDgWYIgt|eS<1@d=U@!8D+k9(3{jij8-)?wN?QFHG&Gb9H!3KY7rve~Ll*-QD z)(CEmVW3zY=9N@QRO`XDtiqw5!`q*8Jxz-wpvlstYc(zLTx7B9d|fRu7_5aPm0VbI zP}|-K-*hw~PEcx@-0jjNYjFD&H4!^7nzZHan_V146{=S=5$9b!4u@nnLzZ)!PXWs{ zSJBwV&A$q}(pR^iy^Pyd|DykC{F6RYYr1MM55;FN5Us0YI`09u@;tHpa}HQ9ad6$O z+c0+NvHEc*A)?EYZesvoTR~mrpkc>*to)u4oq-xM?`L>GD44E42v4WqNqtNWyToTk z*e6z_k87@*B>OBL0=d}b;SQipNh9qf@k3P(yqJ*Lhf&TmA3V5clb*an^IfE4YBnyi zj!>3jQlD|f-WAVcG+%r?@|4-;rf8sP9p|{bnoU}jj;-#f%IwF=CR}a%2tB?6#)MXx zNI?9C)4CNTu)3?#VmIl6*-!KAVuFG_Rwcjs*-5Y`>e^ktYHgwNXKP9{f(Zvs*tQlH zGF){veYKw|xT3^I-$(OZadk1Ox&?B`8=$mgoEgv@_A@)OL{?ptC0&4bkwTccyP%|! z-WR8STwFha7>;(c`BgP6i#1{9buW!gn7?6Lmo=IX({zg&;e{&&6AK|))=eawVz zibK|2pC_?%p!IuQ?~_j`k5SoTzj=p(ZCVxZs?ArY6m$bglsSV@!J2uwn&0Z6(le*~ z?by~5i%z@2g^1IZCRfWCt-Ya}l`^v(*yF_6oQnu+fUIdR2+>|vt9oH7-e@l`if&g9 zHyOh7lv?PjBj1!*pK_CqE6DP%!tWWyF9R%5y=U{`u{ArIeHtdI=7-zb=P8&}N4u@_ zxN#PpuX207>O2>sEi&_OdDC8Yid_oOt=5ShVk;g1P%>{AG{~A_P$xW%O=FpTYckVkZY!_oTnHkw^66nlg>+nd8`zjA5$YI_7pH-D9PnR)$bEH8DCxc1R@QM-4E@jmz51#0lhmXr}k& zcp&t-L6v&4i!t$(w;*)PxCeY!bREsxtA&_8VITX{{{*?ch*)BtaitBmh6@lf;3}jR*fOBhp;wtR3#S$U(?A zMbsZ}r$g6ktnT#*wjvhBPP4PX2;Pa&Y6FZ3uPKs*+2c!)oU32Aq#t`e&-9yO+XL68 zZp4!uIsV&s^6LYYvdkrd&8w;2r(q3_+S;##XqST82W^K_Xi?4v!}&~m;MF2C$170! z=(5^rO;!qR2p?hyC8~XC>mLNT!@=ssr)udZuW+=ZRL$vS>mG*1?!)1gFhr4gP)@6V zTub}t^$eg?PKP|to6IlNQP-%)UQ!JoQ4f&t656^i%Q@F@{((tEUFb+rlrYS7_Q6@n znAA>FALlxbrzFB<8l(HwS+{p}*muXJUbT6a0H9c85O(8#66TU#{*>QZ&{rK9FA0+a z)~)~6SoTK`M&~DyqW7hV8ju(KyB`C=MXpMoBy82w-EL2r;ei~*yV5x*DVkp&VT;>IeCutOkJNk zhub8GcS+OwvocI^3RW{2j-RBj@r3*a2(q|HgBK_X1C@z+%#x&Wyqb|o;jQE!&H=jB znW#vm?Qv2Q^2DYsUg*l&Ck(W*i9*$4OWwyW!)mkR`0C`*Oo256UlRMVEH9qRy zN6HeS+V)7X=Nlc#+evL(1h5Asfz8O$`*Q5+GZ1@sP?1aBoKsVeQ|La?i1c!_8@5+Z zTg7t=u$$`s!~sbUAgHuiGj?mI{Gsf)p*%T}1)!HTg(^o1TXq?-4Up5Eq?vBMP2lFU z_2qt}Woh@PGj~KwFgvT#Wcb;|(oSN}uU=%xA2pV>x3C#R@xcj^!PfdpzOxy)#*@8N zp#p)p@2mDc2-ZK|ENDM{6$EIg6tU(5{&tOin%v}9E^k9VtA9-Oz6=Y(M&e8WDDIi5 z^&cXs1z6)1xRl==$r*$#*w+UG>6~)p^6LfKbD%m)y6J7AO|5EDQK2=VT?x*vLc z1W_n6)l8QkE#@$kw+G}GS9#SL8bC-LGwXbg^X?MGHj(#T4_*@%$vKmn;$18E@tmC~ zY9SQhDV|oxRsq*}scG}|(sa0%KLuU4!_{SKk|jHdwW_ONYpSXXZHbe5q%+&bp{{v< zzc3+SBo9fbr_BiGv?Cik$u0*{%Kawlw8H02YYkFic@Ls&f$F>gPhQ~mz8(G20iOklX=Iv8ldEU5uZ%}7M7oufMBirSyN=$v8|3;v+ zPcv4vNL^vZ(R2q%BR(-(O0_q-OKOmJIrE7{x#^WAfWOhusl)I!G-O*g+y&W!3#klt z^6l|tMYEf;!w*|V`jzg26N;iI(B4|TmbpMCR^9fga|Lqmikdn{6h0mHomk@$d_M}h%!WZalp+RTVd72$ zF<-)!`xGbA7iwW|dX1)mYtkM}OF4Okp_>6QFW#jD931Gxw~--8wL>jdR(UK>VeX@| z$DFn}0vBia)Fk<8L^ZxI;XOMiKD^ZO=LPUr<`tA)7E0;e3=bqrefKq@5z!hzh}M#* zZs!*1n(H}6S~i{mN0>#QoDtGnkYBESIdp^WIvvb@%V|Z`-$Z^m(s1E8S$?JZ<(|(N3qI4Zi2kt-1NEJg}(l*cNW0)QG-? zs15xI`4San#v8ss8D!dCQ{2z+-)#YcefLcFmI{snqK*z;A;;Hbpkn7+qgHIwfSq=$y%Z9 zOb?MNZ3m~-ie=sNe(GZvsoa;8)k5t)QBqH_SSt8C0EG9;rn6P)SsSS4b(Wo|Q@8|s zR^=_{Oi##hmfNIckUmJ1y)(~hNBL}`?#-rf79A*sq}C0vaEsKPu?Eo+3sWFzf&zbDZnr<-?b-?THe6>+VOCbwfg?2BTrWE(EVXCv>9+KDu{d$dS`Jfs%?h>^u=ph*pAq!l*$3CBk}Xt^m~3Cyqk1xQ@ALYdkZ7MTMObLtn49A}?2e9Fln*zxqr& z1ug}a*W*0tA_^x@1Ln{Mjzy9lb@HUQNRPz_u)Ne3xyjT1*w#1m4L4o<+Qfi29be~u zW=&};oaWCCI4@leNJst`0llH;(U`CGABNxB+EklntHzaJF#Pl?`Vt2_t>HY43!=T; zVsw2v#L{uI`q7;rv{iN@Tj7cigN=)A)` zYz7Xae&GfI0zCL`K+sKr8)7UAdQ;Ze$lz83GCjKI*qQie*!+m)G=Io^QloZp0Umzg z){C36EZ>VfB;1>_rU28hh=&bkMSB(esyj6{|1%-&$2qSmHtvnG)ku6KCsgSurblOr z$e>6Jv@0&cp`Kpb9dlv7$`_&qt#$aVM+?N^bGBeUU!d5XT6V)%YXr*Isih_S-kj$& z@QXGaCP-!F^c0iBmn+z`Fl8%m*KN&6on(V>d`^0#gvARUOiLi5b8~JPE_B`ErFs^s z8W4sdV9^RafVsaXr730lE}#_-dW2VT~a?EEg3N}h( zIbo~HR_n_VEtqxE^o&BZ-EkXNXjZrYb9xJT`}c89ZH9o)bj(7yot@nTurbY%^b$bs zHG#So!Jj@0rr5KGHHyx|m>ro^fwA7^t&Lz>(&6r`EW>>5?CA202;53`O)U;miiM_n zR3iQ77cM2$ds7o0%FJ)OWCcQ$!JjZu6;NWUChVVHl(Bt=nX8+2zr$2)D-CO~>8b2V z+)p2P+uXAl_TYU1ClCX!s!E8G+A^RHVa8keZcLQv^BslOs)p7)(X##g-5Nn{rHz4M zjZJ0Pk5E^D9OneET2%eugy|BKDqV2_T=P;@j|s(TKrIldtF>p~QPAy*#~kw_O_s%r zHto&kG-=m1Vt9~MNHd%rkVm9KT%C1hd2F>pF+Q>!pGCRlz=|~qoR2it%{(h}nMliw zP~JTJ7jDTt65iUIDY$}+Vl+KdzLe{XRajP|lfxUx356{@QO!)GPwf$Je!5H{M(l z?a^Idyr=(pyvhcWcx<$bUVDY6nghhM1)|BVt42lc@Job4J+p1TtaI?pPJ1bQ=3J^E z20iSfYoCoz5iDdGYp0-hY6?%*b4;>e^{Xg`R-OISQ75kjFzNMif?Y-rcwzeGp%D%X zd=E2nQa83Z#aMIAbwW+t0KoX=Ez}(V7srqEvt?bPZ0AiRpllj*Tn=rUH!x;tzr#dg zLpEH725pGzQlObIq*hM5;T|5fiyh9|b-XnXJxd^GTDT^17vACcU51Ok+2Ja*kAZdK z7Zehtdmi?nghos9XABJyML7gdC8}TZTqfrpZC3&yg>SLJ!i;5ULqt8qO!u%e(LX%- zVEpIzwNN2JaaXY}h{!9+544HVNl$aCET;!L^#|Rhc>lWH`;S(&x5F9|M1!#s24^M! zPG;C3&FgF+(E3J;&K3c*sI6gX6Xo8p7&mF^W%beIraTw7QYH8CC4X=hV4I4hmNuBk zNfR$SMf=|t{tpzBe_)(EQ@Ss8VQ2nk?w62tSN}rEWODl811>`jnl8o*&4qmT15=T3Yl}{k?jG9U9P8ZPF;`yoV0+a zuz$SDUwy(y-FMCmYDyqE-c>!tZ zLbFAM{nG%@DY8TXzweF&uu0^X?x9T=82*Cad7$+zumUGSQp7J8UX>7Ci zbnZRLx}PB1!||0ylw=)~o)M6+3M+XlGeYwBYuAt$3_EE6U$xMWSO)Cl{>Ag(qi5VV zJEnsnkm?03k$pL$0>h& zi+T)KkZN2|Xh`@3`_^@l8=vMyvAaZ8L50k?Lprsa)|mg?Zu1qM`HpcWn5nb3x-RnEg7|Hgm9TO%|SJmn`50wp4d}i)8*`JL4l_taYVA>ME>t%@#mw@or^mOry#s4$@pMUaY zN-HPY32Ly0x|xCN((#R|8oRgE3*^6dzZ<-uckh%nq(sURa{~wR-JG@$;8*U{K2PAV z0`P1dIiT&V7M<00Sbx1EWFH0P5rTt*4S^eo3gba;o(5B(TWp6rZ!*SfXCf~TIYO+Y z$=E*z+ps~An&*rDn&!9w^;U7VhIzc8tZd0Z3cEN8Drz?9#%*+3T7EW9p@YEbt2X*? zWPl9wi4Pi#v$yFY;|DMp1(4N{nI!~NY6 zHm9*QUa0n{gb|JD?N#KKdY>^43muP#P4pl=a)$rF;xPbV%TU)<6>?r-L}xfXij$EX zaZ*2mSJ`sm|H-Sr?PhRj2z^&{)c()M=0DFSLxV>_K@ky0bM$r?ta0){?Kx!udbTD; zYpe)asg`p-pN%fG!_9q;eqfe_IwD;w0>yR9;*KY185*8{B^5^><9(~4(oy=QU-&;` z{`sd-sWhYhwNYzUV)iOoH})d`YHAie&8GFIQ0}#a4tz#ParJW_@bjXCXl4Mg})t|R)dU(N9x1I9MF~wskVmciXj#oo+}naJc;SXJ$@|PQBOq! zuV~OL@%GQt`mdAYBZv|&n3qUY{erqcDd%!c%F*4L_)Z8ps~ZT({#)z(c_g@3m;$_8 zics#ub|GU5Na&N~7wm?&hEr*DK41yT)Bk$V{}?OHAEoYfK~Q4C_k83;+|uTLM+9j& zA7wc|wBPOB{WOp&1mB=*5}}0!MaMcCQ7I@l8a3o^B_RBdhy3@fk;()m$&{JeTi4$g zEI!EZ11oqeGg66(2L0{`-YYeER`~X|k!Vb_O|{i`*b)rTAnm`M=x4#+W1+#< z-X7#iHe-;$hG>z^fhYomF2>&-zkA&bX7lN_%}1@n)in~ynMVF*=Y8Tm5T!Xr@~GtT z?_bkr<`pIm*$K7?yp$A6Mhvt?2binRd(_|m)kAn+I!yG>mtGCMdCe~>4bEho#{xeL zf}3pRl$}qGB^;j?{q8W|51xjIbe@^j)vSCYIfrxUR_1mwhbVmICTsbZ#^#^@Ay|g+ zZYiy6a#5-*anbW8KVzH_aVbHeTete(W3cz?ao9H(@$NB7l4wb7yYe; zxL;j(v9vdsTV2Ag~=C?kpZi)UJd24=+?CK$ryQxRE`|ILOA3L*&Qz|6$# zCYA6=!klTCT@{e6;{?rl`9}XvQIVy=F+SU}@Jf2Y`k;&^QsMC4Jz0zIEsE9qJNAkmkg};fsNL@Ju`}K?_GNzGK&sP4DpZG4#iuf{-=(#e}Afa;kxuk?R9#pYVj{eQ%d*of{bn5$ z+%QTx-c}~|cT(^jMFdegz<-2t(<48eYL*T-|E#iOaMON(?DXY#G(Ds3pzR_ zx6>_aaSSvu*r(AS(Smt3Q~&Y|q^D@=R$jSfA+EOA2=Q=Q(?@<|H4Z6B{HNQl)5*%2 zju=ewTcwkwDVS@R#ADKO&xyDN?u!K=&1W17hJ`&x;1(B5B?H;G1?=a)Qb!VOK0W8# z(c^VLCDLfM{={ZID*-TGY4S2#FEE9n{z?7)-wlKVLXRW?li{xbWQo=r$;mj^AxkK~ z(}F$(FXZLrU$N#4S7e6N?ERFTsj<@eRKm!|M8%@L1y|B#71qnfLykQ?amOrf?lw1Y zSY`g7@NhaMPbPtlQI4JH)4y%i|5`ZtJ~)fXl1e@$;MOTi_iipHMmzZ(zkyXAKdsq?-WnWWf<3P`O5pz!72&THyXzq-BJ`+6UAY|d0G{bUL_rTkyx{_8t-O}*V&gnKaq z$lm;bGMIERT?-W*ZNQ3MZBk^`JT&gvP2A1F+DyoIT};U7^`kl`9NnnVKi(?wg8Dw+FGVcBO(DPdbvr#RCl!{?DdrR(cXc^YF zwdzjzWCzBD;34duVIBddmLZgiAklf`#m+|he>eEoZOJ2s^ShgUu{%%ydpBZ} zX@R?+-C|AzpgD}bMMD=v)gfbZI-gtWcK@e$_27&oBZ*yy(!1<)Ve$!d~mSPA*9qh-c?bwX3Ac#xNhxTbMNPak*UzBFTJr? z5@E+k+jC4Yxl=VZT=97HWt>s|zY!O~;%8(-N(i;kf4XkR){`YCkNFfCgk;X!P*t&a zDJK6VSO4q9s}(-Jk)z#f2nkH!_Do#mdUWr?#rpT``(`i7q)Yy**q4kis6^*>@L~D;A7| z%O_2EuoXDj8n+e(>nG7V`;wZ&@WoY1?JIJ$Sqs%s@7DmS+Ea!f^iqGRTznEJ1(RNi zo{e^kO8I7L7IMlJK+)IzhS%oNbEi@hiqch%c-#ShHIB^Y&==uicjVL%-uIbA^XbU2 z;ZhZXeOFN9k2%P3G(x{xKYnfVw*jLcw~(_asc;Bae#NuD^y%~P0dX%l<)2Z{=gLN_ z5Ze~1`6OtbC9Zb{ETucl)Fm43ElL*XrZ@NS=-AJGXc6+G)oEV4`;J++#QXXvtK@^} zovb~C>xx>XrUuvTc3N#UY;rD!gs) z4fwCr8=%txUX(Qi)YF^H6C+eW3f#hwvu=SPg*)AGaxkI4iKNc=YyWwQz5aq0&Q!q0DKA+tnB>b+~j? zDg|Wr*es_6z2eU`6yvUdeyU#7;bumqyLd)FKhe=TJIZOd#5VgehP#)KIQQa|iSucX zxB6}R?{vIkic1WDe3#Sp%WTzTSu3XMbhBvJ$z;lv6duxO4lAv`FZcpkl7*&-aGMPB zQNxqREi9V9xWSP__p9OT9Ndw$TJqDI7YP1Xb0&S7^fny@Ol7tZ;BX{7!SITMO?j&t z=2~Pbk3Q-^Z1nR%9y@l^CiBpK!@?aOu=g#9CW7xU)Kt(8fC5ZW>kvaT9BS8Xx)C8f zn>~pHsHKq-larIFb~r5PQ1R$%u7G>OuWIvdnl%J0uSOPWWbWL%1S&c>uKIvvwN*1Q zj}>){*B!?q*J%C@GT}F4e0asNZU3K1Rk(Q@>lT{2L}9mCj4chlRYoa5RVfyZZ z>H`L-2}QET*95oX_q@IU?5$e%94ngG#3j`#g}5y46GW2Kk(WbSq(R+sws6nj0A`0&R+XT6 zLv{1hHvs7zT>jVgciHUnX&I@d?2(_PemPfp?}3;4iVAv47Un=Q_(4h;{FNjhoqE9n zRQ7RRFhMs*kQ-4k1hABwb@(-Bn+-|VS*_zNW^3=6I|_Q5jnT5^Sc(!?hFc(OCWeNR zVwow5xgE&S3atjS2EIuW5n!1{cW%z@3lm*ND@b+h5hJg#EZ>dG#a@_m-i!cJt zFRi%JjMeSl#BLR#7pLO*Rb3A*RzfDTWue@l3!BzkP&oIsa^qd=W1AT40OFlhmF{je zT@MT2T9Y68;zVSZjLmnuFR3GE=8h$9sCPGk-LxZ&&j|u{^SJwGQ5E+6ZqDdnqW$vX zNJ)Uo1kYCVel$Bsk@^6OSMp=kuFt;QJbr?i2^q1^jZzt32avMo|mUA!kr~i zq~7tbTBUDw+>SS>A(o9_KkQ6!S?@U9CcNZ+VeyFd<@!e-&*fOVpw^qqfR**t#0z{a zskoA4%R^G7oJ5kZTuwZkOmg9GT;icE7d3^35UYSX;hh=pr2fF&sDihf`Q8JOG!O5!ol>iv=$dP1oy7IQl?9x>!VNut6 z=qteb93wE$)KaWjQ`UnqXC4VIR^!ZR#*vA?kH*w*YpXch$n2K#(23UJ?P|AilOvya{r(D>J7k(Ni%z^#XIYyo-owTG=U9fFUEEEgk$l&wD&3*QE};`hXwGhN=$;P%%P)I zCZ_KK$V+?I3oSS;es!{FDF8Q!!Y!MW5T81X0fv#}?;xd!l-2Eqz~YBTA6XkFX5%Z^ zfS|}kc#wX5yFMyY=erV2{Uj@)gLSJa+U?OUJK=Pnit4+3I;KX+6#SED%hqN2`o$c& zl@0G?Y5+|x@Xb;M9aAJ->1{3kJAQlmPeA*Z3S;BB+sFmGcQUt_W~~{HT@qwO@B73j zr`l#Tael7|BFBMr4yi|`K+NdW^-c$a=S02!>{g57$N8gto94SFBB!6o)Madx83QGa zd#~j9S=$l0DOrM@*}mJsWSskZ15$$ftj+v?c%F|m8kTxGh4ewmN&x|?Ou zbGB}`E=3b=vp2K@%{Ot6Ng9;U?pMu&S+Ls6`<7tZ-U;4RBW#1Dwa=#PJZ++j4Xi}jN$ z&CdKmdYco?Ax+0K*rSEcXMrDT9bkiJy_Is!M9jIx{bMI@1GecX#pb@f{W{S+wM>bVJd36Jsq24 zz6}GKQl%L$&Z_6Gdl=M4={l!HuX)2VB2Z?UsBp+W}`h8oGYCR;A3 zxJLoZo|{+~*6ZG9ycEu}mjXQNYkRCMXKwOyeAjO^8Vu_hRMq9lfErtW=$ylzPL`7L zUe1p5^DP8Doo2?~w6}%042cks)YW!l<4!tTcNO&HIp1FU*vgt#sZ)Sx5#Z*0z`t32 zo>fT-S9cUtGYk?|qnruP>DhdYx7JCzx!cD3=tr7nB44Y@Et%uah z`cvZ__m-aPao^SZp`EAKKHzm-SLJZFj%ZHPpMr?!TQ8FvKRhFr5fqo7vbp114az08 z4aoLL-d?x(At&wkOF&$$2;>^fZQQT-^Q0VMy$)M@p6$dTLev8ZSmCV+BC!NydTwH$ zkC{;6*j4nV4_C==64CpGZ9GV4&XyFu9-b|e<^-Uq$vjz==4RfVzjl?&`n(V7S)*=oaJ^hnpuR~FeR`J|oAucX1PzvPy zvXFW10AuznPpZa()zD;P;U=Q;LhE>*>q}fBUX+bL&1v{JhXCJJcA&@DBYJR&D5=(3 zOQX2hips>$*EqjDquOw-ShwJip*sOc$>7!QbA{cCJ8hdWo0i3rpMYC_Tw-$^H!u1{ zs!R6m+qb5rIvADf`;&XS(+fV0-Fz~RH9DzGXC)ilm*1Fus(yZ)gFsl!@oktNZb#x% zu!B^4;ZcR%_NsP8h6i4fdT&-Q&JSwrla|Ky@9^?F`Nv0ZOJ&A#Z<+c2C9+=V zeB6hdkr&Y-B8ju%R|6a4MiLUK2zME&eV*=?7NwhYnL|jcYin-Pm#*e(w5LP^Nk8tF z+-!*{y=JiEu4dL1azlP2eadueq9G81%`nV+Qo$E-xmD>tQ-&%GFe-I-{}K_(^B@EsDMLM-TP5vV740ob5lTKvriq&ymT{H2dnl2Y)rR}O zq=}T}Q)H@J*ZEdW#tSxx9+WFkziF60Twxb~7jj}F*>)(mOcFDl76RUJcb{#Nk^I1? zcoP4ZG-$`w%*&PWk^9~&AU_=ABK$HzH@5$wJE@i2#qN}hzuBfKwIQ$+HIK8{dM?s2%zVtVi2*m5@~zIv*) zi@g)XdvRuCwP(7w6g_3Tx2(tNy<(C%AN)pW9S>@kcw;#XE3U=2bSrMbjnbWv{Scw- znLF-q9U%az*O+(PtCva0WWV$Xnkm`)u`uXd-LbTVSx-sY`0`So>*Di~<*?!)5Pq1$ zmD+DdnM@h*b98K4TeP+C-ZPWN% zZV$0zPVxJ-*&rubBPd|hW_XUqImP=(+;^h4X+WH*HsWKdgo*cI6ERiE$;SI zA#`^=srJYH5zt{>Fsi^_MpRZjb;X+osT~#g{Zz zjK-6cDj1{nb=3hL$FGk-eqZaN0EI}!;ZyKSGTuDuJ{E*-+j@^JfpWRe6u`%h*{S*G zZbA@&T=N`irMfrh`hDVSiD*fMkb1+WYYTN7;KuHMy$oea=4n zT)#0cgCArJMzXTrwVpcXe1y-FdzM1AkkK+NN&0T0og_}4QweOHn-JU2PY}Dk(Wo?! zv1_jcA`;)_$unpK-Ow-H#l;&GmzV@p?WDt7RJaGECB)b zjCjfCyMA3~>AujWFka~&XO=;a#ysc*Bx_){L6eY!_J)D(Vzb}{53_jfk>i2X`B;(S zNT+_0G25JUjPPAlKb+nWO~bcgyt+=)URNk-=~6vI?>u~5gcI`|n)_BWV`-Pvn0Hw~ zY-LL>Ee2g&uOw)N9PbxdFEKzhOnWqJdNm0D$|EtYXAhBx5OzJuaV#XdZI^XHvNod?_Cva_k%|%8l-?a8QX(^8leAjo@php(uNg@Ut zgXuqr)ALE~8y|>y!*~9LTW8rP8~_}?&Ip{pl7|A@=bzxJt=1i~I|>hnTee&wtKF*z zR2a=q+@~C@kU{EoWDc*2$qi=55o3LZs>U>ju4JeitO%3bgcG+9xhcYO(ncn>Va;b= z+@FxV;$+aX(pY_z6|y3kPvS6Bf^yaQ)thzn{=TRCfp-mIGrb-3@O!dTJIj6ufiKrV z8;KL9r#k6OaxK?fZo()fqvrs};W=>Xa#$06>a|DT`afNZC32vz0IpkEczNY};sVl% zXUzWns0qbD3blT~InZ%zU&H%b+6ElXTg@kLCf_Z0bJ%os_kQ)!hj-43W3tK;;Gm(X zm>5R=DSN4}?hkw>jauoei4B-3sN;x=>b4=7{{Jt89vA zyxF#=KCc=mylsvaRAxXy=LO8QMu8SFe%`K?9>Nl7{UeliRIVpaw^utdOi}vP(KQHP z1jqcXO!1QmeaGFhAc%+ULb1s6PO(<{D4-46IkCm!w3_O^KIuxLsl}bJ7barIf%V#a zmM0s1DhusT$aP|?|M8V7e-zJpBjh@TN|La&>kOlH`ehShW^~l&>LjCQeaTVC7mb%( z-~7(oBqEOfRZ{v0d3$X4xj$vHOvzUSXY6d)9i!Xmo+o(f65gH;eYhMmGRR_N6Mb9^-!8M8TUxS<-C0-xqFzhjRs%aqEBY= z##U}CbH_=uGuL%(w=l`2`%MObBD=@Ysr2h??;*@h_sd^69 z^#+x^9@Vz@kjsWd4TcsMj##7rB#DaZ<3A@k!p6V_TOTWcZJWhV@ecGa==rcVyTB7V z+l8+LgTOLzW2c`HJMJ!)I^}64pTgK^|IsM$QvzBz73?}o>@*~KM^_87#^x_ja7yfw z9*u^V>)-HK>0Md9D=)U*jbavpGR(PH4;3^`do3e|8LUF@79G9#yiJh^D*bxLol6fS zi_U(L%kT;DIbkpJj3VPss>mdew9PnfN*burA$3Xy;zHhq=C)C(Jbx>vPaaRNr95#Sx*CVuK@4HE`eKnRJ7S=~Xa|Pm!rW z7A-*L#1zMrd~SJ!ozvDqf#c4!S88qGf(Vf;u^27Rb2$L{&RKyvUzbp)^PDBTsI-Rc z)4}K_sFSC@w&APJ@Eo-TvxLM=4k>~?xm@2_YxNiP!1w>E(CCJOqrtkn^VlvLT6tRD zcW!S0cC+_$-6~ru?{~dtBqBCUOmxj7*;a!)gI868+0``to2`!e>m=8fwDccpf%ohn z@g&HC)&oJy=~rhuk0%Sd@yW6ESTkU>xP7=rA7@Dt9U4Fg9ex>E*z*vcpEY{B{Blem zQleXJX(1kxG~s>vy{T6q;v5MtiFF!~=u;4Kl7^#NkuPdk)cSBc;!G)M=>v@cAqKLQTk9J%vupXT z?`Zl}OEPk%!1b_m&~+JZ9VjK5yq}Za1G7>`~b}>hAf8 zE7$GdQYboVM{fXSqd_ytmE2a9D)^9in$LEOn>SFAZW=ujWZC&(++V)8(SVBlu5j*c z(~B&snP^=`3)G$fMP-ki-2Un$aBpgt9Q`jSYHARyA2{LIigo{CRbv}Zm&+&sGxwU< zLIq}@h;uzKaO-_s@;gs^yFCq=0J5&*leW7LnWW$usBvfqorh=V?#r9B4x2Arp}r-U zX8Xreg6q#9g+5^il;>h;Kp(|-oYhWZXS4?)V7=YKF3JGZo9?DY<^QwZR0f2F-OxkX z+c^(!$gay_?_$a1LnVneNI=^5SXwR>Wrmb1d6bA9+i8aUYg%b%hHrBpntvxuP5nB2 zDW9lwxv!S}X({hZ;Bo*tA{*zbPlAN|Y%yVv7Y=i4KW>J4kw;oCu=iCgBXOhU52>AQ zoqXYldPe}&Bxr*y06}9)J*e%6dCxADQII$??9CCR?H22oC^$Ke%ZPR4-ZYsj(kO83@2{=!1lWP*f ze=Gl1!uRYZUchVdC0F)|%tshGgctSx`g~25V?GF}*!PBQRR+aUjO$G}vxB z%)24>g(S27$L5QUG%}pF693N6sfCQId1mhVB?$WFw1~|E6W;5YtQj&?|6Ql{OJoJw z%NlO{0GOvl@T{ce!PWfiMviQ&!2Qs*Mw(ImdM|>#{-F&M{9%iC$Vj`hhgcs430i#hin99^aJmPRv|&Z^vy`?Z~6-{%Dma1N~(+ zGbFk_$Mn$q(Ite-$lE+(@rxxAs4fARfn}gU?^j*hdO2tLX$J{k+rY1iPjqXF>;R_7 z>Zt4cJUN*lchQvy?}gNkH0>cH5X6=4!9~)!0}__DtzSAn(SX}DvC?X)qss2Pu9Z1r zPix#wAMsX}XubF>6h-UlcvP8C-m_0IS%KcnG;a64&xc%wl2EZ4H`B0Py|y1y($Sn& z9@~7pFSk$iLVZpbU|f%!#j^{Cqdrom*@R%~aNOlUcL(DN1lZAVlV7OPE~&c6r27kfqvM5uSA6P6*YkQBP_(asV;GP~JR06NRfy|Z_I4}RAcmrz zKgINt=~p3+{;GyH(2G@Km0&aMO!S^Fo#{|%KD7GFgT>%1_KDn;!De!Yk!7t#zKsrg z*6!~FAA$r@c^4oSBh@Znf(aeFjR*puMl{hQ6uJOeBbN9h05IH;6W33=KBrp_e>hKW?LcCg zZ9Vb`UD)>h1iDf36_GpDQ)S<3kR{BLwCb5@S&|ufEA;cz-rl_xVmVLb43qKpDIwDt zE*|r-aweilTEwOSIwN@bj&G!D4Z$9~i!P&yB>K&xVO<5ZTdS(2-L$(Ho}BMt3anu5 z)J@{kJJkqC=Gw>gh7Ra@Rl+xeUi-eCW$w+5!wjahu-c7o1dmhclxxiRab5k?1~zgk z=U5P2q740n_RDP&SBgNSkvocUxFOEo1MXu1TSrwQm%mRTW|)>y6i(uIADiIfk^o|@ zRkFhXxdYm}xb-30ToR}-JtON-*$#7{A-8FuS%&v^(Cjo?=mNE!2WHnsaR~>IfwK`saxt$zS)+w?pha4~N85 zt2VpXQq6aQZ0pSP1}qiZUK8%hhh8zF{O^eIo91-@d9-Kadit^oF%Ql^W5zl+(-nf7 zYm8X+O!IDEX*3j=7j(Rxbv;v!K|`)Z)a?N75RgYL-1g?`QJ~pSSZkGX{^}~vMjV# zEn4_M>ROQ=O4wQ2VC^L2?8oT(8{dzWw4PkTcdd3Ka#l&@1;m)8r<8H9J~epODFaAp zXSqLt-Z)Qsr2g1>WUp14dox?wd!q?HC#ZtjIRO9%JRO>5&v=21!?HmlBHwillg4+@ zGXN!Yu@oAdNeV05zRk}Oq}=;V=lUjyqWLuT z_}NzTm)U-LP(;48751^S0o>JlL#qT#Hss@G?EybKo!CO`xURjR{IEMYX1XL%uue%0 zbiW!bP62Dpm9yh)nc6QwWB1V3=GxnF;DShB;4-T`ZC-y1f{b_vsVL(T(khlpy!d+>cZ368F5 zMQ@Dy7j!ARA)})4bnQl_=Hkwnl_zo7kxqJs^D}|%Dkrpl@?-W_CXO0XudwMv<6-IH;A8BhA^?a(Gf^ZAJIx-VwleO{* zl`ol44m1%!K3{X@k+w4%FVNO#lWX|y4Z*E4vc|Jsv_!cG?6_@uNugwHic#b}=RB?3 zpK057fo%2BPBN?f1A6bxI zX_3wo+Q9v$<#XFeHhK=o$~)RSF56%2Faq`Ttify-Jvxu2wi;{I9!;(b^y+e|3I!Q+ zZ2jI^cl3ZL!!dnnoJi)&ar5MDJDw)rEaRMgvnNA#^QjfUt*4)IL!ROy2E?hjlB(=D zq^l9C6kK|otPhzl&^$SIDk6B*;?T1;JT}uB+FOFAaTAOCv{PLqBDThX!l@k41V%p* zS8LEwG9dh!p*ryy5dI1N7Y69FjzQ)Skpu`4GVw0~h* zBo=nxT$DRwrGj*k4k%2fQN}giLln|vHEPLm&n2!imn~HYXY^6#7N&q;)qWVLD+ZoC zg%lS_hxhe&M+sPscWpp6Oj6HX_xYGT-x=smoVa6RYGi}1_fJM}Tnzv7IrUV)(doB& zu}hgH9Gf*?Hw;Pj=zm=2eXzGt8tmdijL`)EO~FKl|D;g)IRd7Noj`{@$US2pz)FA2 z7+Kf|`XFifRri&fM~lBF>YZEpl~oG)G1rowuDCdfW7+IJeO7$di1Z$4IO4T1D*WYr z`M%Dt>*L20cn zi*elie(Yh9Wdk&>C>^&6QSb-hp+{-NH)Ax#g$H(P$x=WLQ=?SngLpao@p(O^YGIKO z`dWc>ciDiO#pOa^h{|rPk5 zLKh9HM$jdhhQ4?n-QwECYaoW)L+V*-Q4=DX+OzozJ8K^cKXtL^sJ8-5{7aN%n?hm> z0$toXux5gFb}fCUvR(Q?7z^A5fN^l`ZU$s5E~8uhYL1^zKVnPq`l2QGXzFY~iN0G+ zrJ3O<72Vhcw-%967Foa(AtBS#Q4T~c@W&#mAK<@>-L3I`p56aJSnG7mse4tOuz!QFUygBN`BIW2tr*HLG zGi?m`#|i`fO_YVmKHOL3>6qx%BM%xo#t3SakNp3Pbh&{@SHNweO&Y*F6|s1*=ngGe zhaCd^4;|KT$3V5D$$?+Q!q53;Caa~_S(;nS9r?Z}Vf_IG=UfvLp}5}m(>u#Kk6M8W z1vVqjy2|Vc_;3tQnE(Amf!i$r7o&LOJeAaclgkqF;Z)qF5HlIS+cKv2Dge*-glFbj z%e9;99DRubMavW#eX9Z!>0(q>atoTe#8K6lDfPZjYWA`D))2rEK-jG$XKwazJbhZn zdQbS_s`>K(1wQjLc7RFowWYn$sP)>?@Ze5=ytqQ|0*+iohHhcHg!{baj4_E-!!5gunDL?tOiOC6mvCmz&!R?H z8NiTE5O>Mu6tF#-%r=O9IPf;`RhYZxMcehzY*)K(5YN=E_VBZ6!D#Bn6ww}#%&ePR zcPP^;iU=ox?wG`Q-cR_a3mSd?8^(twT*xS+Dr1ktFaUgt+Y@5o(jN$6>eNh00Dc+k z&@^>iX*;m_D$ew7(JR+RNlOn7ekW9yU;r)(e}^&wZ1fuzpo_?VLgH6R7ECFx)bRXT zrId=qVfqPjNhpK_cbs&1<(k3~(FM0WLaJeM&I4O_PVw|`)OZ9sR7v{ZvCk#H_kcyo z^QTz~NemS7CiL{Iv4R~1(B)BlXFZw4FCcV_w0fxYE&G}(UpbA!^Hzjv<-XevW781H z0!^jlS^!RhyJp%se6=N!5kRm!5`(yKU^JtGB){QZ&(vIZJvCWIj)5D7#7JTB;4BE% z^#kpd{ivRSiXB(Yb;bKBk1k`f(qd9rXoamQ)RrFd^=^-8A>Mk%3C0peLth@#weV0% z{trvaf)hBkF+v@(GD;}`>oD1mE^q0zLaVWR%kXCQVCL5wcpMR)HB?i$xXfaxEmD`I zLQ05TZ|kGA-sTs=Oj}#eOmNI-89BT95M5pg^eF7)sE3~SqP07n->Nu}2~oPmLV;%) z1%SFg&-Bp-Zg2?muQLW=K!6B)6fn@H$C_^x&8O1D41hf&FL0=}Y0!xFP#TSOnfocc zel>^#$$p7r-bwF)6w{8BYJr^$SAiY&lE^Z%=24-(>EZh610)AVuYg^Io&0|&RqugF zbW1wo;d{#w?*v44&0HI}RaTl&zXDGW08!5hMJ&g$lV(abalVgeD8)euN(2BjFpkZ|TK0wpe}w0_1!YfzU7X%RWXJtSx5Riwvte&!r#j27 zX9oQKiXVYKkbL$j51%ayFt(vdbaRk)xK5#?_K;aK{0A_0@KH{h*X!wMA)u~i+j#i$ z>C)~33lkHQE!CZWbh7|vtrvi|?M6{xA6^hOxfZHtgb@yN{Jx(e2AKrP)W10bNNJab zch*lY@YuvQCvw}_i5|)!j@^yJUxfQuGNfSh-@S>lY;g)K6_XLX;RWyfE>MiX=$fy6 zz0)6#>1%PUTDZQIvTc~bP9{KFFQ3_$#@r7|(jz2bAid-_J+0@llvQJ+fDs; zmvmCZl}4Wm_$KToDGK_=ap^K19@1oKVK;_r>_8XLW4wi7l%{Oug>T<(#gPHx1s>Zt z3gz=SuP?c>z(&Ws`ijD)tgY3hSS$kW1b@ci9;@Bf7GEM<-D*L%?7MK)Vxv*c1 zy}&|0G@n$i5tY^}HIO7A3aAhcc&ch%2KfcT;L^1yRHFt9X{BVo6Z_O=+=p=`ffTjA zdczd?InSL{{fALwlDy^AiALTcFU>OO%xp)wWO>o`t=fO&mW)2QfapRh{{LtTK&S9m z)@!_f7?pp&{1yu&zAyT&lJe@S@O^Gbt>1jtm}tD6erKB7Z`9rMvf=_h(X3KNR}!-% zgG+Sn!!Q3ANc(U03Mm7ymb~US0Kc_BNI5D=O(5CS=cwbVV9*`;y+eGDXiCmbxA1B< zCP@YXDwgj5c~*bDFWv>+pcsj-2&{_8 zAhDo`1`mBe<}7!c3UP1cTZ{-RfbUIC{y%=6`ixlU=fur-i`@mH$;k{OF}#!CZ}P$N z&v>GXEiGc-69HtO4&p|qUyf8_X5Na%o-QREw{BrcFFsqHKwM4VK(=H6K+mf~ufhN6 z)&9NH0xy7_UE45oh_9R`KMH4y&;gKq!ll(shOn73bymghG9ja+BI?c}t(t;;ny4ho zDC2=dwz|44Xwv55x$sL=n2YLE|Gz%&5hzyD@{vH^AGH7&Qt&KwlXC!t>RWjUNOee7 zU~e}GGHH=e!9SPL>4}(Xw28v`+bRscLI==Zkr|`Q`xHwSX3d&&AO7IAYu&IK?SZ0; z_fmZ>9s$7Rxg^c|C(4%OEdIp@<=bh$c+cOTATYx9`nA@D#>V>+D>zYXnBOQGtM_QZ zmjo9z){zH^6W)e3h9aWJd@8Ix%w*W?B*rlF9# zxp(}-29srg^l9n%@=H2}Fefir^BSv3Yz?E}!|&eQNyE`_+`^olkDXN;blZDV6m zp`OOWxS85t1=&hyP7fm!b2}t7a_#8EMXOB^;8eX@zW>MbHpci04wxq^>nq=F9Y>?$ zbCna;l0ajaI+qg)fJ2A9Q#(4f@1F&r=*wv+lMvrM5Kc~O4t+}pz5;P&3xb`#SpDU! zns=!zAhD@8ynIcWwKT%WwV7L>xSv03Ck0=l>kkSw9Y2(f&(rq@4WwD|udC=cv($!)T*`Vmwu*&?^mE7dv+&lUw&S1DipqWbg43 zmx|cn5J_uN8hw;;e=^(CI+lbQ1GsWEqTEkQ&{G=nE!BJSXE38*m0}=a^Zx2@I;41oFrAyI?<_x4%PgauH@NgJnUnx@qKCQ}P(Tm(Sh09$g=xW#tMZljXDu!O>x z4-}5mbA64cST^g3B=1(NpmjWBiTIH0+W64MT%>|+>S~K;yJ#1m*qfR15m(}+z@Djk zeLFEzPe6k)@GkcaH|46;A-_>|7%HWaS`Y|XuL|eL{Nob*`(^GOzbOh-p6Dn*z)o$w zAYcpjrQIHhk5U8*nzML(-!Im8M?OWwH-k11c0MX9mY(4}Q5Y8Y6ty|DE-&7Ba2?~n4iAM7VO4RB#9~GOpBWY`F9twf! z;61xVw_u&d17`~)(XKY-OLaja9>+s8c1hdeN8NG+s(^>sJuGlcleE;ZbmIQ+B=bME z#V`MEE<51;_(Ob)BUV2q8l4?H%LZcH+b-|YotNc9{cmj8DtWZ*iu%ILQPm z=>MpytqxYKAmfz|Tsx0vs%o5GbPBU+xVk=`E4=yWVt~#vA5}9lb z`^N`mgi)>Hs@NJP-lGrFoEDEn;<{N-)lsK7>pQRa=W@E)f(W18 z!2RB)m9;joEL1~Xw#Z^2k(9*uD9CO!l%k%3;j`;A zxGbj~I?ic?+!24S1)k8^pQ@EpBI>0a zn`3ThlY;<`>0+Swt)`#mmTox#gt){j$#?$qQ2+a{Z!hsIUnb|gEbV^s;{UMK_n-Vj zq~`|UK0p0-pV^mTgwc-c`f#&?YwT&qm4zMOOJ6O10Mgz3F6~EeiaA1&sCJ1n*73oNgREw}G_e zi$n--QSu3`+RW9x5UC!BtuCrql0a3)vh=(S+H(olbgk}$(-%d`D%@BcczP&!xE`(B z9Z3)rgg^e_jqg$q{yF7j!QOZQ{L{6Nj5zA98;{x_G>GytW&iIWs(1RHbyej%3mPfmV9JdgW z?$^Ouo2ii;ivSrj3_X)kEb45&=)h)BD;axqrJPGCe5?bDd8kC4#9i3TQZJNmCQG0y zz%{mKkup|BDqlq6(*NnNt@@|kenozzimP93GW-AN8YMJc$-)G!MCwcm<>#-J*nqTM zuBqc3?8Pzmc+89 z8mwZ_&*PL`OzileYb4S%-9OuCOxHgUZ@1N;nbq&U=Cr-Ajk2j_Yx{u3P7yHw2gB|^ z(RQxRZ;Q>TPf72Bc1Qh_y$=Np_867?>3+waBiy@(DfKIgZi5*!6<-GF(RO}<00LAH zdOSu(QbjbsF4;@b2`iuCyZ5tz(xdghBFw)pO%D80BwVRT$*`n|#1)AW{KZD#f}6CZ1pW zqk(`kXSvXbYcW^4n{FIN%j4h}Mi}*+1baM7i@9(t#liX|fUPcp*61M4t!-p(jRPKM zDfntqw}Tvi9Daykcft!i)vBNBFv_8cM-z+@I{2N*SN_|A{N>hwVuH@^;L|ZuUuHD9 zt>#Ut(=bz&sHKIw;>DE&a7wf_&EFW9L*gia(h~l#QHAgeVn#h35x^N)pkA8N0dCz= zmGX~)y)fZu^%IpWINObHQ_cq2HLEu6Wib1pSMo5<%apmm*|oWmdF^CK@y!llX7-n; z&;P(^{O9Gc;)Mf#6pj2SGov%G1k^mX4o_L0JqsWGrRvw6X9EJn^CZPm*)2HzM!!B* zCH&)xsBc`XXlcfOUNV>R)}COu3qYb>daD2p04iJ9)WP)88>I3|MQl}nv}Kv$Kz4Z- zNkk?=Y#JPYJ}KLQ>U)SF4AS(ztXX3LV;q49w2FjDT7JeVxM*|`Y3gq$*W_PR>SFj& ze^iLdwY(?;aL`fFg^Fv*Fjp~;0QGpTx$C88kUW z^cL%V;2#{VuKt!`ZhyE;+H#r7DDiU2v8d> zv5BEEiE2{}R)CVG$c2(7o@+QD;PIPN02C%(Jp8k~6rM7WKIhveJeO5+=XYGvpi@?k zSbdqW{wgrRO5^--Wr-T$ej+931TK6$jtdaAux5JsPjme5s2cC0*}1l%`(LRzvrD$= zUs?dd5tN)w8w5o!nt%%6Xp$<^AJ3%cMQeP{`+CBw+`r}7CVMPWqY+P&>GEEbXZiAg zOLw7im8EY1aSo297wR6&>3?!J?*VDF z3r&l5*}v4Zk{>0{mCxve0Qp|a4YMmC?Z;S}B4sde9=A2~9@YUx{5NIub{0d8b^KNc zesT>}f-6gRv`Y;m?^4`-3A0Vzg{L%yo)pM7WV+idr@0|&wGCoQFOweX9!7M zK(OlS>bB7!UwJ@S#w3J`z*DR2?ICg-z@@(&$yQp-H71D$njj$1I~t^y*zdO0eUDMb z;R-F>Hvxlel{A(Dt)j?gZwSjxybJA~d07S1vy+eI+i4mX_|4V9vL@%1(n(f*J1HqY zul?0`UESSL;@+Mwf!3kW>cQu`sbXjlz;=!q?wQVrEz+6Dw;Xng2c$}wHhlE};Ug}# zG*3O*-2BrCbul|nMgV&shiyKUI*La@HZ_wJX6juEmX3C5b(=gx0pTZ2K+lwoONT#t zdD+g@)%EB8g=n&%?R5NO1HP#mJGQ-EQ~@^{W*4#4qd9>o_ZiL8l@m^MdqS=N2B)5S z4WccP-}N8mD~1SxkEO_uI9ayRWDW(=Ie7MZ%hA5TsIL#t?u&xq1P9W=sf<%ShR|$o&UAIr9vm= z{bG{~j`yyX@%ovCdGbR*9eI1(zj|wZ1Qc>@k^5+1((hzx&8no4$!WPaIzvfP1Z=1l zFbA{=iaEiJ{LACJSfm_@s5|{J(QA3OL*2hB9Qbmrt|h&pTv_ z$1dAgFxY|y=!Qk5xK0*VK6rsD(y7vEPXa{)r@mL*_XU0YGQPlLvHb@6ETh)I@||LM zj*FFV;^RrL*NgO2yw0VSwllm%aO$t^TspPd0H#vu8RMx$FpEMpVIew>SEu@>FJlgK z&a&OAdA?7!fW@?Sd3nWK?gK#lsPoPa@=|I|scC^GQstnit)QMxi9NH%H+YD=03;3md}+L9t!#=>ezKY?34RWA zaGRO|4U`@d{pl(z^9yo5zumZ^S>m^u-8??i(?H_kECsfw8y|dQJTN_qDs`w-4_QEv z*m~0Wdgl07)GI0I!3oXbVUEG##6sBh^}*&mFU08LjGK7W>WJBDwmyJ@)G-WvbGCI% zSl-ge253Y?+0N9#oAReH4(2r>a0!k|+uY54ofYx%nybowr+*mI5Nz**JHn zE&(y0JtEm75>pei&Im?b`2rn^%5jk5%JIhel^}yu$$WzVo4kdz!lIB zKvWnCun|OoWFxln@qsQdQLeF2ptT*mo95JOUsfje(ck`Ko+n~g$K@eP2AbV1S2C z{9D}M$AG80x1bJDS)I;3JDlF$-nKDRMyZc^@Vg9w1FSSlV#du5kTOnr^$X5n&F)gn z#QPQr!Y<`ab8dk29%+ge@5nBv#o#w7JuRDzY7K~2TT;(vba-=wf$yobGPP@E3Lih1 zMc6Lo8Z~bp%;ja0ZXZCVnZh`<%cG(~!Id>h{c_nJUA?l|Vp3JjCyk-)k=`b4 z5bRC4J5ipBlo?o?5bzQyEF;+2*gAYz>)n_pCG-2elZ?gHh=@Wm zrCN^=<_^16s}y4x}Uqs!WIxs&vu$Eux4(j zUBJ=NZepz(b6tZ5k-v$mvo$Y4vD^5}M?q4_aXT$yBC^ZWyL_GcZqCOuhbK6llszcp zsDgxXkIvbVQ*m!H5Bv-=*5{S|9CYLbOs7r1OWgsGJ1kM}!@0?tu*D#ehA^DhO2aoX z-5JQD#B*$Qw6Pw*W-I}%`**dF##l#Li2HRI{mb$ zv~$&wne@w_2E=J?d=G1oXOJnA)1`Ajgy$jta@;Xnnqy%lpBU_<=gNrMI-n9!3jHOz z&>P(gx<_+df!PPJr`4CILb$~_mMI&KLbQqb*CMKIy98V2#JwS1h~o{4ogOteVowaU z`ssdB8S7?UJKYqZL^^4eHnYB0B4|PHd-9P}r*xuPpJ-*Xp5T71o8kbK8{&n;IY7I& znK{}MeBnz7tofFR+$!A8`<`3aC{pn~Fo7IyDaNZp4u2Rj-g@vS&O&F{I`1#6~ zFTl9LR+|1j;Cn|I^iIb%0cYw2&EV_Ma5}|dmjvOLm}|1fS-Op|(z1?2SCU;9e3IX- zrM>m_+t{uYis_Hc+nGM1y!fJWbl$m*JNrT9_Gb@>A zPJV77AXb^b+T;A$?G1g z5FJ`S_4BwWVY`v{xV%&_Vp#zdSPzdX)M0Li>wG5An0tpmvhA-yqlE zdcsl1&0|ylO<(aOh=so@px|XC5w4KLq!F<0$rVD$1(=<2w1rWs6y_7nsqAfxQlYPi z%f`d2*8)*|uCNj*iY6$vHhQR_VVBqUG^9W1xNpVY?x)!PblbJJ`jYBFe28|h)KebQ z#8W-)$1P0y)bSvJ6_lfMDV#R$U6}f*HEiH;Z<*F9gk)?wOF?ecRNR1!i<&u2%qk2+ zwcfqbFg%V2-vb{9Gy%jZ096Q*^lfA9fj9d(_!XJm zg*-3(`e4C~(y0(!a>OcZP;0n8pj2l|eBH-pBS^;%+A2s0?R-3ba}SC*Ij&sN40y01 zJ#F4m*`$Ql94_&-gJ4g%oNhm-w?eP9M==%ypu+8hT^!c$2#(4-Q6Z8}3 zyV`4ZA^|~Nz9+l&aVu^RLU4IigH-0WZ0Y+Zt@S3He^ZDr$7kX)qJ94dpPLv})ny_O zkdk5DD~#v3KGS{RhdZu^#iUi}2WF#peZAMt6=VeUz%F?$nu^Ph}rpJza;Zf~b!?oV@-YENqKVXxHSkQ0*@1q9=pLtc; zWFLuFVH_3yd#=eVFJNX3`$`@0rwpS{{AYXCHoz>Wr})`lZ)qE5f2LK6k_6@q-x6u8 zxNSH)UVesf2_+v9C7<2gZgg0<_I%a>x>)hR1pGmL_G2fXp8f$a@K(&B<4K!>#IKzX z_>>>Cv<=)xbiDJfyXBKI4?ICH2G5tBD@Gs#!ybE(bG);bsj~DVr~JkGPy44w*U!ylUIgECFRPQm*Vv`xRs?BE zO;{c+3Iu69R$1<eUHziMeTTSPqxLx^`r`s5hQu_}AZq?<#;z`ykp|{`9pt$w(7gYU-34el`jqOz= zf3>Aw|7dkjI@c-ua_sx_lhcy3n_Z7X+(<{rkV&-&%_`-#D-WP)b{kd=xM->xmyc-F zPihMwndvWkpOsd#yG5~mun zL-HnPR*2*eF_46t^|7X*b@o8_ z$zh0M+Lm|ubF8c=L22>Ew+Gwmd?@H9>@mmtmSC(;!b-x5`(^mg4G>{H>>JOjvN-BsNf2)6 z4@12z+ooa~yZWd$$m@r{h}*iyTfEN!5uYPHi_n?{djmoAUbcGtIt~MOw#d4ysj7V_ z@;Zas2=LUjos+$~9m4j=@CdGp5RaCxoDlUt8_WGb*$6%3)hm>I$1&T6y7Y@=j5284 ze;{_wbpKM7e;bioNrXwqd&(#Pzgsv$+2}4)DA)L2&R=8Wjd|L9WA8`E;4K8ryF!*G zJqCO4{jRv@?=at=<>`J4nB85KGw39-p$Dd5`)nP;c(p#3aFjI5x24Lq*HaW_#J1+l{ zRW$9@Fe5_m{yKA+H<5K{NkuW0BGn>D2R;!6;j}D#F)WS92+!eF+2D^JZgL#W^0k30 z6q2=A)XDX{__#>R?}4bBAy2}d7DIl9T<5%sr5L#Vmh+i*V0_T=1Q&CZcQqZ;{SSq0>n@>pK;Gy3tPSNPh@gF{3PdkGosttXJms z(V|~v;sa#C^wE`P&XXn5BF2$lx*J?gX>OYXn7ilqL~D2al&QLd&d+j8a`D|^(jo;1 zx^!aOdSSIc(0fWrGjr&Svt z&I1@AjYPWpFg$Lp@5l6AvQ;r^B0%qH7U{k_fp6abeskxNthJij&@bQ6K&I3JqrP$BHF~n`eZ!>mwVa#MJrNV{myk(YpVc2X--6&u?A@ z81B|G;E<_ji4RLgKfk*sRSmHG47?}(A7sBLo4rc0Noay|VtIA{gSvK2iWAtmQ0}^f zyPvmAO>z6@uAiH~Z*JKK^{;P;1b;O(9wjP#`8cL)*O=pvQD%B4b$3T0xZ#OzQK@}c zRd4CutPiE8ifxF9?lne2P;-OxDsvE>PjH{>G}%HME2qi~;W#*8&r=?JnZiT!@y_RZ zgFKc6PI5X1ch^;Fs({<5H{26$L@G~wuqi5E81TQ}IW1DnDlcpP@b0paOVtgIQb*C% z(S`Ks)_E`OeM!6z?w7ukfVLyInKE_!;QX2NEW26zE<)hjd}et?K}E>FJhB7gBlY<> zmpj8PW5{8aQ0O;jI0p6<)P(Us?AwPaU3@!T^bAwDaIbb&3pqMBG6TO@J6vM!AzbDc z#anoTu$GPb`}gwe%I+_2m67~RfC&hI&7i|L8@1DMc!}FQ-xSZcOFP%Tk0njJ7_#Vc znA_TjL9p(!I$vAM(PLWItE=@pl{jVmy_HKfFqW5rZ?hYYjXRbp zQZdQj!RDt_XcrocIMR+jB%d{x<3!lSQbcoBKx4X4uP$B3#ExfxKAx(_h}`1zXbDzK z4zjXppxqDOwfB>KYYz=yqzkbjcqYLqrCR_uuidI)pFAw}Zb?rGOA($}PA;nY8dTs> z(}AxpuP*;FX4g+VBsn%W$-Q>Gxa5JQF1eabxl_XD=K*x&iB!2U+mqwEjmOvPh@y= zUbz(-2FGm(KF@EsZ|lDG3|i6L*{9wEMA;r~I&R+A)k%j9m0V|t;~GL7XM#u{tG3~n z7+L}j5?h+?z}9#_@DhPSX{wtZRy$)#Skqoa#^65v@*VS{_s%^wFqt-!wmMQN@XAj9 zC*M_aRimZgxJh}^>@YRP0j`4|`lvyJprmF=`<>4g!7qq5g5K#hwuW6NY6tuw53ZJA zdeu3UeJxaDjl1SJ)(*_6%6MsJYDcGarrNm1u4${;fyN`I-Mc8&n8gIY9XRK~nx|ul zRO9lclBbKtzgD=63;oOX6f{M5btI0*Tu?0^32{Uug6An6&QjNZ3=q zx7ZJ@7wUwjfY=Y5ox$nNUSFFgY;`PcuAjCo7|)Tn9|=LVz;AIBwRb3&KgGkE zIMxWA-BmwV`|cykdk~u+iHzHIxg~p!=cqTQR=Ko6`enN#gjjdxMW^{z9;jK`1XiZ5M{g~%S>M9}FCF%w4C zllm-l602D{{6UgW&BvYePf^ zBs!i5&kJ z*ufI;+#+W$IyndX-2;8m!=xoh;WBXa=MIK+Y&XvC^E@K4$|Bcj zr|0JUf@l4HMef3_df*J%`W+gQ?fdEQb#o%1PL93bJa$iM=o#jjt+r~L&vqeSTP0egfG7bp zZdJM{|LhEOxhA@B=_#!;4$qb zWM-YmV_mNP6O8jU*HnZt&wYRh!dBW4%OKfJHZ#0o$RG#J6bZ%Mew$!x2VNH+j6=S9<<>Bo0f1k7<1@rp)^P7 z5LZ!41Dm#=?O~wR$~Gy-{>@_QaE_goEi@2j6gu?8`OM&hKbH9WL-UiL{6+bjz}|Gr zt+4=UJ7Zq`+D4C!B0%=meL?c3+T!$t$2!z*{isDIb;Oz*l>uk=W}3%zk{freM4e}S z@auV}HPmXsx+tl2ieJFX!DN@F;u*6dI`;R8+X|-lN7OtxYFTaWABaPs={M8J{`<;_ zMTIaqJiyGNQsyGlqce^++vqW3_`zB z(pGC9t=-i};E~PS;ddlhNB#d@a=((i0U%gG-b``yrW&NaGPVEzH$(mx0s}fo6B!P+ z^~p+z@~Lr#eZy^%ct})q4Khdd;79LAy_e(LhpQe9cG^d5MWc8G$B=T=MtVF*<}^5l z=MB%8Ez}_@Gvju49v#BO)$K%S&T}-zJdw7(N;}nD1{S$*C=kWIyP_h5tu`C$8eL0` zEPM8Nec^FN4?$Ry^Wv4MSRKhA&}_ZtUO_c3Mp?#ZdPLH&tIn|O>TF~aUKDuqy@a!E z)CxcuwJ2#AM55oFV?Ri19imoCnJjKeDAH<{;wX$% z!txt8I^+!P)hOr=HcKTXcl3Yd3r~Q9Kt^)ZTCewrMM^1(jQ(fwF&fJj0&x+OX`yI+ zYMAr0z!}4d=rN0#4SocQ7tMKkd|OUFyCpGmcdx6FJB83o1-JOojt%b>!~ov_yc^hx zf2dXUQEd$UI-EN<1w+Pa4zt^$aV)M-#!|la3A98;-%+>3gKLf2qb|6RBYfw(K=+Ps zT-a(gjI4;3w6a7sj^kqaEdSiThod*Y=GrdDWa1>FCVS*tzXXuyG8k;Hln0S&-3Og4*2AaKim?72IQ> zhKIue!hA-%_kDX{C;CYL&ZSkwML1tL)UhE-)~IByy4n&Fd{+M&G4oV`r6|GfE+eZe z5gr4fa8AFrX_&DSlQ{-M@sySH_}bTQ50xByO19H8*eUTp?)fi%`Z*EMUR?3@ls++A z-fx|-M42GJ0&som5LxAU)N zw;2E+qcw-hV8);k;T^A}jjtooyembk0me32Sc>X5NIu}YVh%hj^1IdY(wG`HyJkR| zbnbMR32}cW5|smP*TJiUxM&a93y z6kPAp&Ma23#I?w45k)XuFs{?ha6XvGuUTw@Tli=}5yZs<^<~`25J=8>_^6U*_WY=(3{FILlB~ zs0w9=4dNI&`U(g3K`6uS{8s{81Nv2B!;uJ034bUYsL$ujlNUv;HY9-IodEg7IR-e? zR`uBmp+kI9)R+5HANTLK4%kYktk&+Hpo64WuJwjKR=BWVg>XD=Y226CRW8cT99iBr z#+FR>o%1k8)rC*t_~{@v>!6q&a$zd3>*i40H}B6Sc1kVUAKLLAkMhI1mP-r{dvxrg zTsJlNDffc}0FUzHA3aLGk3h@u@u5m=p{#|AO|5PS`r+#o90(*WeqoA$v}~a}U6qLl z#yvCgdMu))w*yIOJ<+g%iwR?)s^#X~G>O~zQ+eM>NYr)v#ZD%;m}qWWdW3jK%I=fK zQ%A5gCbUp;jzq_;?HQfc3my9zSB|-vyOYyH7(2LflnCX7*hRmDM1i}PRbTy;wWc(# z-bkD|tOX{NGxJ|0W9$JQ56J%xoDfNTZbQ>eGPS_-)PdJ-73*p{V96+hgK;BPNtnfA zU+Xd*o$|EA-CC}P&meRRvybU1IW`V^>G>|HQsXB_d_Rj907FZG;AfT{BgZHA%@h$R zM&=oX2_3=O^qA)2F6^dLIR3sogli{$Y)@kCeY<&It@;FUwE_p8d?K8x^diOhU^9A- zUqqClYXi(VV;}e!9YP)`ydDx)H2po?8*x{^7OuN3q)FNGRN5AkUv71i>if@-?#B1L#ZJ0l2$FO5mAjh`S;H)nnLQ++S09=o(uNzrCnhNv!uev((`|euWP}7;2t}UCqfE8X3$#9XsG@KKx(Q zp$I3eCwHz6jvYLhm*$n( zKvx;@y0{Fvd85W6Q|Iy{OJCEmt`(*2DcABo+MvDyx?)Vz(ve4dAEQp&>XR3U;kNBa zb`{o4VCzjk4-Y(OPWy5Vn-X_HH8IHNHX2(P-4HK+frC8##VV2e!uYH`=&29lM3p+` zEt8o+Wm++|;HhC)a2HwC{aH62=G=3bs_LJWxW0O!c!TKIz%uLjsbEg0u4u^P+*!vo zRebKT+g9!0#cs74Y+>GCOcwMy5idrSw!;AJ2Zq7Uj!L2MWE{z4P@sh?hGO~mCrtiL z*6vmrQ|%Mw{803^2;OU3)2P5|e(NcnHKXgmUA3oGitz7CBZD(+%E0iXwkOnTx$W97 zqf^kDvyN{cO$~WjlK+tZCMvM#dRk)>5km~+Bh{RHvWh3KM2^kUORi?n8o)Ed^$}%D zxc>bp-U8J^rwOUdQ3e~YX3cy$E;EU>vT*v-xo$P^>gCmw{?$?AajDncg}!+~`Ry)1 zGf1I=eN&yob{Ee#-D}8@9Xo^D`m>4_x0L*xgPJBfXifQ1Cm|*K#03N@&r1!kY`^_q^xqg_u3ojO9Mx^5 zk(Yr^s2SuYRi?sTPo<%c9dSC21uEkS(QBQ0L#(#5XedF;>UzC#O?PO6)C~lp=Tv5Iw-vQ47&_I-{k-2YRE^L>$!yrRA_N;tuHM zU&xD^RoXCYX$|Z))}a^!r1K_8t1qg>@w=NK?kurzjuIWfj?L%VFBS-}*9!{OkV00X zsf06e4|aQFatkk^qsnKP>qXNaMd5E8-_hiZvok-xu6<-D;`l{eePiq@GF&0#@mn-U z^bmX`>(B_^otKnvlb_x`baJ^0Fhs0bP>FT$)}<5xwpnJOo!qb#*?BGAZes2|!t&`S z)~nq=`E>j@tTtU8Q4+yNeu)v&$p=RLkpP8ud}_9b=ZI9~UG}|$QI8xn%kGIFwL8o<$54S$6D?nPjmA!{Ss^m_wJIp*_p7O40tTwkdD2 zVCuv1HSbq@_DPB-EhhVFv)hN%mov+9y~vQ>A@RoM&$`Ecg-qCC`ld8T*VsxcCpjAtl7qT}4^phLWdRizLa6{O_X7$*=;@`1LyVakxIY{9^aK z&9~Py)wW4y-vs9wRO(|fB=kL>OjV+n13QWqb@-h{k!$N7PS#O0X&Y$X!U+fusD;eH zSxkfxa`X;DwRaJbYo2Pxu4r`t1$_wE&B&<6Um;l&9zU+(p+V!lTamJszmGQfl( zR-7n^1B8)H@NKN1ID$g-&}lzg<2{;eA-@~tHOovUt=P#Dr7^V?(AZ@eI)+}S`yIR~ z#G>b2KbFIrjEGgH>=LzRDaO>6mu-ASMP#AY^R**H?YAtT562ZtjM=MrzH}mZrGf(P zle}S&n_>*3mDMQ?xHhW2DPb>ki>95A8bu9SYF#TEe0Rm_rx_R{80|O)B=gU3vB|5+ z4mfRRw1w#XY-{Cj<&z4Z8y3|we0PS?^0l6AoKahAT}3VTV^F3gW${OE(fjhQxS{{M zL$De&u@uDE-sFE;V8&}|BlL3<2#`wWNX4y3mZUXyy7G50dac2++JH6bZo7iQf_%%UT_4KjoOeQ(vr5yf+q4Ev^JAwHVSM2+ z?6SsWGhL=|H+g9LqJy|P;cf%>}Dpuv|PRu5YP?)e~BRNZ+)JSSRt4mp8=7x}j})2&Te%>ie&ov!E4 zoA#&ITwn|syz38pV|RlBb!>d7%{yap$Jh9xgk~&#=Rgi828$Ch0FQeW33A}6Sl|) zmFFk=n)$_6#wSrk2s*-&>QtUYf{vZfc#%4v0SQ^4=h2-Y9mKcOSXC#V`Ewy6NKnV! z@dI+?vj@p=ME)WF*Zx~Tp8V$9lcK;#vJZ|t6@{@=3Cp?#{R?f+7457L@zIg>Is^R@ zMvFDr6993il-ExkM(H#Ea^ODdU1sjK@>yQ#EJzD|uS6=}ttWL=OE+Z3aVE$1&?|?H zA`#+M96`o8G5>@Zc8&thXCyzuFqSTqEOOW6UiV=M6Lm}PqA>Go z?i8x17Qs#BCAv2jK_QYcV)l(Sr=6cmWr>C}DY7%YAT5}5V9BxI9{rNxBS6kW`xnmQ}wJ-bxu%H8&U^0ZCq2Ux5GH-s8LeerwCC*D48eGt4N^$xjqum_{;v= z-lxxix*mBhy;1ZIdMTZYeexov;iOw+QkRJ;o@b8lux~+@_mn+C;XhC zAQ-A7<3sZm0d%C)^LSn8Gc6LN?tS&w;NuD)+)xmM{kQ=_IP?(}Ra3vCUQSHxQ}eo( zA!vlmOYrNwHWCcB!G&=FHlCL7Y@)sEHkuB|Mu`P`G8A1taTW6eiczf!Q&#y8#WXV1 z)w}wuNela?(xY7zgGeEX0GH>nbqv-QgrL0|%Nc{Bd%d8f1i0PFfly-eC|t9;8)u1IoaB*QSUHWvuo(Al~(3-X8u$E88H{)e3f%cD}WZ9;-YX^C~@{lLcq4t z-K~gfi`O#AY{4Sxk*5wsvD2@_ci$EF{j6ysxL2xMd1djCQ{YaE$UDV*m{!kObj^7C7yukaLFv~9%iDow&r&So3& z{y*jT=szF#tRF}rUf6;Rlf2f8x3sMSrDE8x=w1}p!+{$tY7i|GHdDpP*%9=-H-cr$2R8?XDG zI|VRp)Ivdye=w#$eDsw7xZstcFb9(WjB5+szMSr|2iz7!Vqg#LnAdUw;X&NP~yo5)O-LOWB0r1n|Y+S5`$?N z7LmWRiT^%^i+SL51~tQFlHY$kIHk*`4l93`)`L*=ZV^tI2;I$%vUJjNAdgY@&Cub|NCYhzWGWb4NSCoF2CZ-L9U7hhkoJnAYG%6}G4Fe##aEN>^NsRzAV_#C-2Ol4d5P?Pl8E@$uJ zvmMDWf(e)@Z>K-R-u~=T=OQqGAW<$Jq(cov)uVwTX9q!uiu@+tuH&1tR_n%<6K570 z1dYumyBTb^!c)yrelQ+Jq!m(e3_t>07!2=or?mf%{{QqML@`=;C@T$9 zoc_a^q6N&K^-Kzu7qN6{=XG{dCb2l`Yr>G#zQAiG&yYIg0UI!`p4Ns z`i%d8JpQm@{`zrxAMW$UKuW_~4x2>^Z2I^FlfmS7q2mjS#Rf}05_RLqC0y;{wBsKw z)6jn!hmNm0>*}4g?WsEq(tqj$Of0MG**J)|>GD}D<=53@dXY37)8=o&)l5CWkE$(B=U|r&JTl_U%z8HhnC! zobbBZANJ`#`AGH>uEu(XJ_Lt3CUhTPF;k=-NW~KY22GfFhmey+PCkBS7N@}ai0@^$ zxivvlgix$MNMT>iEXrIGtKAB~SfIofwVGYnAmwTeY)FHu6`gkOdln+b0Fl495YY%h z6O&w!ZnM;6>6{+7D)`wl0jC+6abGN*TN)z=pM7CA7r5X6WSYu^0?wPC`C_3Ppv1D^ z6LeXiVqrAV#}1P*cT^sRP+3%B`;{Kyrqrl6x-<4sfau>}+Fw`?kW<5yemP%qS_oK4 zkzy+t$(G#bX3ufj*C2>ifG5oLB}bop<1p)NoVf6fg+Ho|72~&%lk%PbE30!)A9kV9 z{_I(&R}m4}-)-EFkg%v=CJI&KxQ_@l>Xj9Cz~%cPzlIu~&H$kXwP#rPl-1LVl+Cd1 zQi!$J)%UMzY&dUtoK3DKO{xF>?2&>7a8mIga@z&XTub%JPX41{`}Kr_1|C8qQVtX6d2m=Q?tH$@JYkA{(e9EWpBfhg(-(qjya@% z@03O??*2?=aHuOj=;sKTAA+d&|ZnD{`LXj_K0_g7j4V;uHB26O*p1N!wvOI8aP z%0z%8&Vo)OS1^MX=l|sUBHBixJcp-m(Z6Z|7zt~r^n;NhOic4se~G{UJu`q8844~C zW}~p^c(Yzhmo0X(jPVcehzv;(Tg8>lel7GxEOFMi$9a@;-?|`Gj7}uc(cp= zuRWN5|MtxbbO2?#MF^$v7BDa>GU8eP!(|U6P4+K%$zJ~-co~BK?^FNvL8p%(fF1cL z{(~JmiT^&%zWX9SGIcMA_ZVbAuT%|C!-ELV`;aR730}n->QwJwwXYKxA%irt`pI zRP=nz67$by;j;@m;;&gu`M)Bp^4Nd&hh*q*6%)wpviG$wluV~)jzeF7`3jAg4T=wHZI`=u2UH<3F|2@#wlYg-c=t>bOI!0}MIOj-G z7y<8__8{u_==Fae!zBnxAUb+#7>_@dL>nkiP7zzrLN_LtR_1Zj$kup1|Mw2suWu4q z80$E;ZmEg1jnHq#tvj(iJn#2+ZCC#wfcjtK5G+ITbNrTSSNm$f`a52Rg!#whn6|S$ z5?~1u{^KPm{}uVm$bq}Vr%mNR=TzB{@vlorME~oGfB61GR*IXWB4@I#y34S3xTjJk zX_drTG~H%5kMlw7{v2znH~t(nMASYhevb)FU@1LEZ!i%>}ZYq$< z2kZ!7qrJuS5t;;Zf>}r7437(Mt2<{JdPLtxz@0v4zYzV_|g zlA>6L+cl}cRS#TS%YU!o|8?O1=27|^sMVbHoz?J+iIGvhSXO3gFkAKRcvE`c6{^4TnK2JzE>e(yE^ad%&X}Yp-f<&SkxZGY1zM@Jwy&4 zptOp5vqxN!F0}P2T!|MUrI z5@2xr^$HW^GlKCLWb8iN20MGf|8-OU`5qk(MiXioCmNC<*>(PuPJu!@ncc>qG2qFU z!#6uwaU%OTo=|}MbLTxE*x6QbR9=6HO$o!}v==*5xrtl@2)$y(!U*$&T-;a)$ODA_ z9!IxQeuxYO%8SQ`5pqgH6sdAV`%O#;8>6r$NVX8agp=&-y_nEPhDho&)YH7d&ERqN zv7BjCp$8X{0qK0N_@N=z5i_iRKZ(;0h=Tn|oX%e?7aK+Cl|jrvHGTy~(_wt?FQLk3 zW0uC|LeeUhDgP(l|O@}*!~)(1!~xhn#AH+ zvo{-O1yW#8DZsmAv3vBNC-dL%;MZ^75+Pa6R?Vp8=H}%|?s{tEM`=M;Uk?ZoV-|Ui z*~`cm^X^Rft#FrhpBhLKURhmuk*!#XXreLB#B1_C2;XT$K$KStO_VD3wn6xN% zr>r6g*b5%mm3{n=OtA_YbmNXJ|Bc&VW4YME9GlMdVGr1DBo&ipVlSXiD8cS9zf*wqhzVp(wpen{xjvDAoI`PG0VE_f8&vGZc%Z!NrZj{gFjyX7Z`+d=kc5c z^$yIF7i(}P0GlM7`^!DPe=vM!BMC@d#{nbN$iEeXSnJI$u+%kT!saU4^E&lE_ zj{$~O$@by_aC#E|2XJEd5CNjlR1PO@G&pdnx}o`0fjTRiX*vDhJQgg^2L_r;oZfGC zIbUBEi(E&D4rl=;~5cGm|!JL0R_Uvq{4wF1PjoCw0m(&4hZ5LgsOe~OUy== z_%Jx@#s9F1X%DR8xD2-D?-S_IMaUgzlEYqE&hsYKy{iW4)V-;(?2l!fW6d!NdJj=6 zsZuU^3)W!EC*Mz*ifiX)i+YtKwch80{ns}V{J?}mlE=!>gK-xA8|cC_$DpYlZHDp< z5&DoJK^lI{T~08I^+UY}-X=V{Y#ZcLDO8rT=F2N28ZFU3h*ST|TUC_MJ4n5HW!Pn@ zUT09Poc~s5XJSs8o4BzBV6i!jRn;quk^mB`r)sK1n-HQ{ZQnfG)i6aBlF(a}zrUKv7*S>BX|-?EHCOV*$e$P%h7se2y3$d7Xxw z?&?KHtJkhpeu@uZ`=n_AgfzW7J0JFM|HQ(b8wKCteekTI{s zl5I4fa+a_Gbas@k+?35032KWNxw-gmcOta}rDEihUA#G(hz$)2!{YUEKiH${PbiKB z@_a;r_DdbLxV#5Fl}5WYRtk>%gg?Wt(BzaKv;OHKK@JmaNmz^?@eadwjHOZ4b%qdm ziQsO{rf^zC0m;?TbTN4ruE{?RP9@Soy!x)!6U&DiyQN+)!TIPM4aBbb9<1x)4q-Wkfv#fP4;< z9?Ho2lrVJZcKAh0cgq?8+kEzI>_!0fRCYJh2tC-vEqykSPKVv1$2MD70MaPeLFZ(T zdaq}Jtynte;5V+_;MAMp9m?|Vp|0P=kA#%@QOtXHxrc^XYu9VgC}I0T9n1c5ilc=) z(tdGL52m>qE6&2hJAxYrp*8=8c(kQ^tz7vONK4N_G(gfz0IiC$rwlI=RRWOMqkW}x zy^hng+>PZWRmmMYXOrmOmFuNQY9dIkU3zO8_Ge``hPF;>+gd+2A1aOmE)PQ{RodTkkq@vc9Xj*+?L zJk*tDbANnC#Ugu)5iN0#Cc{;NfMb46Ge?3%X-jgU%FplZl<7OpEf_bwa7XwzBK*M$ zI29t*O8_y{6FN(&=a@o-IF7cAD1aCoIjZm&uF6SCYobZ zwREqKtOtAB`SV}}ej(IiA?s76r9p|88;H}*Fum?&l|N@cd*Fv1&kGnYJMkgr6Me7V zGQwX7d`{zH`X2Ea3DP@X3O{HIG!DfjpI8v+(D3o6f9|i$iGT1XAuGWZ%**8kpA$d{ zj|A7Ny7-;axIg}QzvE)*j)K(tbq`)xF*U5lRs&4q!Z$S zunX6j5_z^<>dVd!>taq^u}U zV|XNMz8N@xM8m?~~Lp{$;3(RcINs`B^T@4U@`LMg-)_&)3jJ2K!JoLbA`G=<(c#-a?@0g@)_?RLGBx$KT>!@>@rN} zegFyQ{x;WD>L=a#EwT+aHFM(O-xpP+Vrli|gX-WBe%S4d<16M!8LvIh27G`V07Eoy zEod=vZ(X11T|~$vznlmym!A5VT7JzD9U&8#=3p`$gTl#e?Aq95zRqCa>_bXS z&O}%^XHM|VP@4M5aH6;Id68>{koK$|`@3BZ#%$RXFw#PyElrc#8G+43w#guYVwTBS z{!cZ0EJed8{Xf%OU@`dsgja8qGru{L+r2Q~8L{qeEpaW-VcIB3!r50X z;D`hle2O(YO+AQ}sj5pnJ7~^IXq>w@+@9YMbgiAzwm(lw=#L-Nq}*8oUVHWzo=`k! z;ejtuHip)D{yLJZc{6)EE8C=);K0C?l%bxxW|z8b-t%mbpP=eaS;^^pD@_job<^CA zotT>Uv|JB5yD184{l{CEJjn9W-uuX8a5H2Mtt`~{%pIPy3z>?%8G7uO)9ud3P? zuA-&wZRWOdW(rgh4iBWC`@(RzGld#7bLjoEAXxeyX_Zkglp~Ho7p${$QI+bo8N^X2 z2n|Ap*t8t`e9b``B{lR4MaL@c*BzjnCL2t=fqsA=wc=c`G0^|<`a&6>e-BN)X-6Oi zZK|!zvgx2;j-8_uD<%L<;L9Y=Wbueb1#e05*w~m6fCXx##yMK%7cK^DC<8>Q(Z1Bk zRe#I;z=_a^$Krg|q`Th=wAk~c-c=EeoXatR zF7*c?WMUFasu|&RE{_~Dyw6o!uY+(G@{(@d4_Z`?_@drrMkdqJO;Y}M49drs>ZOt} zBOicw>w3n9`9bcSfrm(kNqIWal9+k;S;04?^?chy*q>Af+5)wV&{Pq9{_s z`>@GqtZT5IyRb3Uc63Ax>(+36=>*W&=0Yq=T9iq=cYFd(I15oioQfL=R;o}bl!K7Yb%3EH@|R!iD1s*9(ER=VWG77=b|kjd#DP{ z;N{KjAsTtdkM84!2GRs57BkuAk zZZC1H$b(P5=~dbW`&*jAm5A>vF^4ly}CW z0V?2eQRs0yfhIROEP%>dE&)WLB>prC^ZKzWd@5!=K1sM8$H%orrrm>)NSJ~RYTt6uPJEdc~3u|2F zt6%B&<>%+$ERO*}6YQ7Q9!HNEP%C>_Iw;@9`i-kLUrINVaPjFl$1v@>E`G7!<4<|v zaEH=t6`MaIE5%;h2L&-Ffh9V*!cy2`$(Oue0M4vkvdi~7nvF2krMT!EsXhVBPhvn# za7er(y5I`Q`;LqyuB|#}Zf9Px%|UHiRV~jiytjLl2Nb3PNr^FFEMZDpM&9p*&cvcL zC(>z6=Dq9q-mh=#lo-vIytkC$cZ4MC|8q|LttfnB9(nTk~6q>dKx!lx+DWJ_Hx?H9JUYBPs7XS+1Q>3%Idv z;;%r#Bb!ACyUn7A8=sfWeEl&pilW3qvuDH4!zj~1-!t3rR{f)^)7y{M3?=T1PTO>$4MS7l}&EVeU#m9fK zTQ-75)_ZrKpNxHrWG}vf*~(8@U-G^YerTHPx|^0TFA86gksoPp__-}?FMnhyrCBds zj{lA~EMV>}na5CCrC>cNfqm=z;n|}7kSjSV_*=trCdr9G%ePyE-LX?QKatxRlx}W2 z(FR98QFPVu;KJi4X`zwHPo3R|fvk7O#F9x~VE23WiFEv#y?FKavs{e!`)HCpZS#uq zmyb#yQOLm!X1rVNUbye4y>+i#r}dl_20W1=EF)$AqTGK^Bfz|QV7tuguBx^fa8#iS zoL-?1-jRLDuj#$jWfjm1Mmla^aeufnv z7;K@i_1GUP_Yd(YQc|Vmhax{evFsn!q0>qU?OdUG(_3(gbzu(X+|=PrPM=)*6T1~+EM`r|r$Jj#VMX{vh)ymt76-FCa-EPA=uuQx_ z`->i;dQm#f#WHK2wV@x22-Y!khxM%{;+AOp9+?BlEaF2gMo zhSS;Q=i|S?cP@C|a_V}^lQdo+%@kT&VV+KF<5&P;0nn=fnm>UadgyB9266L{!^6mu z`g4QrNxt`+PztfIEUJ>c03PWXL!R$f7*4|0Rp^pPrx>!%frHmeg$_N%?%!D*(S!S6RzQ4QG3 z$=>ri>|l~F>p+H_vAw+ZgHU=5@7%6SQ|p+3eBhb*NNTt2AR5&bK>_OZGG=2B=^PrB zBou_~=k&f4x^`!x&HI$BK+T%@?6&gJpk>|0@Mx8DcC}$uZB^0(yO%P zwlQ^P&FFA@^K$jHG%mvnk2-+FsqE?9CTw1~riAB4nCbeSgS>O!9_oCiUYt%yyzE8< z%Zgh4WK&^rLQhTq7ldBbB`>@OgP@q|Xgt9^(tW zipKu2T!1G;8eKhq)}_TK&B!2L;JEDBiost2{_2Uv=X7r^YW^P8)G zY%9^P)HE_NF_HpLGI5>#Slifl7A|pA?DV`ki96Y}IPAUjrs*A}>Nno#Q`yw0Jt!#x zRYq*NO!}7h^sy^Q3}Rz2{E13VFpBzvGy0U4U7_IXj)qDAM&eYqAm+e-3>i$R$-k%= zJ6q^PV%gmUzeN|?X}A@L*Y(CP3Hd2n(tM!Ej)T~A(_hS7-q-XiFhFxmSF4Pk`uUsA zt&ziMM@d2!S#yLZ2u*Dy1@oM?8V&%TZnF!A{pSN|EC|!huG+3Y3bJyvg2E~+ZeP#D zl`AJSbYSRI*7TGNvc7OPg_#WAHqN%Zxnn6!d*_3#c(MBp>~(c3y)=d{a<|xUm&Z^y zYINKOQIslHT`;??e^z0$TO#(z9wi=3!~g(347EaK$R0nVfq;OJ`73LU?t32faGqB$ zP*LwTJ+Y;D#D8>?sD)^BL=dyysiqLG%wF6^=*9tQWwUgZJmr4l3`}+EUW7Z$8gmX! za{yYU%|HznSOPo~P95h2kGpMjh<0A?S4AA0&N7!4f zacE4OF%Q4h6xW#XCXMEBU^V^AwOI}F z-u2Xr%Ps%qaI9#sHca6H`AYid z>~QMm$(+(%&J)xnw_cOnxPmVwo~NYFW5^Jzx$1a*%9iu}aK|eo>3XWdT&bYvRGB;4 z-e<%oom)#10B)K{OoWxWsx#FlZ~qh?T{*<1qT^mEKhAX!+J*zb`<18wfAF;D-3E`n z$VJJ)!Rs67qDzLy$q23jILA6<+z5_u3x;l9QdwgE?5&OK*~T4ALXT}b%&s!@Q8?4> z&#{+v-{&o4550)*a)R7_J}M)+BL|2Eb=L?if+2gfOJQ9Yp1*ae;1BCX8Oa#PmsO|*-MU+d?r zrfU*Hp<7su^58W0AJ?e1=owe{zN#@TzT7#~R_&6NeO45j93wQZQgP> zy4$)q^g4p8B*la6NOlX4$5Youn8f=BR}80X7pbCYsfLL_i*LGF$5vLE-*_I0_d}V! zWDE@N`?Zw(^rBmvv?^<-vfhi}{)0Y%62wh&F8=v5t3HUovGVENwWqT~-N9DE4J8^s zRq2_#xCYdWTsQ#DuITLouGggx&BmGPA|qCaGGarw6PmhTMA~Hvn`ITWfnoknUY0<9 zp$Lr2u-&fgMc*Xj+MFNxZzT&LNd zH#Lf*Ya>IG<#Qr_%6>w=G*#vkg;Gs!?lVf1HQx8^;OjF_&9@gW)j~}Y#R?(904d#`Sqq zd8V9=NOcTq5S>J2kwHPVo0hUZvocl`iJVZNWO7HxZXsIwx?$aScLVS)`&t9u$J%%y z79Su((P!Jya)yW_qOMMHdT-GdO2CENCu*hqFbTEZ_ctM@u>J1od;mnw+tY3cyBnoE z8A7;S&sGYr7G;5gmu0)EnF*{WmmkeknN`l)p2m&tvLG%-AsPDefIFfaFDo22>TpRN z=;72Q(zpz3h$gRIu;SFN5uaV4DYkfNn_5nDoyLYZ`mZc0_!@e_HwA{&q5>>gH8=tN z6FItjjOKC(O*XYd%emW;nQQgZqc^;(b}^r>G@RUT3?okaZwHi^qZBB``g2rMnTPrB z^Q|}xEVde7f)sI?b?Axb966`%-+!C4Umpn&|KX4DCseT@jKB19gUtl}Sv&K9=tFcP z)ms7Wp`k#Lc7qP}PaaA1SBQtg1ylLd0KAO0-{=#2Z`=MUtSe?FoR}xn6e@m)ds{N- zDMTT(^AMU6@W4f`b_Dpe*Cb<>JV&pm@-F%-06`F&&4>t7!wF0RCRmf9(SJo8$QW;C6JH^tYD zhpp+Or$f7xeG3T8Etk0%@+lk^Mq2KGOlc~x-~r$^Xbjw*-w@F=#Fs|vL@ZGP-jwlx zTMb!@5WPX1*UgzcjKw^XlDL@q_U+vqZP+RPD$DY1!p6E%L}dahP!{Hi&t+L4=3I#< zA>eP|Vq`7?#r$$A>%VlS{vT&=9aiPKtq<>ofq+UVAT1@`-HIXz(lu#m>FyGxyO9>8 z8(~fw1nHERbV)Z-lZNk^uD#b@zjHY5@0|bU^>Xp%c*Zm0zV9J|7{0LGI*jLA=qvfO z!nrt3$4HL5AL`QH>>W7tk}nPh6TqB0tExM6?NH|gDm$kx^*Y$%i(}iy>s`I(E@iXP zw%(WiR0F`Tju67&!o7xzIb=#;s@94o5SVZao0 z7gtLA`0P3~nq1Y*nYF9PyBCH&b_0Wic=U?1nBzW9w=oZE6!8PanLET!#=P3PgZoXK z%aadYUg2ko`tBiEO_Ks5TxX}R9@OQC?v-|nKwq$%autL z?keQ080{$eWIVZRi6JGS1vB?09&4zr7Vj6{;dIHH9il&tXPx%$a9wb4>IAK}UK>kM zQ|%p9PFLf8|D_8jVfadJl;FTw6K(vUbr#2JIXQCn9MMsua)C0NM<>gXaq2dMN9wlm z#QK%CPC4z@8ZK{K=9Z-_skj8=bXFfF`^CLrE1Z@$$e3)(?yAVPxMq}DE1>M<-MgOG z#J>MPWMh;6qd=BBN zTcM)J3?sgbuRH3ai7Fm#-0@Bw%ryBNU}RbSgH)f5D3V|WplXPD-JByEmU%iQ^084P z-_Qa=!JSQP-{G$^Bm$|Ak*++W8$-1)zV__m@dl4o@S|rAGKN zvW(wg$OMe8;i?r#hF+H^2_jZ~eIB2obEHr^xRrZvm;##eX#j04uY1`dyHhsJBuN0Xas`#l#ZY!xb7iP;usyPYdm-P&$&n7_P6vRm_j zkikM*Z}#0X#h+vxRFK|gCf;k2_+PilOLZ?^Qf4>jSIDHOSGNg5>!<2Wf|UxDnn;)3 zY5|8;?Iz}PZy`F+DukOq$A!mFMiS-h$~lIJYDfi_4{?srU#^}(w;hs zs3My=V*Wf<-7&JBsk9t;#R@iW;|abo89POSLb<+1W_FfTXJY#zzjlsjAbON`UP`9w z+aCs5dhO_WDYeA3C3U-n$zBLJyIC7?jtQi%{^wpawxvaiXj%$c`xgCoT>3|YFkU8N6jfGnoR4^Fy>$?%^*s$Xi+$1B)i)8S4T_a-tp<%t#I+6*xJbl*p< zzz@X~$6@{~~-cw$ICy&S|;FYV!TD=M;9;ip#-|nZ2E=S6>Wv*>kj7lo7@3 z4(QeY14W}X8HqNOw!W4L9_@-ECNB}n)z#zD--4|mC6V{CY#}r|wvc`emy&5;Pm4z6 z#FG4QV@HHry6Iw#3YU}oF}Kx#lGcG=VGB-m7_P?S*U|~Q;-54fM28c&EwanrTMWUJ zHF??sOYcaMKyvz$*);Ihj*n&=Q)<+UaAS&B3ep@KbVCDA`no+*OE79>`~)@$YOHmK z+N7T}NG!D73!#P;NOn2>;5SBWx;&A2g_^Rnx73|=PH6r=F)<#y0O(?rF}}kSa|zx0 zPf6y}tFu0ekQcopLne`ltW_D>k(dzqG;#z>8rizBlQM?5&v0#qdElFPqLF^c|*!H1z<@{~j0pk_R+cjO2%E4qF0j=}obghgF&J zW{hfPp0OlSGs@%+I#7vvo^-o?MY={H;fZ-J`9wPT$v01;r52U?f5x}nvX#!vzXOVH zMgr*M!>J1KuFQc^8;qRvLF@az!*8{{dq7>R!$d!4|0xnD)t5!q*#{S);;dmm6J#(k zT$dbKPcvvA#H6aXMWvfQ==168mCZUw&r{X$EVz5ZF4%@la!>0=G(i78GR<;V^O{Wm zFKXKuGYGX8)bbPr!_Lu>}R^;MTmUDX|JZ}$Nd?4^+rrq$J%ht5$-mN)F%Zvceg(?<{h zsAz(Xl)Xx+psC#!H)6A{PLNLV_A`b30};cJRY`KLIQh5XRbNE~>^GegZ#Gk9&d2SI z^vJ2!%XTj-M-SC}V>c|2toCb{9Iv^B&oxx@4~AC8{5x+fC)^=}K7U-1 zL)kn*ROTH9_en&@z@xul&>kV@P5r&Thul0c*-kjtQXZ};!9_S_f@|$e*p6Nxa@r0meVxDNn;#?s2tgqr z7&ElcqJC|D_t(K6lt8KFXFH<`GxWXLrGq~D;bYm0%lXETNcH0ziVs!iP#Dm=>R#G_ zJ5x_SaIyH^T6*l=Ooe<~_8usCljb21LZ+X_f70u3;Vug?m$HVFo~7OgvNdaRdL+L; z5i3$Mq*gxoPI{kCK}e$1syCmC-Oxw(hZ&4|fu?THgO#Kx7B!elBQOE1FX|t@!!ZF6 z@89`YiN~41+bp`yX~uHSO2T{hm{%aBu1hobwa#3VMH{}|!rS}9;7Aeq0dTG@1OLsi zePX~aNPJ&Y3ANX1xRbRuQXedBqT%z0MMy1W({Ug7?*H}artT->iY@@jT%GoUh)v#b zyu50XxOuZsjKbrw{VuWRAZi}4URM5li|W7s4oU?#HpN;Av`}wD37LYoPmo%u;AM#< zM3CBf``XWqZB+ot7il6~hTpUJd_sObB1F`Kv*EEY4U7zcd|Z4G0T_%G&)dU*UWuq_ z;wMPx@87sR(!V!F@a3BrjCOuq+Dadv$7E8t=fm^^6Mrm@HK@+t04a;}`5XIxFb1T= zYxh8TP%dn3)5sQxtQm^;kHuXES4@`xPJe7qMidb zLs+}$;ZPkr#i>s8Tw3Mp%V#gt9e!yzxtP|gw+V!cnufJ!RV6NX-_4&REs4%V_9CYeCmZE(XTPxWwre}8hwwVb5etvEEq zev+M|LVC}Xuec{3NsYfffT2r@54nRHs>O^2PIjxgwd*g}bnOpvq;DVnV3;MBGyU!t zO}^{JRn6r)`HU_`Q&!*Pf8awpP$Q;gkrpcX;y=Yg3cwxPa$3K{4-NYK79JdJil8)% zET}$L!JN0^y>GKzUr~N( zBW^)_0Nuv$@1!=~zK*M7yPU`s_~2o|?{`n1Ft7qO?-3};$BOIoXfH%=-t>jV;jP3D zKnjjAwa(P(f6Rcr=g%#aHZ$9(h48OCxc0{nq;Gawh+{#Ip)>oJ7C^Mo)F(G#-{BCp zxWWh3UnSd|&8K2$7B1C^MiXE~u(*6>SWtIAxBgOo@7~d{gEj|lSA1mH7K3a1t?5n& zw_Ej|UrvE8*?=y=?=Af!a?AR$x|H`%0jM0L095btXA0BQRGHvm&a&8ZQ_VNj`XQd@ zF3>6a{`XaV$sVRmd@!fbs&jLW%j8lOW-``(vuy3@FYLB*iaS-G1gb4dK8?z%fL1O$ zKjSqhvcENV?{I*{?KPg~KMQ!_{be~)$ot6(Z6+D>HA_tesVX%SDt(783r zO^Z$GeDu|Kua&1@{&diF5`FJjD0F_Wytf1TGhqYqrC#L79aOFET?gs@+b&`L>ZW8; zpCUvbpW)*&+4$?)&|2rruXvM8`5;O9fZG=`2m@jn5jBolsl;B0yK1#~QjEg)JaWr8 zEi{E29tS6DVx(1DvIJ`c3&etw369afBb>io<*$S0kAquvmH6b# zR}U0&WYct8h$lsyG>SF(^5j3)3>WB#ua6Wh;Nf;(Hv0w}4z_gO31$*PY^pGN%!$2v ztbxLDnL>SojF54ES!_X$%zf3t;cv7 zWE4+=B0u3kmHcN5gLq2;$<`oT#Z(Ol?&Z1udqIxPTX~9=Fircl>TDff2PRk&pSQun z!T#8+A)wAKQ~DV7+MZcO^|jP{VwJAuQfyO(E%31I7a zMst<&GR#L%C*Y%66rj;aU$J2{`7>KOX>p53WcbSC@tAnc@~KpmFlHRH){}4kAu&2$ zCzRWJ@O)*^9OjpF-KrPYRFgNDavOb0aCD}#tZ>oCCx)0Y?$4-VERlbT(%H5aBxT7- zMoDa^X~nnx@f-RWd%-m<384A&#CtG%|KXEvB?}D#We3ImnJLTqee+wrK;>PWdP*S6 z!aEi`?}hADwD)s7x9GQ%%S%h0XN27;m@F|R4H`>JWG)g8d{+k(Gd5pRblQp{{ZU!s zPB2IuxUYPcrMtV|F|2nF8YV67{jpzXv<_!tphV?544Y2TtXzR{mW#Lj`czYQH@wfP@seCtkb8>r=y>kz=1A$~3v#%uPrEMV z&o;B2E-=7~2CzF~rI!j+Eq1s83iabCVOWoMHI^oVk~-|7w{u?_f-k~K>j(H zS*CXVd}S5S(_|3<&rRFKv;Izg7N?%hI?gLE;qU~*=^7LLom;^rP%LyxlQID>UW}%f1#p*q&6J3q3-o*kP$K{K_U?o#+WWgTj8%OqtWuhh1g$s z9(xv^pl@&P%S6|^Zf{SocU`G;>fi9cGX)bBY*@Mc1Mm_z#fi0-M5om1U7@Zoa&t2r zgUG&!Za)8#T~fQQl8}zqQykOI?fRPw@vS92U-j_NT~c6 z=zZo1wOnTho3(E(amUqQAmZBhk<7i(}3TDa}@3KBb<}l(s{I` zKU&zhSI&%ru|}e1|Md2tgTEw#)e-|^*p4}kWC7<&6pETy8`C=8$y9!X&T2(xNtPL>`_0mX5UP2(8)8$;Kx|v2J7sCx^j4QZGT=Q3M2nmu6bjsTko6DWN38^spPbd>D3mrXs)o`mwDRtiD2WU`bjvH(~kwYY#MZ)jO(rEzijw0 zD>8H$yXz$#h3uF2YYWxYIHEet%{W}v-2zsrtx}*a)}EEXX)!XU5;%EU@&O&FKq_o3 zuT7N1`q`WzEV7zMaOeA=1l4c9!3--Eb6&?++pTmGI%rJGt&na1vCk*3-eB&VptIB! ztIV(o%OTs*{!tsg7s_e6s zx6G#@o=+t_)&p~vIC14_9d=A>xG&NO=zNE<`|B9Vyf`I`gcw%s<~{QuY`m!m)Ey6c zmJs(|NR}|);vab#`(4?e+#n@3hG0THqqBC!Y=hR^DWvRNB!Sg4s!*+51>gy(iqx%$ zR@uj4;TtCl!jtSyJ!QA8cZRa1Re4$`-_|l6T)yAwP9+*>t@)u!to{JNdyx08YB4E z%Ln$lq^tUBEl;+hoexvI|arvSReo3NN$x4k)24nT64>NZIT zuZ)5fy=T%qx0v>{uq_brx=?Dr>YI8q^|R(Mm2?b3Xz`#7{l(?!y_0Q^(HHR(hw<>} z`Wo?~fX$QHg4Ojl!Pf5ecz@1VLU{KCJ=^3NDpQnfqvqPuiSIB7#*a@M`ECPmx$^c_NXn$PP}RNp1=6br3{%-ZD>Ji)^oGg!36yTzeW9%Hag znlK-_;GV$YmRe24NMcoY5)`9SV|k_Rb*M$DDN5{m-g$--#ED2oRhBpmeo;OwdPL}u z6l8xP9WQluA6rV2c(Q@avT3TGB2v7zf2gx;xfa-ex6swzCV1X1HC(=K|d!xIRpfht23?+1hSLtnUkjV3V2^-Y}z{ zkJOd-t7J#=V<{9aZaai3|5$9y=`w}B7R&$Ke3WuD^U%20e1-UC{f+w*s@iT#799P$ zv3CmA6i*R>Kg7^@?vTxMMSx1>oKc~}}Y1t&)*xqQ!`Bi1=t3zLA3UndfAonEduX@8qeK*I;kyMuyr&yt%j# zxu9Lxo+QJ$Q(oH8R(_*DUpk3GXT^N-OF~fiXlo($q$fT3Va!liJPj$4(M>~0?Lu1O zBUwlt#fga0Q|&J>c}uYI2Za-+5+79L;c%+HY4z$d@i2d2lvHq{^(c9u$vl;^6Ed;# zJX>`A%~D7>MZ`}5Z%pr>=FJ?$Xk=={UW4+stGc^nu8XEfpKo{L{JJ)1tHc|r0hb_b z-ODUwotwOa^Hg{y?R*?DW)8F-ZG88grPSV_A`-N7Z~WY4V6vUKxF6Tqfhj#qCR=b1=&n-o$D0G-fmy}?weh&DX(to8;p}KiL$y3S3 z;k*7f<&JTx+T7NB9jfxf_b@CI%erkDMf=H=(blRZvFx)wvi8!T>794lV~0^BG*ySb zxYUI!=3hMg-zh0hL$sYctLa}t584JtKGb$;Sk{bA-MHz;(a%5rpikb|JE$)pHn41` zti9OQ^U4#UR1O{WhEm_`8S3M0s%r8L2)kKXg@x~R{=?YLoGGUF*=3q$MVa}yErIog z))PFM!tSr#HEA=2Mn4-bPW=T>e=4>O4nXf#T$GB22??*xdB9J%zCYrrfH9_6@Y}GI zr+lv7E_c((^BGnvU+&;iozj!3-fokMu3>zcRlrR2=7mGc8wW9WH_TG3_EQ1n)^8q1 zJ0{C?FM9e#E+pS{`ujMHeR}3my_t_8$ly}7?kS}ex$yiR)m8c92~WM?;SOkbZim}D zbw)DF7Ve5$1wWsK-{%;~bhm6GyF*SW&{NvHc5fZlM!u<|ISjA?#e&v_dPTjZ;m+_Ftn@Mbi&N8gZ z^Ti#9OQ7v%F9Wpw8a6{a`$afrao|dDkqQYYm1x7X0v=?{XL#teKT5fPM`}qDyJ_jQ z!^UKTMV-T@X%WZV%q^q1B8|rJxxBx)Mr?0@@5@$P!J757M1z4CVBltwa1W&O_n7dBXoMM5Hvgl*Mi_8lj0i}R?%bI-{s&voLG~9vR zM~`Q`;oJKoa7wfgj=<9ZWB$|b9Zu5Chm@k@yiiMx5hlBDFYhfjpjtTu3wEm-G&^js z5gHGrVaX!d{;YU!<_-XuyzNd6tYo}Gx0Svn%6G}UH3*x0bPHn&Uu17C`ia@sB2^;Y z@i+}~)b_LO#`xrc9RrP+0d5i-#!0@!4dGgzL`jL1Ot#lnie^?+qo&gq-=-Unip7T| zo$Anww-#%NSoVXa+g2hiR}(Fp2OB;I^qvX-BDqLObJ z=M<&iv>xjw@K8ZWhJ1D$*xg6XGnr$z6iwT2ntWFw6Sj*@@ZP)p7tLxQ$K}LJNl{sX z5A85KPa!V{sUj|R|KPIY(roL4`jLWytg?Dy3u-CB|!yqYi1IaZofu;rTmZKf<71+SA6rv+Q0mAeW@)7J7SjQ7ac6<&H=LXSnh2Gsi(gkb1+2_RFt zs?Z{cjTyn|YbwX`#`}fcUKdN4q3w0SZHB_#46v;n9EFm)Wzq3;DXN#{PPUhQB$dK% z<}bIs-mpAifXC68O^!5;1bs?C&#O+GtzOxjvC6nG=#MmD+}cn7B)VQoPPrJ|-za}H zb8m45)hh6e>C1(W;d+K|aF%VXWLp;Bo?WAGuzz*^^;ui#9dkNR^4g`7tCc6=yyB$! zw)rw_7yp$Gd^zW>P_1~dQumX^Nl=x2TorcW5J~{maxA2(4)aLtvoEE{!d0M=)nK_O zNr?VATyQ$7tXq)}A$RZ(!(Ez5%CMBS0puvA=oM|XW#~Y$!z}`5F9G?)>`y)vIwB2l9Z2VDEz6{DxykNA&OUMn5Oh{B(}IdZc9Kf zvsugJiOGUrC<=aYfD=X;!P+0V?L=>rAxWDKSPu@{l1eJZZ8WO%x1A<@+g!g}T_jl$ z&4yfRFGvf2E>7$2ipU{*MZ27#7%sxE)Nth(a=ZYo5$!AQ7r|ax=G*pDFv!GFuwxUq zn>Id*Q$WiL*l^j=!j>Nrp_y{ryo_k-Cs3U#(y(ISne`0qMa+j#ER3&EmPOPzpQosD zE$>b8qwd=XP>Kbsh@1IS8cdXg37<}4lcX~@eAKx)qJb3s~KDjV0CRUy}w$|jK`xDN==W2jiB@` z3vp;NS&5uThVkyb0URkx>4Fi$QI_p7sq7&q4SbENSrUO|o&-UY{GMb!jpcrgH!Zf1 z!%UC4fz_QjhOg(`&`TUv%Q8ei>Irt&Xm<`_b*1<+4M{)X3UacMZUvdP~s$5q(UY}&$GKJW7@Ckc8DHoj%= zU*BHnBIb%9_#*m@lm>OfO8r@sq4wAY2YwHRtxn;8nGW|0a990AZo)oQbH98`&12Zo zqMsnwGXn0Ljy(+?m7nN9GEdBB{ zCuedo?bK=#LaPxT`f^|M%aFn?fMl|kDT+F8`a}xZFdNnKNXoN4n;r#Ysd{@F9A?o* z^Gj3jN{W$ZHXNUC>|NHTNr0IpOT}(h=<9y*s0>rVXW~p4_7J z%swAe0;b39c1Udz)gGa_u9l5bp-b6Ng^NRcha$I~1;8S=t3K=tINZp>*QUXMlNjni zV}zbE8C}q&G)&qFpU&9XGtq~2?2q0oAwU8Ye!bQG^gO(S_7-178U1_p8 z4MadB{+Zsujoh}qIV`?~RgS(P6gXUmNBVQB6i9da5NqT-p8T6d>;Q}4m-Th*VaqJ< z6H;b!KeCn$pKM$^tu>e!UYieJN$L|apyZ}D6H!ZWyV1&&IZ-{2po4op*%u({Tb1Po zyN|wngmJg_E07iX4#`(mexO2gQY0_Gh_4VCzk&E}WRnRt6^c0ODTq;0p`1M^ z@^_)pkT4|;Z=Qe@F(%e@*9qa`k!4&$>K#vuP7beW^Ti7I802}>V%c7Z%e1%M;PsBv z1Se&7t?p~TiUgFI3MjJC-9VkD@a#ZpnHW;LRYYM7)Pu6*7c4@m!rprgZ|#SUioIxmA!-^f zNgPnucu5l@+U2b&VZiGFjLP-%DrDfDkn83UphrG68FE3#Hk{8UfpdP@ zFqmU>dkZ@T7_1w*Qpd1+R zF`s<@`Ud|SOeD>nQ!>?U*$`KQ(gw#OSs^B8A4Noi{IKy#rR%D%x*77R*%h(h-@tC^ ze!+58#R>piE=VY(qWwT)TzX^j+0Q6!Db zN5;xoTakR43JD9)NSU%Zx|c2Uoos3a$X;0novT#9N)lkZps(EZAp9PWg-Ax3?Z(@l zsrT6iqB74WMK6@*AXnFZwjq^Xr$}Yiwthg775EYRL(h*-~td z9p-p|bmxSu%Cdu;6Yv?^<>tqB;kSxOT0+}S+s^xyB$)F5pHEDmsqL~R%?@Z2Yk;S2 zld>IVl7I2%7k0p8yoU4Wo$=>#HZ;YRuMa9u>f{2ra5N~6vy6$xn^wBauuI+8solBe zk7_?bWiIoX(BX6vMH9)+u5rH-!?#=%*S#{XEUUD{nS_AGiK_`dHfFba(b>K~+*D&_ z8v@j;ZmS`8a2@=hO=~H9g?J_nX8emCLT@NR3-+CNvz{0F$r~~vKOgP>7#9a|V9907 zW>$>`k9>$y0NzD^twNCjPdA;%+1s8`h-#Z9k;>h@_MR6h_S$pCU<5P&$PyEyTjTM$ z*#q$dP@*&3gZ0XpN7auCxotn`j=iXwZI@z~`f7iAU)UH?|J7P@IrXrJM>t|s+hy{A zGE&U+WN&Hl8E%moJV=;muNLIhdw8g&`DVj!iAu(hEB4LO#MOC0kLIS=?)acbWlBBl zLZCpk1W%Q*a%ruPPeTL_%7cl)zw+3ln6L=+qj78RAV?SH{W_uV?1UD=C&xp3y`v%$ z07Y;>8SIuteBQQ_^ZzVcX{t<$u%AmO+o2a5@YJiFg9O_PuF9luN}}uO^_US4WQE&O z*DZogJ&n8g@R3eiHx2U^u7bGmS&;j|j};WNt25FXWs$w{3H|l$b2iJmo{wx&rW@sE zm87+a8Q{w&usfagUnjgwD}$0Hy+UU{5{Mj-^AL%;&8dHxKMy8qe}8}s<9L0CHMVsg z&t6naBynu1t44S`y_}ABy@lpT>Z~Upayk~F8KGQhW=q#UXcK;Yw^bY9( zX!Xd5q{k(DteLY2NJxn3f@&W##797*CIUU?XogGfR(<2m`I(PoUTnKUB@NqpYs1yg zMF)YXwaO1xz3Gn%-M3;-xdu@hGMNXOw zOd3$ui=(SoIXk>7`Q5iW0Eu@IRERpfQQI;4fZk+;1`yoXUl9{-lK|AMqWhDT(Pmav zoPDwE+iMdAmXV;5(bD45$|nIU8SAeI1*LYX#`E*q;aqtw-wTGUCho%l&DZK*4Dxg# zpI>tEtu5+jZ7^%q?b)v+JBbE?xQ!Nk^cb+o=S+2!xXPN~RcW7x`tRCj!IK<9*|7FjF~cStY7RYmvKzuKI_z;o9qJS zw9=Qb(<&%diocN zM?eRJ64~@P8HZU{%q5-r#!vHLYj^U-`vx?R#_MzJeUyg1u7seHBqud|4xr)L8JI)N zaF2*P0x4r?)fmFToXcZAM*CSd_<0d9NxCjB2H$U>JbDmSu>E6n*DOxWhd_Ba*r@7 zB8)TlpBQ<^9Sg{`JX~_3!h-IGW&H#Rcli}4)OCDsqU^c6L`AUo=io6>#9cb8+$BAz zXnU8c>*;p;e$AwNfG!bT&|$(oyGhw=yVksacC8^R|H2)=`X}x<7GxrtqNX+>=WU@b z>`s?2q4d+Vp)k6%IE)_#7XX>pSsdH_1z1xv`yPlVzaQ6{-K&}LC!=jIaGOwCJHakN ze<8;+4e|oqhKa;N0~YwST@+egE-FS#l2z4Y06(uEl%l0hEEtbyLv8WddG$fi6~Pab zc1y_vl-Z(a407D9ac9pl*~L`Enr?shN?O=U+;QVsq0m*m`zV*0Lr%%Jk1_$IEdUJl z>!?sD)>PB47m(G*&~qmrCiR+^-vj*Lk9#jf*9Y1Wab9qg&6TpoBv&j~)gpC^iKn+; z*g^_Krw(W)N6jamCK=UP)wSQL;e9zvzRsl4Fa?pOz+`hLf8?}ZeI@PGmmX@bzz^nv zcN+b2JRVtK|0H(O2E*MSFNaA(>_Flx_DR|K-zEUt8GG_T;R0`R+hwUcJ{~Dw1X_M1 zC;Q=OXiN(Ds}r?q`nV+!YzMd5hUXg9m)R1pbtjE<`*+NFjn9Z*apNIQ=I|A?cy`}# z0(8_eC0cj4rOt@)Xzh->IVSwV`QCUY-e9SbSUw~#<1ykOOuzxLK9LqczK!rqol~S1#AGw8bqUDKk ze2~0Umi!PAB>#y2nC}D-hI`+AoxeMhww8&EEawLCL~r8g-Jp2+80jpsMK9BUgU)Z7 z*oVi@MwPAWJ=_+vc#pd|bGAt*PUZ$_9&-ruUe;^&^o^fFnJb%yrXjkr6X*A}XmJGN1uO zMq6UfMe;V3T?QzMoo|IvZ<0jXgRsF%#|r6o^XUeo2|o)gl%HlLb(8=&0IxczIKhJQ zJ5I=)rrKnbL7*p&N!!Wghl&B_lVhW5P?56o9omALS_#O(iw1o8MZUW`9kt`;7>_(B3w(1=B z8(77CeC5YcQN-e(d;f@ylf0Ns4DIR1kC*+g^Q;HghFj)Nv^sXSpn^v;+uXo?vsR2gU_^OzFhRKc=N~9O<3L`;*)s3LsaDjA)06(l zPj0VyFn6VYBnm1w`*W6FwmxbS_(S-#$)8~@n@QIr+pVNf{F=(!8j^XyoVR%RQ@gW4 z_RHSe?*ul64aoV?Tmjm5_5s(8_wA;w_nnyA_^+dM#Z) zXt1c>;~f;+4!nF;(s_CQ}3cvB?z_i3qB#9P+EBfj&KE+$+i96JSj)~VS+R{$v+F zLD(bPX8IJsmT#uPlHJ#BY=5SoWqt5$eY4m1Y6Kr&;I9*9qt0;=29yHAt{fEKmkB0YG6-SbZgEfG zWEPpei-h^FDKPvvtokZ>{Esp>wFhHB61_0J^l!|9zlfy&{^8B^{ugTlGqb;x zuN)CESAHOcLH+|4IO({>&qF{BvP07XcD(8rLc;tQNe#N$w*H&t2mX{!4hSxl&v6;U zCp2qA>y5OvQ)B2pJkE1DO=JHxxv-nILL9h25`ITPX(W^9X6t_#0-25^N3QzybmitJ zcAZkGkE)l<{G?Al-)E zB!Aq?^d}hDdwUmLsXfY3;Agw-Q zzeGWci zFlOFpx6F|ST)m0hYs;zsUPzA(kY}>~inIQC_tq|7*~O;_!fwj{%Ywe+*CQ`YSu1m! z*$T54&kHbiADg3jRego|$S(>}Q378|Koa*K$ax9Kttu>4`2|$km?xUyI|bJRQ|h0< z`{D4?ZYszCm(WWm!~Gw4-0zX}&)qjvIC9brimO|$0;veXyu^5aCdCdk;Ew;7hU9d^ zm|DJ8tlM$c@_|k-sJd22NWZ36q6Qw1klB&@2KZha@7fgq{YL+`!Tfbl^x3b4Os8|_ z@IcOr<}(I(kUJWk0)ZHXi=k+4F&f}3cHPm}P3QP)AN>bf3^9^dDTtS!wj7H+@M|cu z;T|T%U_Rxqq0EXKaP}D&a3VqGRyZ*}#3pjZAvg#u=M0M8|F8-lav`0Al%NXfp$@~# zSD|>OfI&bTCWlSo~BLv-hE7yH(U??ZS zgjHO~P@-8aiN~bQ2sIsvA>=ABo+c%F?EM<8Cy`5>+hU5USA(;+0xk8r2=t$k4KjSv z#Egs;O>w;OW%!R$71{?E8>cs32GW=3Ha)aO(R@Qr283>?y#ZJp%;!{TyAaly*?t9>I$hyE&oMz26QuM!UcIr$qrGgXHXxY{zZovNV9I^dmZP zI(>cOdT=zTvf>0V<&wVWxIs&oKyiq&Z9S7(c?ONGDFwwR{_blh3?Pj2geVa|Y%+^xG^P@^J-v$prbuwu7Xz%@9TZX5K5h4$0UXO4awWXj86o6uErYW4>#<1hqs#TJJ zV_9`Pciu1LMQ~bro;f@ zCMEcZ)C-~o&nv0rwf*vF&)37Hx}CVRU}`2x03eTN~W=|b};7+c@V7EEyNe273VTO{&O)6rt$aJ;vRp#g~(6t>PS zOXOXP(H|mCk5XnzhzL4!Tl3`!*_dDMP3m$anULrubP%vcf*e}7m z9OJc|LLN}RxfK|G>rIhnfg=<2pwM`oalo@TMM$dCyRJxl@cXu~PcS~K1h{cMdmnE_3@%jIj_nmHY>gi68`a#q4o# z3Wl=MkMA>~s;~QGZM0tjt=;cOt4gw;AJE}}YJ0+)WTi=-2$}6Rpon3BINHZ- z0RP6o0j~pwST3YViZaWcao7BW%;_3d!yJ>#cEphYD#m3iJ@*Fp1yT`{ODD3OSH0%- zF}?w!jGqeC30U%|PR>MZf#uEQeR97vnk$-jw(4Q_3Q`;dHvHiHp)SF?*16W!{kF<6 zv}%I~=XGR|g1AWI+_8rY)3hbf1Iotxb{$h6z#T&G35yOoR-1Bt~isAJXn({L<=zA?~haaYHikkd{Yc(X8dsb zgQFW=5t~3>Bn0ruxJ<*RtKch34V<#CNMj25Ef;D(V!2`(?sUHGMAxXSabJ97s5ak_ z3OK{#FkCD+H0K8{@lMe@9g2e&l1k;b>MO1G1%ap8G}z>;7RVkdo%iFlDKo zf0<}f+z#2p_Vyd7o@e{I9>zb`lvERb?0?bZGKp`88Bp}<+dgkCJdBmY`T(Kg(P|K5 z-qTHT?T8MF0<8wC>)LY{E$>nxH0qHbZXeu6^gf97ON64jS z@=$}v82Ii*tH(GSN19z9@o^WW$t&|BzS#H>aoBv@S2{GIT$Ie8Xg^G%T>vT0rO6V~ z>`oGXA_BV5)4ISeepZ%GmpQWV^X?P5uHLZVf)MvzrRlg)Rqdk5l^^2=AxL2+psrn~ zJ6WfS)r=21aQYX{$82Eed)grS^?INOu+#SR@I?yEPS#nysBUD) zGTU{Cq?mZunJy#BQ{C|82@8dKxaI8n?6cDw0pbjOdeqC=I&ar*Ys(CSlC3b-6wjP)SQw`yk|P+zZKPv|9MU3*$v z$gXn>yACg7J9hOy!!*AW-W&HI3jdQMq5T)<$p*g?j&OHAaEvsTE;`I_+L|09u4%i? zpDbL&(u;s(AwQ4t%%UgdpYoUxEBeHC%VI*b*>=(Qn{x?EtQhpyZ`v~UeIrZa@w{$_ zxkPlnse%((u4SLP%%N1`X}dWr1S_Dt!@^%3o651hdj5;lv3&U<@4EJ*IoH;v4@!i$ zlQqbRVM4y6w%JWzHDFFCaS@$B$6S3`UyZ5dh}vDlOf*)JJHDCi1Y)dq#FQ z-IBT$bhaKY+IQG;`_8hmDKm_3qR8r82gs2i%#26vX%ACofQ=WMEpAXYy7^JOalbwi zTFjZ--L2|LSwyn2S~NI(2zMW}@&5;4tuO`{=B_ZQ2g_uVxdSw|F0jLyRA#q{=ln~@ za-?bW+d`brGrKiAuYh2KPwbrQ*@BkCZoRjwS{Xb7=Jp{olaGK-zvpiAxSfm6j1*_@ z#mEzsF>xR(STcr|l{iY^%hRhgg!UdFPQ*scRa>Tdyl^Q!4IzWaZ(|F;EN!1T$Q{9> zG0C8n_u|$YRBrj4nAgnj(H)}ptLU8iMr3VDXdzFhCB=S!LHqhzZ0sepmWoGpm3or0 zR>~>|x25RJd6N<)s`T?xp&N<8DmgOyDyL8N=aH4o7Wl6sTs3OjDzm1dOXA@wueS@Y z$XDHXmRGKgXdu<=;I<6iU$LZ<4X4%a1UPf6! zyb7?XL?w2qoZ{;UwUF~FWz)M^-ps^0C4hRB&o=9GpEM-A6{TJny*uj>Rx3PtCM7*q z=I!tKbY5&Y)F205lLnCSt82-s?CsT?aW;KbmwQa%R{LINi&D625gu@cRVKGJyxd%e z*oJwM^Ko3%W@whN15~1Hq?jYc{D~E5rpX0Hq`I8S=z6aCgHiBxB(D`d?@e6yNh35T?d(oHo)50g# zgapBLQD|2eHBUGFr9Xq8s;O?yQ~e>2Yp)F82&-CTU!T=pcee@doDm+7A2`tTa6 zn6uYZ!gzN4czsG`TCsSI(-p(c|35w8X&`_-=AGuvKir>@s%Vi2%Z?3|;utG9-LwF! zSQ!4bCKiH*S491m_G0J*#Ugc9Y>yW)T;r!*B*NafbP4#LJrFfZkzOwps4iOM77aLY zR=B8X;bm@Vk^qlvZJO^9+QX?-lFzNl)JT1qVVtx%PKDj~_-B_UUsaXq1l6|(ltA|a z7VW$OSzLFWnW5VTvX$@$s`31{7Zo;DYKl5>GRarr&Zc3p{$fPn0JQeZ!Qe5GEznno{l4Id63BP65qIlQQRLfmiotEhH;zKeD7%7AhFokI5ha z(d$)|U36Kqh{jG9`$QA9aPKYVHU;;=#4Pq;d6~FYF4>d9-kUYC@;gxT!Rs=wG@T0| zzC{l7<~F?w=Twj`+9D}s&~BV)jN?=9Su`{nIjPw?rx6$Lx_#m*LH9P=lDO(^ms=U? zCyY+3BfiY#K1RFlDQ9?ccnF8>)hj9q$W|BY9K`4%ShRCNqNffxFLvj@V3;Mf__U7+ zRH($^-JTO2A-I`W%_OkuSDRM2gzk>1iDbkSsXGZ#Urfjan>3b+?aLIR?up`==fs>l zSE@^{y#{$mCvq)AEiPa(28LK6OMwS)`)ua z)P(bH(Dqam4sieyZ;M4kCg1&Brxh&ypbuM3eZGFi9UiKlS*-ak8SI8mEdjDYg$bCn zakcTVFuqadm_UWvnSaz>P(~3Acom;!01z31DaOc-r2IJtv7L35 zdTvgFel=mk&_=Tjti)dGv_WO3N*43vrUo_(E1~XW&@3HAaXMgrF~ahBc)+{L+F(hr zP$S)LtP!Gd_|yLEVuA0Oht&I!^d=X?X-i(-7l$}o4Nhw0C!J0QBl zX~f9VD!Kq~aTJ<2KzYl2f}vmfNo3;#&P~GuOWE(s7*59mEvp|Nwk(TZU=*wJPlY;C zuCJaWdr>v&uOO1&a2w8!{n%VaHM9>~aRC*Ga~Rj7t$_y4Gv9#b$kR(y?5iv@7rDUZ zs40eVUqO$LWIi2MdHcOS3SPNr>YVGWI=)r}8hJDw;4?EExwSRQoZ5Kppxpnjv*w>4 ze1iVrvQaJ6P4M{D=l(z;@{d0EPgOXWYg^knt@goJNYzNBIF5TY#U1F!k>uK}2lz#` z#!#d{gX$;qB*97>9Z`s0?`V^s?m^PZ3I+LSi%IrLs#gQ42)%*qo7k&g=QS#Ns_DJ`KOvypBmUT!b$=;^pu!c_0mt&)y< znA5r8$)~XqWQ1uNfS6Sqm$*O(dTsm4Yg}@qg`VJN(TiK5-My0LOtJ&}9mn%`n?I-u z%~x^1Q#L*kwE%(0DI`1!kiTnKyXy)+L7A*&r`ZRNgYAZUV#MxNv~9Y2UNg2+thyO| zGj0PlK(u<;cLuRQ2?a+yjyaB?7PIAnHK5`Es(ZXL`ehXruu=eL=JH1)x8t9XnTThi z7C*!3hRb%!JU@iAer^*no~5n01*zQ@t4KNtE^hf`Dxrkbz7t)Qr4Dhs>|)Q^XTj06 z1fEwPBn0QY%j0mYwIU3FQspUABavk7;A3MQWLVm+Z>{=tuj9)rm#zgaw-oC(tUTD$d+ncjLbMp834s%E6UE)ndv-veu5)J@~NqQ z3GpZrW7zjSB0O&h9AXjF2~EyBBZxb(;G6UO64I;Wyd)^sOU{?UTStQ9pv18R#J*5vzPtZ7(}IEdXNziOwk?nDaQ z5)p){m$P&KImP^A*a?ZYPyno4KQ}I%V`1+K&fIjEI@$l4O!G0=CUna`@re8K2jCiV zI+#Sq?Ty^}o*!NCL+tiF>iSxc3kM#;n@t>HQIC6ATsjdg#UiFrL@f2~&l3*ACUJcf z$5@eIJOcEO=0$;ajKxJ7H4_)mwwMk5%Aq-vBdg_llM5GyvP%?6ps={wd>x#0@yKzF3Ou>d!i!w(%VoH)Zh`GUF+c+XP(M zwlgz32v{@EfhyLTGtSlC+6KQPZ@g6AGZCmcBler9Iw~NmL%v77v<-i6-|h;3Ac@pF zRk_U}^I0WH5HdxlR#G9u$>%VBBm4!IcA<`{+6qen@%G^R#WM;xkt@uoBGh#P75k2j zVO!66m$H$??L&$G2A*!DF*39HSs+hV%|z6*gD0d;kGcdZHRYypQa;}RCD8bSXhIZM z=;p5*rTlz}wWk;+@P3xs@|+$at}k?~xz(olprE893MJ#1mOoP{Y7IS)?9ZRh{Pl_ zm80#W*k!WckJA})Sx{X(o$~=-MGrp)Jv%5m=sPBT90hiLB5CS`+WPp>5t1%8-=hoH zwHGI((ym&nY-%#jDCJ8HpYC%FILL4CbrW*YPYF z8At=#%b%*OTM23Q0yR43<>!n1yX7zKE?glg0-zpi&)cK66!i3G2g|A@+JwfDLCXt< z)IN^#d+k?z=A!TY0^-A1nTw-`AnP?(n78kdtolfc>-yBWr=c2R^#`xP1YyerwbD)15ooZYln5Hf?Tg}%_4T-`M=cB|u)-L`P0QtzG&cZYYg9>4B%^8uTau@Byv*CUX3 zkKRkwOrI{J&#C1S3$|U0I^V7!o44gi^>n}nip_omViqj;a31@*7d{v;ygwOZk`)sN zBkRrcZQgH}G*7##6RcMgq>#B!Z3&Ftu4}rLdSYhmKSrwN{d4MGyCiVrrSFmP9p!;o zd#O^%OHV9<(UQ;}Y5|e32NWS{07>O**7G`&UpA06^2i^gcGz@M_Uyog*e1fCLj27M z8n_3|ue804QDM=xcC|}#@4)P`LR&|;!?uTEs{V{rPEg!iq|^DHyMs=mE<83_r=UTx z@wFqeqAMmALj7jXN=4{uHU$z#>v`@PC$ic=<-P$KW#2R@# zw(kTfMKg57eWS^>%gF@exa53!0`l0}l-GWFeXbD71{rclDd}nts}s+m#!BypMa8>j z^q(pPe$ln*@gCjC_rTw-16{K{{HLPo6?>&*to`fap=CgOE}KHJ0ga`{0W zEu|?7nrpA3E2JkCUzy!jcex^{AX5Qso5?_Z|Dp!}^Uri|Vc;XFTTQwf(x?mH32V`N zU8H8JuoTxh2~sp)VNm)thT6?DqULb^Mj=D<*GY= zA6nd&4~|7D=Dxi*fwW*0}JdsybUhS4GCcQKS|?F_m?{UUCy)5o~$ zY}!o2@yCa>@p;5Dt7_rPmMw4bCyH?x%G^uCZu;u6WlqZwQU>ZzOCg?e z^&{>9B_a2@go*q0XAKy#s^X!QggjOk@W-_2juV&~`__20A&g7n+l<`>&24CT>4Cfl@?en}vlBl12Rs@)RO!t+ncMRNL5EKH(?6kJa_L2+;wJv^t# z5IDC7LIBT&tB4?3ylNgd)JLh&-tUWVtapIjW11J81YN<6X+E)IBL#_ijg`^{?*dL% z=AItBUk&@*6?8usyktmyL9%MdQz&UE;eZMB>_t~Xf!=MPLc>e~FsJU$Pe0Vy`?Mki_jbx2eKQL32Cyf z7VYMnkx`+ugY1>p0O7&_zZpir*>(`k>#upZHxQ$Ln{}H|()O)JSZD z*lxHed)zb|kF|K$mT-xQSBM)&gZp2WJ-^0MWn|Ya+d>O{YlnR&|HK>st576WI8Q_P z%M6`-isogUT)G;d^HVJX<>q$YNHZIP8ADIhaEgA7itujUvD`EC8OqKn(Z=4GqUuTG zgKA0w;&Oq{!miBrT2im?Yo67w;J+dxq}^#n{SU)!hHyns4xn@@bt3pBx^Oedb^oa&K&j!c zTp#;&6GJ74NY{{b;P* zM$6>8oO!$xtmh{NOUibuT?2z`iUT&R5rt@`EP;xp(6<{5vfz-t`z)a(owSowJOflT z$#r6nmx(_YJSG+|464d4(;2hotAo|^G!CY28IgLdmBcOcMnr0refMlo3fRJ^HOuew zkLiFwSBZ)xp?-awgeFZugc;qg=68wOs zf?B1-7R1$SuiG>T<%cpP?7IXY@`!G^jBk3Y?7d6I5bYEn7?H5!l(ma~rSV#bdJ1s?+_Coh#?s3X)$Q%~dTAPK!da&O z6UiKO)BhtEPK8do;qvCBvUzs=-p=n&sW08=(5ZfgNOPUe;MeEh)U(XoINpF5lH83- zaVYzBfD*^S6joJzo|J4{+USTM6rfj4S3fnnwX9sh=`%7qOw+0bD4iAFaa?L5Q|y!Z z<)yw#AgZ?8_q2XjIxj*+cEBtiTTrT0Wty%ZHjD~7Zg*hpnj;{vjt6o^rU+mzov}ma zubmO5)mS)`*>xC6y?Y6dRnMYjoh=0k&sELK7-__LNCZU$F}BULO3Z)-%?=mq-RUzA z-mknMj~~z;WJOsGTyto?@Cr+Bmg@#%xxNrW>~!0syog_Ah;xdhXd`1CzkF1%2tXA} z+Mf<`=r$nYOYBYZF&X?lEHC^{u|_3^egotti%(fXjjdO6X$a0h&AZg#n#JKmdPlv> zS|GF#ij&jAdovD@mrUXVAUYnDnZXOXS?_obilC~X_Dw_88^^hR->j3jcZRk*S3kYP zd$V{FR@9(fD|Z$So2E7^`fkjV!hXUsOsSb)uaf=walK#>x1OgN9Xq$fZdy#yyc0~H zD!Nt$ixj-()3>_>Hc~*BcSCuISB7-}-^HmC7a25Lk5P*kz$#I$$gzQi)cGP|BA1eZ zeU)5w`e=2F8cbBPSTMLebbp}6PGf1w6MNC|35rM=-&o~H%r4Ye&WGd%8K~UUSS4$b zz5v4-y}^vMeTo#FiB&4W&~7eejfp^s!&79|Iv-x zjy5PzdIpf$2PZL}RT1=96Zw>~tZq%iKcyPZF#UsZpF`AFu5z15O`4S~!c5Q2AofN^ z_*Xv~xWq<>Q*WXp@+US!I5PtM;xK8+$&rajJuFJB9K`I0uMAL&l7tVLn zIO?mUIir3lD2`#W25DwN+8>ov?i zurNY7%lJtwhrIBOd1jj7EKsXFRs5BBQvGvL(9wLgg_X~Q0ptq@+MYy!c|a)sH;X4S z9#XP32T8N|!ODw{s>(Dfp9fe-t*EY2-VX2NNJqW|YVYcS(ixYjAJ!Qc`FZ+XEbR1e ze?}I9CZH$hqT*V4Q$Jk`&_M(RlQE9tFK5?JKZJ|(Xa0y(v8BmUXRq%A2u6I*0V?2K ze~zBH3X@#UPcob-Sm{@*u+6cfBpl&sTzh?k( zClf7Po-EY}zy6O*2?2yvpkG05X%7uqm?*{A#&kv1yDZAB7YPdVrK3-9xD!Y{$KGbC zPdAl5fI)7h#$?6 zycx`Q!x4t=nA+jbXN|;zV(Cu$l;OLM`?0isRecLu5qJJd5E6UvXVQeXtH`XH_+)|? zqG-=sqy?YezwnI+I+%NzXACXS^4OOEMM2Wb$RfRbCStXnI*uw1zUhUYe^qB|%fAdD z$7I~9^T5i*QE9`|vJHEtxybZhw2uF-pa>B!xx+t<#h*e@e8|385AuOTBId{j@i}d( zd7cQhk4UWpUH`XGzXqR8rRRc`_CD1nbvGTPBd`{d-u8us zn?piC^Zah?qNzP?HcT*{4;sY)ms#WCW3E{@Q~O&M|)d)C_SQW%}$hV`{H&F<}Lat1{YXD4});ASw_`WwooG zfNF44c31L_RB48BbAEi}(|tH`>)~5O(`(gm_#E!mJ?AAJ7H%A+hT0G-PN5BWhmE+1 zpjTe*RJaPU{Ua`iQ;XHE;WM(iu(X%ZxNFb=tGNohHq#Ak?#_O(F8GzT2j_nU(!~I; zLUH#KO15XqPAPrR+!Pn?=3t6`;}9j{ydxqYt<75jC;XO1ifG}obPI(wyjV-JLr^m9jWBW7KP+& zr?~W#RepH}Kq&3{gD}*STkHKDP-qpm`n+u6N>XIPTB@-_np)<{yRgXLy8+#^r)P8Ar z>(p8gqDK{?wn}@yNia7=DM~#&B2tc^N;cj6QZ|4ZNV^HXbVBrcEBqM5OjYq3V>c5= z1#7-if9t$^k!{yVv5(vShcL1E-A-q4oFYg3g<`cImDNP?QI_}|< zceY70rC@wc4!cNTYeRd*Xsv3xp#Mfyyo=GjW@WR|A$iVhb?#$P^Z?oX=X?DHqo*iI!VVT;ue_e!Q^6XFL+OP zx%)-;%k>Y`@Q$||ZHmaB9()k@Z8cKNFRMO_vRoCyCpOayg-ujdP-&AlGZv$NC!#NK zoQD{f(5ksYkwhDou!ojq`R>|525TKu@MV_x5I`HR@D+aEsd>PvGI3FKhq! z^mvYr{?!8SkYK*w=aM+M#cuUgQ}c-Ygw;A=gsIB zdS$PPgo{RKFtL|XE?I@;JX?7*s#a1Ps+hg@Aw=C}Tas{?ZP;ma<>6R2lF)FPr>_e7 zF7H%&;0{rJB-Bk)-%An$gOy~GzLzy5==G^%ngg_<59!-vktjF}W^b5(a%E+TfyF93 zG%`}Nn&Q_hIdr(tOQKveEm09ji8A3}dS+yf{m6`?sKt8dKHI9k5N1JhZ9fn4#tzx3 zCzPszsmJdVCPm1{n%0b%;c#;F1gK_%W=WJwe&8vh7T{FiTU48 zO(r)d`o=T!CASjHR$o=Ca}2o|CCn|GUe%4R=zI{wD6ZU9RXvfKaJjqhO2fOJK#bbJ3Mk#(dBIOQUPN#xA# zt-rylm$_CMNR?l_=W3y$Cdg9x;jT1+3l7A~+U3Hyslr3Jwa(%*zq;Wfg^=}x;biR| za^Je`8S5NEfe@KT&5m()nH{HoA7A)D1>K3&? z>$#3rARAx9r3|6aq(I;~tFQcj9`@Y5!Iqv*x)L)s-kxd-b{<)=iat1~XnqXc%~Tf0 zA2Y`)@pj;c9xhLMBo})$3Fa%(ahu3_a85K5u-ZE9g~BZ_`0TfJIaksk0EMddB>#D; zeSg_lon23_*23j6|J|oUh`fAcL!t4=fo&S+Fp%LzHOFe4j|81-HSkzEjdMp)NQT>s zsz}PP2TO~7U+Qf+vYVp=afO&^vli1@Ww zi+grCcZr(YxQTgcmmi{CE>)UwC>HwqG=36I2m`#<#9x!~xu{$)zgnY3U#D5{)}=zlfQ9Ll+s)^=+luuUs#pgFnc5LTr1%zS#wjLj+7vzlpH z4bQx=Wvg=FRyXg}g3vwpOtlul_3Mo|dZ(rjBJjBeOXqR*DsIYp%wEgOYn;aYK!#Fm+TG_u9j1*%57Y0Sj^1BO`3r{;(uhw& z&<7hls|t*7h5WAXs}q+Q@uSS~W?_T3Mt_L)F9G+wA{Q9`+UWY~i4ZTp_g6!z_skAU zZgu@Je;)pYS{ntL<--EHWF4+*~8#g8S{tdaSWmK&>j zEr2Cg%bbaiW|$F8x;HI)Bl3{jVQP8`)R7&U^GU~piq$+uV|9zYTnqn^nQjJYskWrn zopz{&hVm9GXpEu$+qak*vqfPKV~gHp6Zc%a+506^Wb+K~N3$+`81ghH*8;FJyDa6l z=vfBshJ2UB<1p3lwzmu0cwQ3{{M`nBe*4ThQV2DhNM|@T6d&3 zbQTwKV)NvWr}&pvZaoCnv9HcshHp1SpOCeZo&~2aSxettE)xIU`nWUE`_v_lgyhl( zf@bYX76HIqChESZ)LFkj1Y1Q;FWB!T%5TBa|F5~`|C;OZ)2<=0OjTh9RO`orX0`s- zTTZQRu6rgVEMw1K62?zz03)QExdFtAd}6AGV+iArBIzGHa@ z1*Y)v$A<1-UjOb5u%$N=pD_qj4e-(|espXf()Lb#gurEVRQCWgZp4~os6_4yNtF;8 z+rO)Ve=~SUSVzzYoU%WylCT50^m)sfVIk|OLk%JVdE%iAKMGiRberq7SgO&0nJ5eB z@Tkaejs`F&^AT*8+BP| zpDdy&*`|lX=$-R;zmeDXd@5ZqC5%HiUv+%p7Da(ZFQq^6=a=lCi|Lksxg@|30&c*7 z7i@hXY@%>cB1YPe7)30>s0trmH>a#UJUN499{=X*(J#or?y);T%sFK#DmxmLQO?zpr>>-g4t9dEi@A#k4Vdy-ce36)xK%7$^ zO~NYClZw7MoXlW8T^LKR#r<(_itg6=Stb5|h{d1e#Y7i@MzuJ`7ajLwaHG*!rdZ7* zhcmEU9UBd6Zgr&DjFgirib4mk8}}BE!BE1MM2Yqy#Fdcc^>4=%N3h!*{Cc+cQ@Tu3 zOp$gvpH#~QL$e?!gOUh?xNZxTKCEJFw!iRcR=i`;E{oxYD8NPV%e<|00sU_f(rYC?^`V%J2Vdey9XQ59%V!apf zzj9YBlE8?Igf+9bb@&NQe3?xSnn0SpDVqiAvJB@Y;uX~KBea_ufc7(WcP1P~!v8Vk zpKgdBCjc}cVFFT~Kg!B}eTaB1p`xOKh@ICofPtLFI1HspCM5%BlTCU;sn!h#RC?D$ zB!@a;-rZio(~%le7F6jGfAa!%!v$a}4c< zg^XWMHm{%=S20fwp(tdQ-ZTj;N9HeU&qTDy+u9}0^((^U~35YO)DS*Q_f?TQDRFR3BCzD!Ukfl;D>wZriHh0mg) zDe!DAyJ3!VWvr}jhvVBZ=|nUGpkbH-AWxj3wpj(vf;K>FCbz%6nV4rN0bBnKa&T5@ znj90;Shizo%2=%pIls09k&H2j;H9LR?Ji>ycu{UhV=z00^IU4Ii!Uy8kp1@NjEzY{yNk7>2Fpvq~vtz`t}B46vaQhMpRDr<6}RF}2HK&l`cm$h*m$!@fqg@u}; zsF(a(#@svX;+?qk6!aH3mcQzE`|_2|cQ2!DoqzjeW~h~B(NwUi>Cyed2{TZWx%69< zNy(F8&Umx;B&;;+MziTndE!Nm#1TIk#9L6)wQjlYadBVF?5wRt{d6>-`zzc05BU(N zLx_)mm7xXSWHB@7;VQZxV>YR?s48W88Rkbe_EUDpLOMdd8Qc7Ks^gbzx8fl@RP;?S zx;qxmImqIEW%XzMKZY0jf63lz&ODf!6VnfIc)F(UBy-5P{^MrUWIb=?^fpj8o=|4WaUl9raI48WdF&g z{O8wYly84vHZ&B4g3fN4znx(e0wq62R$$!ytl;&QG)*F^XhF)C$@t3kFHmi1J2<(x zbYFvH=6~Mizb8TR3SsrwWaC*!)WoPnMH~rhXWNV4?q@*0FO*1hC1l6@C1jJGSf;Xt zi-JKbgU_?N$MpXarvI_QE|gjWg?NX;%X;k}Sg?ts-=3a7E;N^7D$scUH(L?dly^9# zbtZ3LPomo_e>KbX-t)rSPJT*X#t$wNE%*hgCfD2F{N3CqtRE&uCrDP3n$JjGfU&du zT;KcuF}c5*LGz1IrHRhWRdTq5SMYkFeKk`7qUokq!4=R{n~OT+@AOaW2!bJ_X*5Gy z1HYhI^sCute3KAr`8FDB8hF~833&G4h`Mb++-eRGN>sM;4a(xiql@Q5C?D$)C z<3@u;6c;nZiJojTF^R0oQIK3_B^rV6+lBDo67!#;oQH|9%4m9udZV4%f9 z5m!%We#5GJ_wk?h{h8e#_xP)KDkqDa^m1||hmwP}%yf39Nz)jy;tR>7vfkfoR$8n< z!GW>Syl<5mH0arR4*lPtyK%qAUBA}qs7wGbr@=>mnA0k}<+u+f4Actx2xVA*_bzw+ z1>V2L5Xt(X6J5b7Xm%-cW@Tw(Q}*J;i~9dH!7h$PKsDQM9$YT*mkA!rlMmbY9~*3B zWHe>OjDJ*zkb}lLbh92P1)N&R+NqXomm={WDtj0Q2eA!|0bzp4eAY8KSsl~w-UMqX1;wZ$UO(Z*Ld+`aMo=*oOt+ zl!$bI%}KbpEa2HsxsmUky#mbkVb#BU$AEPaoUC~VdB1g6)#|WVFdg#`=$E(b&|7RB z+y8qU@a~gkyytIKHhn)}b3L*1Prf_kU}?b(t@_FMi{z@ftu9_&aL4 zRNg07ivDsB&Hl%ACEs)jJrqplb^TnAAoB+oKr!Ipds|m0ovnI!P(1>3vGdeK`R#r6 z9}d}W4yI6DUJ_)PT4C=jHsto%y15Cyo3W=t2|Z}8DJ(m9^ke98p15?It40weokq0~ zE`z#ROVenH2XWuvVBXa6%&XsSAd)(St}Y5D{VUq}1`~h+C0nb3t&dj_kvHLMIM+Jp zNjf+k+6&vqKk*Kl5PwBHV!hO;mLr|~E<5|7M&MkbT5))ZUk>@d;NX9MI#boXy>iot zPe@6jM0|A4Dq^d42LEJ!(!aKu`JE;s)`$HY3F|_~v>o~odXQ?dCY>{#hB*8{%MC)u zLiD@2=N%%PY|?!L1Wp2Q3}j1A%beZX?ww!pKutf7RS0No4$hU`rIAgNP52X2P*5;A z|Fr+^UiJ9NHVr4|?AdC0?Qg+K+7cSzIqs`8Wgg;I7&jPAyK5|GeCIg#QFy2gKuMe| zI8bGI$u>ykb7dL@An+5d-Bh2ei*EY-cW?BIKvpZj^KEcj@1__z_gyKcI!JTKO&Mto zInSZaIGcM%rgql+vm%|I!BJD|LfwfWEa9wrUt^%<)o->upg|RMTHnQ(M@~z*oNmWl zVaOcgaz*AbPi=AmE&c8&oN7>j9O2_^w<@FEZ?6=TD%Gwqx5-2Ce_Z6lSa5&0#3p$N z$jH7-kU84E;o&Gx?<-a%zc*TrYFN9Q=?B8mVX_hG6x9>+JMfQRhpy#C{46-d$HvDM zN<1r3emATDi9}i@3yH@KUsofQvcW*Qcji<@8X5v6Gx|$eD#r0A(hx9;XhB-g!zubl!62d=SYb<2A7~Fyr^#RDkzz{DJJ&c`NZ(I*e zIDa>U1u%rcvIpQjd(GaRaTMl0^l0hs%TO^Al!%{S7Vv9d9(+F+Mv5r`>9L#7=SY(! zR$%<~c+JlM|La&$xt{5J#>rH!+QW}k<85FEk=c0#@9~sW2eFe5rRwKdE}AzyeTbV| z4Px`y&q&0jg##`BQgCkG3JfbYIMUG?U?l$kaw$iF2Uu`eS@QtUx6;;Sqr#;TKk3PG zQmlO2(`c_;^Z(tmtNF{vh3N86AD0=d7coX1?mhKJr!`z~Hv?yHV^3$Z_w;hWux+fW zTu(95;_X}D=xE<(nM5~~$QISCr7;i#;p(^QaFLKv5&gN!LH1YcDbW89hdQn)2GqF) zs*?R^u-944jLaCtH}7EuHx4u~?(!1>(R-Z*N_;hphy2LU?9JHIZFY}unBCG}g@f-j z*vx6~@tcG9S+X=(zs=zMm8`dh0|5b6*J+`LrXeTW62<}IT2OJYt{dOx!;?#ltV3KiHz0s(0vGq^>nK8MG8vS#uJtdwFappN(be51`c;BK{S^_-DW@ z$@SI$0#v9MP158rh@D(KwEgE60?~VqC3f}X+ApbI6MICrQtm0$a>YERj#koCeFYM?2$CL6`^8{oiMn3eqGHA| zSQCU6`~13#Inkw(d-7dPl4Iw&?L>-3udE_&FGjdW%IKsOcWLl~!_pn9<-8ob%biC_ zKS4MCJDaNrfs@D{T349ckRCVi|g!FuC@U`Ci!9A$45{0 zV<$$sX&9O)GKpN6cH;n6_!3Rm4nz3?wf{>g$gu*OF~6>BX#gpGctHL`d|ortdH``uMf`XU9k?gbY zh2mN}oXp){o67eNJ}S$9?A3U*u*U@Mw}1EUUG&02PWg~She0RZeN3Ciledn`SGBgs zYuQZpD^}-Yd^e{!vsV5^UtQueiQOecHTi)>>PdFW;$h)(ah~v_{lEMH;^JSDs7P~s z*n;mX4==79Az#}QJjN`xB_i;xHs(2r-Y+k4;isg1De;;Rla4T33RaI! zPe+F0KX5p#&CYf?uIe#txrG--6s13gs(W6{kdFjC3IbXlIW_^U=4tKjQ#OWsfHn;S zjBEn8?iTM`9jbvOl1PB(C5BmpW<8olrB7y_ki=Z2q-tw#A=_K(=A@x^t9h%~|3~pW zapfK}GjkTi-{B{`y~}}*p`@b6ZvyE;a7R;!E$)w%_Dx*Z zo)CdI(j-b#Xcc;rfJZ>IdIR{j5nK*~;kIKoTm3nq$Ww1h4AD&&>$87l&p$_12h~c&VhD{$(MSik@z! z8Je0XNvMB!7J8ST%w(AVA0ajp9Pf#-u`h01?tVplL(57VI~q+l1MwVoWtV=E`#Med zSxdGOnC}uGHN4{L>V8?QFgmqlnQxV%$pieQn9~Z8>UD`Zhki{f3p80ke@ojt5#zoc z;}nwnx*dAw&A!-QF!j=0@BNEV#|gsfCDqM4R-JSH&kS`NKhd+q!i$+oN5ULVp4HrG z(7*2~ELtsKXl8!6J!^Xr`8beI^04JP5LJOYA!x!{pKUeIBZ*qS z5GB+wqRwSqzP8V8hCDty8e}GW#%PL#IPat-usZg$c$kk3F&-O*ad^GE^{|gS;TR6& zXU0jutIzL{oWS#Pyc6h2&lT-Q)&_hC@1PtTW8H-B7!^ssf3hA`Rp!ZY!oIL@oaGrN zIn&Y7DWSlQP$Lo#crT`NSWRw$UCiXU{(S`|ui#YVUUi`rdA$X@v4rjyv9XjZ_%x)Q z+Y|A|q8&B2g3u;S*wc4*bi4)!iGe5_=jJ=Tf?%w?Nro0o&|8#(k5U9(!&2N<%A%s9EyAH2^lAc%hy7}h z?#C_1GZ^89aQACaySK>ID-@kwv4!*GOX*!y25p^$`u;uG<-jh5+P7Iz6DXy4& z^I(qK-qk*Po~0^3)*;h|lDqS8_9 zy~!c#wSI?IQNB6gX56c{9oj);$^!~{tmMR|(Y!MJYdW$nNbZ;nDu@JW?q++CS zT*b4a-?7|I5q4muZaZ#~_jo~?#7*jmA4!Eg%y;@FiBW4cgld<$&zuVrS$z>ydkif* z$b$mPg#!B{gB_RkfdbqyslfOM#t?94Xaum?%W6C}$gf@-SE{u`H#L-+^B87eKT*RiL_K?IG}%KS}p)EW}k2Q-19p8Cq~AUFBt0zW*g z#zYY0T1GGE& z{aVIfVQ(P*+FdCa1yeoDRT<(l&Pd%QagVt=$IfW*c`OUDg(MZNa_pH(<}t?IUS76! zN!*igZPle$^75*yc;ZlWr|Jkf#QXaDt!)(=SmU8;kuI0a{|V@w1Wz9}fY4BcqJ5;B zXSw7_C>S zIoGaq>{Y;Rajjw1#@`{q2rV}=|4kyyrG=bJX z0=2}j)#kp4^hW7VzfH7WfEmhp9euR^R1A5AN8qm2&W(}Ryg83kHXDqjJxLvbvV}j~ z?mys(qfhs5QQv6Ta2jIE6jkHws|*+t1D-JHND0Rm*-ayU3F6zb30c2G)4n9tS~La4_7588m<-kGB~ZCYC7Mm~TsHg^A55LYPy3UeIpQ zc=8M*V6u*{U%is~F78v*M*U%=J8=5AX^wY=oH}lU2kiQGxfMsBZoX{@k7EM*aX_&C zHi)I-`-%-#-zByN4#QQl+C=#LHR$W~sWUn>v>bePZ zX)p~2V4D@2Jg{F_?YZf?rm|EO?E2%IU-?2QH}K3dt&ql>95+ll;UpGFbAC}X3aJL5 zRJ5VMz}irDbn&U<`7Br z_r&O!;zi%tPkOe1k`mwU-77olcO&N%{{9bMr(!mX`$)1>hqpxl+6pxBQ`zbD@+P9| z=z}7Bxy-161o31DCYinW-u>Z-wL9)qn71e5Qdl7vU+p3_sOW~`=vlk*X6JfB z5rckQTwIrkweibBhAMANCx2tSeCVx@sbmzay|YXq%{*R&HnQ)+EkoN@^e(esWEgvC z;-k&((ihzf&=twZj6`r&9WYB1FV>P)d1F;zPp~3xp`nL2pm8xV6IY3l;f8g4X9;d^ zd_$po{(Z$m+(*4`!2gGBU92)zDks#H8}?}QTfYP$*mxV}s(OAC?<72ku|P2BnKv^) zCCSbAMRo@c9rUN*?LtD2u)tH;LAcqL-E*Ax;GvhP z-D!$5cO*4V+#OONW7$Bp-7YPfVWG!W{R)cIL}rQx%%aCX<-zJVV{FC;`@U|(O5ncu z*U}E=(?O4bhV?E3$o7d+9nNW3{r+re*RTfArB_|#=y+|xMmHZ6?-eNKj4(U$CfSg^ zZ4O}R3hrl%8l@@`3o=(wSOmlH{~#BC{{)l(0Dg}<>I9}WZ)qMH2<~X@t7io2F<%eF zTmMe-fsH_H_Qu2-Q0lP{ttmSW-xt$PtKYG)t)1b1V0vWR<2%d%iC#N`w;Ssve#Ed4 zGZZp8d5I=r@5y+F!kJ2y;cIYHM)0m#kM}d`DUv>7tpbLPF<&$Z#h5qmnk__}4TFau z5AckG%9Z*u?(aw9&HV2cP-4>klkQV&_@_vqC(WneHTpw`@G+e6!&=ZNgeX(s}-)MnAa?`?>flg zwxp>)x#T3ee?9z!!haT=_uC_KWKES&UJ+Y5M{fP7zfmng{7zVi4o6m==0ua)<}L30 zR~kKX?*v*x_xxS_zVWK9FWF_dbfHdb{5+>->e%zh6~$ejp1%p)C+qux!7NgDwHeD{ zICEa7RC<+8&GNeeSsA;-rX?`Peko?Yefl0Zr-sXgz%zLX3~Z&qfv=i%8>=@vNjEjU zOEsR>AeKf~T`#Oh=_A<3Ah^DRRb1(1YiovLWeM`O|Kz0E@j)AyT*yE5 zu@r1l)Kcgy$p+9^lT&w&Jb+Hh3$uh{NBV57hypY>^-{xo$MNWXNtWX$cRe;59LJa8 z*nNJ(5pLpN3Zn0HfV*F5hjB9pYNcC% zk;5;G(-z^qAOR)5CLHM+KmS2?mH;}ubuP|Exu5_^1JBtyc&z!MsB(!X{AK-}d-v4p zs`{h3z2Whd&3omZ7@JEsc*5Yd%MlVPx2SyCkM_@v1HnJHk8vK}iEW*>pI@h*fT)9) zCe;1XZ~3I>L0UMNywyv{q~fUfapR+uqXvenv;c+LZY)*RCV{!4P;29H0Sy$qF*phq zTa!prw+|<-8|}bDnEm(tL}v5eqt#Bl^52uszaRA&eOFheeWzyCNo=A-KlQA~`w`7> zG+R3pUlBNo^=F8v*W*)UJYLQVR!jUkEU8rO9(eKZwK!i4Ae`a&wT>3v8ypsI-; z+?fj(>2_-oWqRecIlLMi!)V|d!9O~ z;RH3*Mo0AT`AmBdVylhJCHi-n{UZU(^CnQNdJ>;J{p3tnw^P)y#9{e1eptqDcaPut z&BTP!`C_!gMkN6ZJ`^?!t=a&M-bCw#UlVzP8(ql10re^z5|mXJZlq~&j((j}C5CSf zxa)0T(eBR*u-V@5Zg%%K#?pjIwD-XsVyz23N97Cs8qoI;VvqhUo6raH(;#8qnD=pv zBmYK7`CnJT&%(6mfXvw%M@+efLhas~Z}yuu(FZ1kkQlln$Dsz7fbl3Qtt2V6Gp(0& zICF4{S={Nt-gsHPC(&X}-T)UZZ9i=NpYGtFCQXFzr|Tel9(D-bB7bkUs! zM`kwP-MkQoxy(w#;$jt;{naRW7d8dg$l&OuE?|w(Iv0CEFK6^gjI?b!hhiJ7nFf}RcQa`7T3P#db z)Mp3P^zb*cqrr5!ft-Bd6IKy5^zAWt7Gz=V#59PtmjmqJ`NP$+Ld@5zRp#& z&T^7McVQ6cWghzCW|_Rtw{Yf4^i3>l36vL9ib@x_u33iL-%~7hN=_|!?h9L^Gy3hb z=rlYh*?2t(4xm0FD@!kQ^Zoq57`6#H!e<8guBE`V@^^_m!z((@9xw{P8xFQGKi5~$ zel&}MTbQ0lBtM&s$-_=m+`QtuhPql=pV6jQ!w{Y5Sy(w}b`hh=bodonccnIMw zp7J9f;8g}m@H2q2WZIK})?=~SR9rc{%2vK~)h)`7}*?YgB=^yCVMz2e|H zN#I&ZNDh9wqR*2%G&>9rbJvG8FXt|cQ4;kWT>`greAy5iYNL{QG*Ed^zbCa1${!y~ zaYRmV!w+e)AMF)GV#Op*pu!RJ+^K?5vtFSoUmWN?Bt9x?$c0lf_3Q{Z0_f=>x;$=S zur%=iEhS`FDM1O9%W#u%=yUO1O;u6n5-xVD?c5~w!zH+o(66FDYKfDAt7ON}2C zLH46qd%%m_uMvaMP-935(_VxI@Feqan-ckoW6PMH75h!;6?Z-CLk;o$6gNOM=9jju z+w%%&qQEL*gSA+%19I+Out_1ml#i736{%fTgilX6v-0t2-}OpTAmO)_@(z}&2{&m> zzFKKBVUk|HvndddLyxMP`7ZQsZPW0|yUQ_FnNOa2ax#+nYiI{a{KY_z`P=x!4 zqlR#^>%}GNZC}T?fnKX!ha|~|jjFr7OnjIswmF~M|3dxE;|5L%t1{|_z}RNyiRJ+t zeo`ccy8<--26q8*>5~(p(72aXFZN2gCl2d{IVyKg?kwNiz*&^Q@XbBoK&4l?a|PbN zzX{M;uhv6g9fzykM;@mt*D->pYxY92eW*xNdkjb?GZeQkY=5YZA~64NGbF!_w*N3rra((5zE1Rh6RB`gGl^haHT z+x%dZToQDYu+-S|o_Q&Iv1@KA6Qk7AzeSFBIPcoBRPg{*Rx>x9PZ zUAK)9lNd675r{|WLfT-|=2e7oA?V}BTtIu@IoRk+-c#(ZPJDuxs{>Qnj$kFjglnCLt{->SiLMzsE-%}JxPRo^Kh@UJXhpNf_aj6}Ho<)r7W)mOFobCey{yU5NE8MClX^W!Q z9Mh=Smr2wmE#gyQEVwGgez?^)13xwTxW>j~st^JJ zs1GqWhpkc1iBa9h{Ro5UWo`=mLlrt+N^h*?bZBqz%7y&>zMhlLtChD%@J@?N%;7$O zMq2G^N{8pBdn;Z!Y(MSfdQrB(WTEbSH5cf$6;W1ztkc7Sqf6La~pEQ z@3gecp#)fDH(>&#a->c>Mc<@+d;>~n&nwVMCBA!% zJ#UnBn*%`Ty~pq0^dz5MH0X~oAo$0!tKIUZzA5B8)k)vc&g^ps?d=x3!Qpd@SoG1+ z_+w&m^#s~y#yn`+5_$Da#@@?w{}z<2Pve}$Wde1NV|D$&it4CT*^%45{06ZW+L4B| zRX3&sDY(u%Ush8tc>3^szM+I?6MCZ~MJ|K1Eka)?ZFgLWgSw$N`ZA%R{9E9dA|`sa z1rGqGDLiH7Irm@^OnyPsH#g5n0S}$u8l|^j6^7>nC5_@rn`#-!Y`hi?SZE$obs|6pw9p}zxAO30-S2^MoOigB#j6;4c%Y!h2OHXhm>P7 zDl2t>(gfB7`fg8}Oa&(Z^6nC#nD7{1SG_)^2nZhVK5FW$*8wD9yW3^dN_g^>@os;` zLLCDy+&m3LZ!cJI1bM_MrgD@ZDzu1@EgU+sJeTi<;a9I!l=L~~C)^f{rdN&sF1A(^ zib_E_@4?n{K=k6oMv9Y`LT=;B-vP(sd-Q3L*uC3!koJ=-`3c6vzrC2$3D_sj_O)Nu zg4Jxx1HEtbpi0ASTNnl2T3zJ+O;I2}>HMA0H#ju(v{|}^9Ho1OH2ga#{)~o3ZXWwNgG`cj;G#45hxV82^!Di2wG( z`!rA$#6IKsdgkOvSLGApiXsb{j`YXooiE#a_MXePjf#Y)TDpt%Lt{kDafK^iXaF=V z2@ESlTV7$T{#Yq!a&RPWo>`=i?7AQfnRd@g@$BoQO_Re_^jBaYRK?0_#LoX-#YA#HVFE#pA zz~~Qeu{p=+qJ^My`M1uPxolQd8yQGA7mjgTefx%7B_=k@e;(ybrue0#s61gc;) zkX`{W$@!%gjjGsj!_IuP7G!y9(hoz&qi=~Sl%6B-O5LKHUcG!6(EJlX{L8S1Ht|kIcHn zXQuo_ag;`6rbylXTj7rAZO`qD6R2Xnk}y}Lhl%4pdY4UDgw%5d|DW8-AfRGwPnpd%~)^crViQzB(q7G|hT~1R%I>(sTm*oZC>z zXY>5AP%H*JdHH#+FYxPLEPXKOT(0fIn-&ASV#Yuf-xF~DMt5zfYRmZoHV>UvdM5Xc zs4y*fTHuza-S@H6Ed*_>eAS)XZw<;-9Uryx1X5y7VD%rT`OTj9B*sEy(+#MU+@DP8 zgh>Lgb?_8frtm9o^nT; zwrC9mVbm!ll||Tv;a_Uue;(>pr(ftHGv_ZuUq;;8xE!)Lt*ai9bjJWa*XS(3&95=! zO7UIHZ72>fq*%vm2g-%Sb!@N?jGQXdz?!lF^U%vS?kn52{8q?Xh>@cp-~i`rl!a4SY!U{%%z?`1)%EKL z*m9d8mOEvs0`&a(3r>M5d!zMWL_H~5zS?s*=S<=jPQbwGAqgmUsZ zq=VX#l7S~|JD6S+pR2L%{Toe9wnM;w7cu`VJv?UzPH#$h0FtT4^J5%Mc$=up-y}CK z31*(T(}Wqdtj^f$KlLKK;LB4Gt!Lx$=wpzQ^R$dI$mdQny`+7>2)C--%IKwaqtws> zqsaaJ!i|oN=$wvmvruxu45gc5NPoAXz1zjr-cCy}e6+-k>AG5r4ND}#A z(Hb5Pp$)g!L5KMvf`)>;x@?NXnqAK*ik^N*cs0k}AGs`orJk`j)CD(0xY0T@+1XYL zN``ZID-&{@BDlYswV`#47O#huzjr{#Z&>|oeE_Vw3OVLU}{cnm^ z0F>S!E39(T{WOYLG1gpeYh;a(8oiO5^yUpxvGW3fp+=T9s|;!-@EE-hj@!;@=s{=( z21BjI5RZQ;tFn1|qM1ibp2$C_m(b}fRb5~5tL{{UdZo^|MB^k6GK!x z0ni`<|4R+h68IlItCl}MQ|U;fLS?BJpbZ}-SUCUU*qwSeLJQ3Dq&E$q4M|b>k2YkE z@c92lq3+^j?D4`-2*>5trkQ)W~%L(;~dneDnKK_``ht z0!AiykVai+<8Hy%`Y7VN$1Q|q1#>j|9c~JiYmaQL@F;?RCa%rtlJ@J#G*Bw}a?cGI{|cukT^|1S=`l_Qm45J_IGN9ZoqF zwz;eduCE?oXm4DYuW?;ek$xxncCaOyPVsP~6j`r{ z+XU&^dj9zx%Jj6>904Ow9&!EhfBe~S@*Q?tpU(d+P+jWDxG}1p$fwK5zia$8L*fNc zb;i8LY3(hKzlbgJOYs7CM>Q@wgQW}@#VorjsKIY=(T^k7e(+DWtF#G+ip0 zQ z^pAJH*C=w6;*~&HcViZW|{;%*ze};-1 zmoEaS1)r($AA8)@22!5SPkiea>hB4j>1wR_Yk*tHbjd3N&q!z8_BTCUnl(N7oDSso z`Ww2Z7q|(0^VtdrWv@EK{lyPRXChv+I~&TnXoq+nPK3D{c7I05w1?9}*~uQ8M!K%{ z7q#sB;_m%=0gM0Gl+VQe)DFdcC>G#9J-h)s3Wq?)T8ovBL>Q*d#sP3;Ic=-U%16pp za`j9u^O6H%hWp{=j-M5{?wm8eXWqB0k5|Pzei8z|%4#!w=Y?PJ4|Km&#E_l%v&szS zU{GG9KUTu;dz6%6K~^=W7tkrY#i^*av$+RQJm5=3 z^YP=~zEmgLqCL-olMQ_Xirm2)daksN;@fi};SPhOEYX%!Y99LHi|(HecY0zud2Hr- zTE++Y_0kL`NMJd+$epEhpj0s0z?GPmAXM(n%qY_K#tT7yPYk zX87?>Bc9BY3A_&NGb)_~x2zuSiW6Q~FOS^mHqoi9E%L^y?f*C*xXNSCiP)GJNpx?Q zN*EX=_YDg~S0VRh^=t>lThMJ{Z~B*LxAuhh@R=Pff|Kp88afm-=knd(_C5G-qU>J} zB|EvV84Kcq!M6ahllt6$#7>h&|Krm$7Ehsic1ZTq%?e61=|inFE4L9?XSpr@hV#orZ$B|ppJ|w zZE0#TUc>PVk0DAw*8a?BR zlK^VkZJZCGX0Dl!#>KWHl$?ft_^D^5dD&Lg@2KBTK!f!yYS*WBXEyp99ivvAs6X~c z{;?k`D|p&~Db45;`Sp~_{4qjvU5rSI&&bX8DWU)67PPkyNJJherix}g=c)B8OYxWI z6q)_HxD)NV-%4q`CJp!T)=LE;Qyr2BTrdsv3OX}#+T9|sbluFyB2=6-bqiu} zEIGOLV|~!%ycmOzAiJ5oWMy+=?ElL?JXzKN-O*@|KfR2YhX3JZbZhEt&b!rmQx9~>YKic*qzok>KqKbfH*&PzT)U%6y-6a>8Ka*%#mxE?Z~KM zeSsjlhh?w+IkqndG^u|1x4sT|J2yYDs?x)?7tS1Z#VLn*tyW|CqAhv_gW<@ExS=mD@&3sy5B6 zMH_XM9ox+Gv@srA*4;xkxv8u%rqwl8xGl#l@OaG~R&9MJ6rw#-m&Vvsb_6%{(7-Eq zi!|HI=hQ5}H;eGO6!XcEj{oE~Y3|qeb_H$|>)ym)-zIv0Tz|PnbRX(G^Taf>wXZ`# zLEJ>J9S|%`F63hmYowjX8(XE+O|DsWisb5?4b0^W>tUoCS2{&Mzx$^pmJ`H9+&$6| zxk!IvrafIP`nZnsrRrLvUWw0+dtS33NDc^N^jSBn%0IJIff7V6{UTR>`;_sbKxIm9 zTacRHEk&Bg#})5)`jbIIH2)ivFr0%FfYk?1_So?L*N4AcQA6636)m1kivHN16=_Yaq9w!G`x0;k354YWXB4C-ZFlrZ zOIuUaEI34lIo?&>_Cfn?&>J|ik4fsdy7L2c22GZ9Q&0f%qxQ+~VZy-8JdQQu-0-iK zEDETSBe;od{aAE4i1aLfJ@`sQ>>Z0Pb;8=*N5N{kSHSz}bfWgZ5G()8A4mRp)Z1Ma z`G+?{ZRVI9*Xvi8v@DkQUK;?fdVtql91<@7=FL+&UTz`X?AD=t8PGG6i}B-qKdU)w%krvlaAdQ^gLn=YKVyD3VUgWc5g6xT-?!QUW+WO~x%{Jp6i_bk=pG%kF#F{i>Cm05kqMR(aC(rRh!AvOueUmvKf{Df1yG;$;;flV zm#;$EFSfq=W#WxIp(~AX5C3KR@V^v*fhE3-wEp3qe@ki3<JQN2HP6`iXy8qZs5I$(>h*@>%xcB^U{SROWjOC-O z!~d2_{by)Dw1Kf$qHCUCEl}A07w?HZ=R1JV7=M$yBn9}Sh57=7yWi0TgR+;|l|9|j zbe_G1-cNQEzIL6Vr00o>n!Ub^$+C|PZwmX@GXt$Vbu+PM(lAmm{)UOc%Ktk^A; z{-Rmzv+O?r|2gcc}Z@#SZaR85yu0s7Z-9Fj>v4Cmqj@ENdMvuX-xv?3M= z4wd*aERWP?xvh+3XpI{g8*>2-TB|HAEfpL68nUfn_}HmNQHK6BVtkQtt>J!x_eg=e zig(`rwg}h5hfx6M*LbSR{$6gHpO1UqiVxP(7qk6rvDbvf0!fzMPwXd^7FS2Ct*se0 zN>ah~U+ZK!!hsr4|tp87k8zCq0o%z3sZUMO%KyKdVx(U#G}h zmm@Uz&Cvb|^PHBwyQZz})3Fj0`{QKzn>lvdF)qEWEyM4J8v|@rX`%z z!3$Mi6X$9AQ3F3x2;gyB@C&4BfV5@NhK`1t{bDDwq&y|Bl3HZ>NzS-Dj^py`eKe83(p=X%^o z`jo(X+I=md?(`d9BCNYV@>4+LfwM-zTd!<_&TQ z>+$jNH}4GbzWc&J$P%y}k_DrE+e_Vh{&8IZXIBuo-L+{7X4-!XS4sU=<&az&`-=U= z{^98HX4uGU*~UJgC_?NT#LJ#PxC*t?=L7e6*g8EFuO@9WW2%&`M&4^ot%Lx|O7=mc z7%fyRV7^Uk*cvjbUY*6G5nX`Xfej`{nP~eAv8-*~^(mAg+X&9%auB4T>!H@I8jE)A zN8ZiO&IY4^lJROc?zwd1?t#%BF;+D4IyQWI(r=-7WV9f9Gl)v7INepohY_Mt$&Jz>?*?wVbV;}cG*&wi7zR9 z{n!r#26gT^mp0Jbyq>CgV_{J3Rtq#d$Jry+;RW>LMu{NIXGinqg)1e7rB z9)Lh-0Eqs1E;yqDXDo}aXrHtRX9jG|nmpZ99{b^JHOfI_%qu+!)=k2PY zH{)nuzWig$)rVrbMw4FN}&+wKn5 z@Afuw0SU8xbwO3brRcaMDaG7p5i6B2$uH?X2c8CvP7V2AB>Q!4f6}|Wx3C{Y^c-Ft zElI)p%=s=4WTX~vjaXGNLe-C4la^OpBDa?S`ZxEqQSy%~$wIUp;ZmxP?%0>DRgs)> z#CpCL%KHB<+~kHrNET>q1p<6I!cCiTWa@8>Li+Xng`kC_?Kzl`D%viYTaEXW<8-wS zB!H!=E?^YZh}_rH$X@YYjptoVG`%I7RI*BJ)W+Zx(~SVJ0Pnfh%QS_4ApbR`h?Q`% zEayM+dz<@z6f%aJSVjoLO7iOuPF(H$>8}`-aJrk6ULUokqRkKc(BCfpLbCClK;yI1 zQUIx-$aWzX96An|GLn)mt9^{S;JUA_>l>bP#Za)uY2C}#qV70!JT|Q&9ht=o`V4I! zaTF+ejfu=V{RtEY&CQ*(qk&&byX2TvEy%$k_6m}a?z8>z;D~%fWG}|Tu{F_J52AJa zv%r5zUHFb=H^>veho2Ya*KCG;raYiClte9lhvH|#5PTJet#Q7%n33|Xx z(wl$|36s{(Id5AyW~H~sa_T813l&dWk4w}h4!GK%&q1b-c~&>9M(=r|01;}M*Vg$? z;f;$Dt@XS+fJObLD9xiI?4GqEfo`xr3eMP>as-g`px<=f;hpLX$8e%9yDbNJeFuz{ zsM`cxj#s70{7M30pY4OvVxqkrzJ&+zsg5{WFb+uLIF}ds5x`uu9+>2l6c9K21SL-E z5JQ;d+CZE}L`WvVf%Vq>GTEG10o`~$a#`w7T;a<8E#kdiH%hIs+J5YhrSUV&l&w2Uilu4Rz>b@IMuJJLm7iiNyl&q`;ivyIFk3m zY1$j-A7APCEjajW*poYbZxlzq+gw-s(w5xGO2P;qP;C$}o*Q3Qk}lB@APH`)27*G9<{0z3-0+mM`WG_)7-MiV?q z4JEp_2+w2F&74Et4D;trVb1_oYf>Z5hg2E*Of2N2SWcb3`1EVl#jgYb zFLCYIO0=?I??Y3bvKQQO^pMx+E;g=- zVe_u?#U-MJrAtyhf(wHOBQDU1b&M$}Y}D?(lgY zd?=e48P+K9$Y1h(j%6$8^Eh}wBT2bA35L_h<)(n6E?M1czSZsh<6U4&d3Lu2N*6x8 znnqmE?j5!EJ7|wL^WyFZc0_Zef6rNB+pc+&W$|Um8-JwW^hGB~{JMC!=*rJW?o*qX zk37z7TPFT!wax59T5O|0eZ%WF;n%bZdu95uD6S|=NAkeg;TEHvE#tcpQL-Jt5=nkx zHgmFC!mU@MenhmLQ_ncu*Bd|I@cL*_qvUhI+b$-r*Ipi@voFIc_WK%ttZo0SImV$j z${rhPxpVmx;2q;khh{9(z{sAqqmr8`}TzP=3v5Oz)V_8E<4hh}w{S z{OWthOqz)+d0V?*;hjfR>v~MLriNEv{Yz5qhn0I_JrI!sp`g&VEgkPP9h$8AJ~vKt z>zyLcH;<)_9c?}ys>X=^{Ca8A)0k7fU@8ZfVVv!?Vt;qZZ*k0{NFs2E^^Rg(r_ow1 z*BwqypXI%bx{)2*+%)~cKm+GKDiyW)Tl`?1gWNiks}cvF15_+hVYsOMK~!4j^^73| zE)@f&H{^U_+L>b>_PJcuh^V@$4Y;FUKipGtgzGD{u*y>X{F`@d57kAG(-ks!qN*&` zVkzSH4M_W!Q|m9&rcGD9VD$&``VDsOH|(j!m)Zu#jg8^seID?ACwnnTZ{Jgz#T|8}IgQ3|i5seDRd9gpw>R-op zAnDwI=dC9tY>br+L#+4kbjVfh{7M0&Tgrxcq~VK^?R4PON_}wqdiG2)3pDNZ>|@Qh zHwA2-`)od-{QPkzKodlNPwjQYw`k-x_*yyVvw}y;^PUG~xy?v2o^j~++;SF8aSa21 zC68F5oRF_2FYSW5ZovE=eVR5%e7FdiBgJ=bkxC#WDso2)1Twy!JVFtq18m8F`=Wb6 z7z%?#pjS%BAaU`9_s#kjO{mQUgy}Os;sI|mi?~E^6gi30q+!C?E}&qlkbX8ki6y0q z&*h!+xTdd56T)YY{P;?ism;o6>ln|a8n4df&Q^)=+6SxXg>H=SwV%;8zJ+z7(Rd`m zS@VN8+zz4twtN@vJ{ z%q78ujWZ0Sn8ft>V%~)q4rqbYxS0iAOxw=giu%s~H ztUs9Y*;bmKDQR0F?P7AzvyCUN3Z}i~Q-JjB6%7@!$QbTqQjcRDTG}ILw7Q@j?q;6* z2Iq2e5qeeF7ykk}(cmQytg+2rnZf1m^JPPR!3{o7YjxA<{Cr-_uD)9YxicqM*eaiA zyd+(~tH#gdEz0BZ4)7>9%=ru1x2SSiRwqNP&dk=}Kiln7vmr*~7!!iD)Hw@9HJJ=E1_EsFY8Ux(z z64eSJUxuvHOLA+oM4;9UTr9+Cy&h~R2(}d_xpUE*hnfqt{1U^0IEE~tgp2sD>G~_! z^pMc-p9h>wDHU_i4lEHIffEuBGF*x!I7g{_6N_FaYs<;S!Ik<47}dUkk_E7mRp$lm z$LZ)2mPp3#jaGBn)|LAYs4woLb%&gBOf6mLH{<+F1>HDzJv;^X?sKX60%N6@`9@H6 z@z>8%`Y{@@R5$C4eAsQnRwX0kN=fdSA1_Zpb}?m4$2RlgMA6A-{|LvY&J=5$O3f{^ zSbLUNET$1VG$dEPSBpiJDso@5$4fYsE;30OVjsT)UDX=xE^<#bEyDT?n`NRD=t(K^ zX^M+hQ;P=Bhh}ur*Hx_^yZ6;vpKG|P6QzI0+`7g<)nu!e(!z8{hS!7n2-g0d@`EXl z$~eOfv5|Ay@E-)ehA$0GMK9P`EkCkuyf-aP_w8G5tzKF{yp&L0(}~hu`Di93jFcTC z=@yQCNIr5?xItZR%=-FpuiAT-@+U0uc@h#D_rs0M4f)t#?3Y~>;WciC=f4KgteGWa z!$7X(C9(GE8eaKD6QzAUgyCW?JP_KB?tfCu17VFPiWL-NyT>S)Jy;E)otesySHi>} zV)uC*19wuL^*-pNa%k#+@~?}>2v`dUkAxSU+BjpO_fcn5Y+V0vRG+Ghel6Z?v-b(o zE6D$_N?2}?U#wcRec1nKYwEtv!EUae7ie12vdC0$l_MyV-o(c*kzpN|?%ipuFL|>Y zX>q2AW8*!t`}%&SbBfu_6=4bamn*HWVps5t^ac*nwIMq(ZT+Jm{ss(o8Pahk_ZE}e zTyE&p`xBjr;?XbeZ_0*nh*sQxO8QzP$}!m- zR>YRUMK`Km8#G$M&)X@wXIm(vl$miZxsO$1m33vwBd8MjS*{-I;Tu-#>&FX* z42wr!%}XfGf120&_`00c0yilyU#^6Xv!{Hw+}qE#-y;pKNmRTT$Ckf1bdhGm>TtP} zJdRDH;!sPSos^qVylGIzQ#~mAlKb_W%^nT6rU#r*q&ZzkcaMwav!OeIOt*`R-239V z9W^VBe~vXjDN$f{l`T_UVB*)io6bd}J^uZA!FIjQWEuOJ+~9aWT&eaP zM};(}OcHu$y@P_jArcZqMUJZT23p0}ySpIeLF=}=rxwPQc^h_!M( z{Alx8jXJaTFh<8sZI!VfrXa`UYNeM2bm|@#&p_boxSTh7g6)DF+bG^L$yLX%n9ZM_ z#rEI@zkyBU?mfrct=J(yyTQ(DJE)ZM@n^TVYX&kO86G45rji)PaqX_lrCqK~T%0Qy`?xQJo@TiKYg1KO zT&Aejj1u;EI}nx0!j_@6jvh5V)xEDp=jXM2{O%HGxr;ctp8w%=%Zg8JY0Sm@O|s8Y zLxj-Rv`l`04wZd-?k-bE9hv5Gt!^!TrVKEc878=gIvX6IigZ)t2d;I91zX2EmBkZYAH|dD6*3;jRALUl;VST}O3uTAu^`pwUQudm=Hzh?|r?C{5>)`k=vbe7s_GTTtw0Dd#?;H>Z0}gQYxNW)wqiuqVGwR3j|%lv+>CEh zdPE{<-O=6rk}kk$-)bf?<^g>cIlh`waW{4``kbx7+pZ}wwP4DW=HiYT;6|~-G3T{aA;^Nd@vLFebZp7@p-zREXzpHG zMp5cKfcoCH*DrY4*Vui_uNx@_%$%h}z>h;1P~ZH#wJOHKlCnGB@&4k87W-a#JZJO$ zp)O7VNX6~aJ`xbA-tDx+Oyo0HYEkS-y--lV4|aVl|Ig8IUk*S zTat=TBk9!On`}B{NyZ+>OZCUa4PIDs5eOhT@_i}aE#l^!F1X1YOr()MnmFE53r&cp z)3TV!i9;&WWOd$S)nLoxtSa~UG1+2fa3|IR{Q#Xww__H86ix=wgx1{7gf0tF^lpM- zcSZdU;~xOsC-&UXY+(Le|LvmV55Nb82L}upux23dYJQD{Yrq#{R&aXTmL;};eyij$ zYDEVWOBdsQ8ey@plE?2`Ni0in7)FR=S_%oF-c8 zhJ6p~kMTw6_4_TK`Fs>?bMc@q>;yOT3?14z0sjr%$Y z-R>(wA{>ucJ6+cEL>5w_Pi?LbM_^Gil0v-IeYWCTfshRN(ww+0NXlX8zRTLd@bWHL zK!*Jj>F|EI0CNtH?aT$k0XWV5O&(QVSycG)KoB2gDUCJZ1HenMxJy4%^3n7VSFw7s zn@oa*rT#mZiFOA!9#IG8(`_K7EhrU+Uf+P!1F_}a{=rpn$>x;1&K0~EX^23(ec1=9 zct~75AES*4Om@)DIZdM>9>D9dLD~(t2qp)o+{^LtU5!Dk>xdc4p`r{#fjxVm%2PFT z`RdQOB|YAzr=m?fq6C0kQ-iwC%j}7Lluv(wy3o#64O~CD&bq#ahO1P-pXhb7sFqv_ zS~6AFzOO4FYjn!J=RQ!}EgiOg1=3P2W`w0cgVlIV=yp6hmp#{SASOl%xZv)|rimR! zYi;F<9EqbFZlRFs@EcarL(m?Kim&r4fR#wDjrANZw$A&EI zl61Ck2(8q98{Lq0$b`|AjtjZ%S7|D5w!xX!P&?p0KxZ#r!qzxR49TNs)UOl-!Xg$fOI;=J&kbfb+P=@n#$>+! zl&%pUZE6ZM{xL6*u?%^{ZE1TtFW8clw$RRxto z?yW_9&%hYZ06_baJjonH z5be#&RTitrw6iS8FI(X zR4Lxbq%V2$b{T-?3qEULpcwN$Kfc6!jrjTJ3WjgP5@felNlf9oXdVQD0P~2>$qCh6 zs=%Mub21bnEy1^d0auAzBKzA=jr5dq*c` zA@QH>H;I;4wb;)qor-I{rBa*k_mgQ&2CX9i^526^B#z8TTOz$|pS%WIq4}jcnIU~V_!0fDU)q{HD3@=`Q%4YNvZ@x2T4if#Rlj3g zR1#5jEfpR5o>?t-{g$bkxk#3Z^>DOQ5VO(i;&W{8LMu5@V4 z4I7hf4mz2ItPo0S?cMhg`#K3!#u7DskETp_B>^H^k722Bj(TIbuDz7&jnOD;2;x## z+!Dm8H~kC|qoN_a;S>uJixyt3*pXC(@I#I6$JAO~4Dq zeair+U#6UkuuA_Ni(@22H%vJ=W#^y(m((V_KH{3X)pQ!dd1i7w7JPP^Iq zZC+yqo27q=ig~fv-hZ%KnCd;mT`YW16pJy&3Qerpa2ig*U=GXyi`WK&m&=al&hTTyd?Hv;N{CAJJmaOYr-DI z=-RQn%?4oaACSia54Blw2RQJN&9TojKrzh&x@^}OijbHQz}N>Mvx`Em7;(FL+5@h* zz*bk|+6l{EATryV&PsBm;FU>tUh_%ziPG@e;xbC;Q~cYStz8eXD!iF}ga`6Bfn%6l zAaOJPWqpP)?sle|mH5W2>+m>q=S}#Kz6{ihxZ2TI#=OmXlJEEz-U0?VUv>6X3b-lt zSL6-t_!z=!}=F9;dy}!1-Wlw%>&ZUy{~DpkZAl!Ol6;X>di}nU+yFxt?&4s zs{#~oWJa-R`J)7n3zjqi3Xi^8v@loZRd3KG}>NkI4R5s zYdiQ8nvW~$>sYJlS14ujc0T75unAT}{rh~s8j2Rr9~>qv`?62D$jp3 z*r@$ss(+KdKf>k9{C6b^dDg8nrOr}AhF`smjQz^c<$hRzbwLIB1?Q_vGsYUI2F-EL z)#ggT<*K?AQh-+e%2MxP*_f!#st-$|L*YAnfv~B-?Iqo|Cp%x?Zp{F4$0pZ^T_f@^ zlTx-=j2=2ECdeNFa;)zYrNg_W5w%tOP}}2z9UjX!YUt1X2#MnB#FK8&9auSF3Xb(N zM6QsYGihLRxoQmbTkrjhY2Z2VPA}cjRItxeP+aNs_<>#e*h0^nnPtDQD6ZCJ!SbIp z8VOd`z9(k#=Cm7YSDrV)R*^6w5m@Z){rsKc^{oWDCSVZsgFpplvy(2sN`-l$}kMkQSk}};@>ON)WvKxt?6}t zjj(6U%&THPv!(2Utf_(cXDI<}BH;1qB-(u3Z)q$lwMIJ@JVLN!WzU~2^4Y@JfSsN> z>08hp_N6|fBgb!^643D!KFVh)?QNcayW|l5%=>ku{+dr8kvFYKr+4ZcdvJUWTrMwA zzUuuLz-EDl)@VG57A*l8pMkLI+P;HXJClz_3FW02L?q52Orx1i23nlm;cBaw9(0l2 z^Dgj@a~%}4^sRV9=?{HlAn09H$hjmQ91mf|O*31LZSPm*&(I9cR`6_PCwrNqO)t>j znJLsQY(PtQ8EL>9`LRCi7xYUWGTE&s5ei0@=)yNTUFaKq)vimbi=W%vnY~ug33)x4 z({tOj&eUA$V3TT_5gReKP#Eg^0va{Ubd;wZdhYT#yB9T=aIvq#z*-1abnpEWUdy%B zp{bI!t!*(x(^B`t_L*A4MqQV*rKt=66QkHnYdX$B|JlnZyBj4Xcrr3}siK>69#6wA zCm)wp^{(RUpVbxuN-~vT40k@*_NedzCpVMAhd~jMi##==mvM!)p2_7ES&>j?PMt(B z!DP1Avmz5?}YG1vM$fo zSY=FbxX%(DjSN)mz2(GMn)7pE zKZrsn9o9Cmfdx1$PII}x%cTON&ij*M>WuHsxxu-pL*2$j(6U(7g`;Nl+`Fr{-?c1d zvhb;|BJnBGb6HCK&$A7dL7z$RY8P5dRa+i4t@ojCg*tV#df5B6PR+TwOw_bZ?cOD2 z5@sY!eSLfOtFwdK=>3Sb>F4j{k5RDr+^OzktR!?!mSFh#Vi#)lbHxO-Y;hx+L*+DD zYlj41lgS1->1qF?pgCDSn&hpGeN9?wU}+;N{v0=w3X_7K)e+)+RGZ5_i2 zyN6Vr;jxC}8Yso%E&d{y)1~^ol_F0!r+oUO62%yX$s(i8n0wVm-94whoP6K$GQXh5 zNJuM!Ys#B_v{7ThQF(+VHG$Z4Nc)aJ+S92l+0jmpuv9}lH^5q9AjdeH>i!vuOBX5B zL~m8&b%svZRy#k(J#Ul9s?pecxR}hj}EYs6g% zV5VQKn^7o#{+{1_8bu7Ra+KH>iMoE&oS{nIQf6fO$=fn|j6xaGqlU*5Ksz6McPY(y z2NwODq=-4X5A)nosG9V-oLbW;5*3ZFtoM)0h3r9Ly656CJ7(d@hUH(Sol0FbFf&zC zwq75EzL?_uOxHtVNbJ?b(#%?HHw|4jqd(|c0XX%8A7Rq~H{v&9F2EyDlX0R{y~fBO zWS{+_H6E_V=K+R0pONkTxP2+OOSVWQHe_D{M@AMr_r`rh2;eH;K@iQepOMk0DtKAM zDCw!?wEk*J7gHJ}>s?hK>DGP~T>^EzOETYkz6SP3M}#zmjG1Bp&u4#FzZi2TGQY48 zM{gBv^<_j;%GEC=a(Lb|Zz=MR`h|bn^OZNJM7su<=A1nrcq8t!BSF?f=X2Q^)le>YJG<01EO1?myX1)t0j)o$39$;H%T70suL@8m2*b=P%pm(UH-k!P4FfKMyVWR1nmstU7Xke1pz%(nt3i9l*5R4jS1 z@xyGHygHS8F_jZbPRF2S_UX}0@({B28IOT>K9s|F-E5mnSLeml_*4A& zvXWEFeKi_MDvJ|G+dj#}+0hFy5RM(z8mIOiCkP%$e4-i}6InEunQnh^!ZFo)D9+tz zrfNQen1LPh(g}5o(wuR|7f4z~<9oo9Im4cd7?>p`5C)+%sDY@i_!0LNm}z`x^!4g* zK-@=Hg>~j~SVHhXfURBaxxrEp4||eTP`}rnt)%rRD4U0#?!dr|e`!r)B_RWxY1R_K zXJ8&>5$9J_F=4~B3efe$@w_K`@SgHmK$BTXG+u*wTfue;%IN_Cpw86J_nyZ7NJNu^ z=jEq7Ac31@n6K9B+*FI@<-F^Tu-g%fwXU8d*Fcx_!mW57)^<*l#ESSgDt?Vsv4&9D zN>;^qC=fwJ64{Y4m%H=kSLU>P|0IVm*t~xnSN8%x(^Crvw%@;TM8_%=VN5$q;!h_( z_59$wI%pgOLIuWBb&(S$!X12OWV9XVNV{!t9;&OulpD$I^i$r&H*Yg}%~$Xuwo>@x zEw{cf_rN-i*FHNTjtRHQ^Q)u{>HQ888B|}5@*BQf*Ub;dz=doZeRm|E#{z+>+}&M@ z@5u&93DuzF4y^@dm#P{>AjrsopE%R3sS~Q~bywnBK}F}`#@)$p2iTH+o);UW5FZFk z%1-r>tUcUV(wx^$`Cy7q7GDXpM(DRLj9TSW<)+`V zQ72^xlFYtnmM*M{9RtuiPda+{Lrc|z(KIbvGKU6neiF;2UB)K_R;u?l`r$T%hy%}T z*i9j}B%7^K*EwGkbV!VZydwAvgTw@Aj-EBM8mMbjpK&KZ&~+?d>UnDXx?S18M{?@{ zlm}velQGGb>b9iby2-j<%r4(j8xgxzWuP&Tce_Znq1f7Mj)%tm>2%4{j%xN_!8c=S(3 zGsN&zHHwBWHlyaE{TxF7g>ozv9>sf zisIG==>eg;umTY^a0S!M-#&9+U#qJtgcSv4^RdOF<^(~zhK3)NUt2m&<$Bx3gyPNmdLZ? zIBwCmI_Hm0OsxosDlzsSo>1ei4~sVERLW!oS$%VYg|VdK<%(4-oSGxO%~_mbqEZa0 z9EpE@F*wI4S%KyZd%R)?II*sE&cywC=A46YnufTr^y@yd(fK;nq^k@64<&pP|banFJ zYeqNnp<`|6w+Dqq_gRNlr#IxyjJ$n zAVu40CMRk59q+ijBH~kD(v8vQ*hda!)r7NGw8Xmd@)fENzONbkDa92uReLdozkAF} zmfwMzLgGiSH-|AWO&1S{#@_!=%=%y9>*@1lH%WAfy;!;?5UC=sgj`nf6uY8@@qDl% z;!vE$BzkjPiR^rC@>y_;38|00y5dqWPmRdPzkr*&E(LViPTwUiGjpoSmOCBw;V8k3tGxY$W+yMS|F!SCPZ4Y?pqW z#IGabU?&MEJ^)CsI#TbMoJ=O8?9B9L2|;aKZ2yn5 zmEYh#r)(tuWT8pTjPR0ni#)Zj(s^*$f(RZ&MV3p~4s&$iP7acT@_m}c_^jYWG8Wdq zMRU9zN9Bk65L`n6PkzynzlkdVz)@_WV&FkULVB>vnNKd+O{Vxo83EsCum_j985~*% z_75k)vtE)4Bu%E9yBY(DEpJbs*%wi(vElmj(Owx$p)7b-&)Shp(3!_OzI*YtX?;X6 zW z$cOBXnJY+6`crIeg(qS5=WL=2uR7FV(Er#e&HvaBJIXZtD@FWo&Ghft=%;)oJz~^I zdOnm_5?XZ6X)3V0!Z9^=jWDqKT>8A4q|uEq`f~pDnuNsInf#{~_{G-oLKY^_{#jKiOI)-8Hg*nXIC7V8W2IoiuA#*cfyHEDcXu0H4WSi|r<;^oB1 zc;(b-WL&Zq zAP(*?1Q+0D;5zj_U!;UXsjxg4}LLdj{s-_U#wZa ze=qdkL96a z7;rrAyT5tKtU3IG*y_Vi3Dw=B_G+NM28R!6DSP-z90efvs!YD0-C!{*kC(TSo})-+ z?+M_%eQosSNn}sHTY}%OZrE=S3}BXfKHVH>?NoD z7h2(Q*X?4PzN}kCX-AHz70gfUotu5uUO^__m#9w)n^|vAhE#(Uir}wq{#S}7rdl5A zNyZ@J$2{)uD7QPGf#aRf6+Qp|VdB3)<3$rN=FY;n{Sn{oC$^bsi8MDbq}f72GF<>~H0Wc=HROHULLujJ1CoiSfH+iua@ z&iuK-Cavt_n@IgO7~|hq-cNTaYY`J231T6U@U9@kzw52a_zIJgl?Zel1@3A6vS+>8!F$j%bvhiyHD) zq<&iM?rc^sL@#rejE(kB-1L97W=%~-?o@UsYG5^nC5&|O4ejT@0}Aac)`Gx(^GPSV9U2!zm9vckS8V!LiQmr+jhM--pLBB{B!SmVW+QQl^>Z!w)`5{dL!a%#`M5 zd#_0+=;?E}S|03F$|>>&m|p%n6E!9yXMM2u(&B+zTV2zdrGtKKkxUF7k0xqIFH@AV z{IC0d^_c34BDmKLh;D6$lmlP@ literal 0 HcmV?d00001 diff --git a/side-quests/smol-nextflow/bin/classify.py b/side-quests/smol-nextflow/bin/classify.py deleted file mode 100755 index af63518c3..000000000 --- a/side-quests/smol-nextflow/bin/classify.py +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env -S uv run -# /// script -# dependencies = [ -# "torch>=2.0.0", -# "pillow>=10.0.0", -# "open-clip-torch>=2.20.0", -# ] -# /// -"""Classify images using MetaCLIP with local weights.""" - -from pathlib import Path -from PIL import Image -import argparse -import json -import open_clip -import sys -import torch - -def classify_images(image_dir: Path, labels: list[str], model_path: Path, - json_output: bool = False, architecture: str = None): - """Classify images in a directory using MetaCLIP.""" - # Auto-detect architecture from model filename if not provided - if architecture is None: - filename = model_path.name.lower() - if 'b32' in filename or '32' in filename: - architecture = 'ViT-B-32-quickgelu' - elif 'b16' in filename or '16' in filename: - architecture = 'ViT-B-16-quickgelu' - elif 'l14' in filename: - architecture = 'ViT-L-14-quickgelu' - elif 'h14' in filename: - architecture = 'ViT-H-14-quickgelu' - else: - raise ValueError(f"Cannot infer architecture from {model_path.name}. " - f"Please specify --architecture") - - print(f"Using architecture: {architecture}", file=sys.stderr) - - # Load model and preprocessing - model, _, preprocess = open_clip.create_model_and_transforms( - architecture, - pretrained=str(model_path), - weights_only=False # Trust MetaCLIP checkpoint from Facebook Research - ) - tokenizer = open_clip.get_tokenizer(architecture) - # Prepare text labels - text = tokenizer(labels) - - # Process each image - results = [] - for img_path in image_dir.glob('*'): - if img_path.suffix.lower() in ['.jpg', '.jpeg', '.png', '.gif', '.bmp']: - try: - image = preprocess(Image.open(img_path)).unsqueeze(0) - - with torch.no_grad(): - image_features = model.encode_image(image) - text_features = model.encode_text(text) - - # Normalize and compute similarity - image_features /= image_features.norm(dim=-1, keepdim=True) - text_features /= text_features.norm(dim=-1, keepdim=True) - similarity = (100.0 * image_features @ text_features.T).softmax(dim=-1) - - # Get all confidences for all labels - confidences = {label: float(conf) for label, conf in zip(labels, similarity[0])} - - # Get top prediction - values, indices = similarity[0].topk(1) - prediction = labels[indices[0]] - confidence = values[0].item() - - result = { - 'file': img_path.name, - 'path': str(img_path), - 'prediction': prediction, - 'confidence': confidence, - 'all_confidences': confidences - } - - results.append(result) - - if json_output: - # Print JSONL (one JSON object per line) - print(json.dumps(result)) - else: - # Print human-readable format - print(f"{img_path.name}: {prediction} ({confidence:.2%})") - - except Exception as e: - error_result = { - 'file': img_path.name, - 'path': str(img_path), - 'error': str(e) - } - if json_output: - print(json.dumps(error_result)) - else: - print(f"Error processing {img_path.name}: {e}") - - return results - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Classify images using MetaCLIP") - parser.add_argument( - '--image-dir', - type=Path, - default=Path("data/pics"), - help='Directory containing images to classify (default: data/pics)' - ) - parser.add_argument( - '--model-path', - type=Path, - default=Path("data/models/b32_400m.pt"), - help='Path to MetaCLIP model weights (default: data/models/b32_400m.pt)' - ) - parser.add_argument( - '--labels', - nargs='+', - default=["cute cat", "ugly cat"], - help='Labels for classification (default: ["cute cat", "ugly cat"])' - ) - parser.add_argument( - '--json', - action='store_true', - help='Output results as JSONL (one JSON object per line) to stdout' - ) - parser.add_argument( - '--architecture', - type=str, - choices=['ViT-B-32-quickgelu', 'ViT-B-16-quickgelu', 'ViT-L-14-quickgelu', 'ViT-H-14-quickgelu'], - help='Model architecture (auto-detected from filename if not specified)' - ) - - args = parser.parse_args() - - results = classify_images( - args.image_dir, - args.labels, - args.model_path, - args.json, - args.architecture - ) - diff --git a/side-quests/smol-nextflow/environment.yml b/side-quests/smol-nextflow/environment.yml deleted file mode 100644 index c7a564fdb..000000000 --- a/side-quests/smol-nextflow/environment.yml +++ /dev/null @@ -1,12 +0,0 @@ -name: metaclip -channels: - - pytorch - - conda-forge - - defaults -dependencies: - - python=3.11 - - pytorch - - pillow - - pip - - pip: - - open-clip-torch>=2.20.0 From 955c8779ecbd8f1271d9677f513324a28ba1658d Mon Sep 17 00:00:00 2001 From: Rob Syme Date: Tue, 18 Nov 2025 06:00:21 +0800 Subject: [PATCH 06/22] Adding side-quest but will remove and squash later --- side-quests/smol-nextflow/main.nf | 120 ++++- side-quests/smol-nextflow/outline.md | 695 +++++++++++++++++++++++++-- 2 files changed, 749 insertions(+), 66 deletions(-) diff --git a/side-quests/smol-nextflow/main.nf b/side-quests/smol-nextflow/main.nf index b0a510fba..0ddac5bc0 100644 --- a/side-quests/smol-nextflow/main.nf +++ b/side-quests/smol-nextflow/main.nf @@ -1,48 +1,122 @@ -// params.models = "https://dl.fbaipublicfiles.com/MMPT/metaclip/b32_400m.pt" -params.models = "data/models/b32_400m.pt" +params.model = "${projectDir}/data/models/b32_400m.pt" params.width = 400 workflow { - images = channel.fromPath("data/pics/*.{png,gif,jpg}") - | map { img -> [[id: img.baseName], img] } + main: + images = channel.fromPath("data/pics/*.{png,gif,jpg}") + | map { img -> [[id: img.baseName], img] } - models = channel.fromPath("data/models/b32_400m.pt") - | map { model -> [[modelId: model.baseName], model] } + Resize(images, params.width) - images - | combine(models) - | map { imgMeta, img, modelMeta, model -> [imgMeta + modelMeta, model, img]} - | Classify - | view + Classify(images, file(params.model)) - return + Classify.out + | join(Resize.out) + | groupTuple(by: 1) + | Collage + | map { _label, img -> img } + | collect + | CombineImages - Resize(images, params.width) - | view + Classify.out + | join(Resize.out) + | map { meta, label, image -> meta + [label:label, image:image] } + | set { classifiedMaps } + publish: + collage = CombineImages.out + classified = classifiedMaps } +output { + collage { + mode 'copy' + } + classified { + mode 'copy' + path { sample -> "images/${sample.label.replaceAll(/\s+/, '_')}" } + index { + header true + path 'images/cats.json' + } + } +} + + process Resize { + container 'minidocks/imagemagick:7' + input: tuple val(meta), path(img) val(width) - output: tuple val(meta), path("resized.png") - script: "convert $img -resize ${width}x resized.png" + output: tuple val(meta), path("resized-*") + script: "magick ${img} -resize ${width}x resized-${img.baseName}.png" } process Classify { + container 'community.wave.seqera.io/library/pip_open-clip-torch_pillow_torch:83edd876e95b9a8e' + memory '13G' + + input: + tuple val(meta), path(img) + path(model) + output: tuple val(meta), stdout + script: "classify.py --model-path $model ${img}" +} + +process ClassifyJson { memory '8G' - input: tuple val(meta), path(model), path(pic) - output: tuple val(meta), path('summary.json') - script: "classify.py --model-path $model --image-dir . --json > summary.json" + container 'community.wave.seqera.io/library/pip_open-clip-torch_pillow_torch:83edd876e95b9a8e' + + input: + tuple val(meta), path(img) + path(model) + output: tuple val(meta), path("*.json") + script: "classify.py --model-path $model ${img} --json > out.json" } -process Measure { - input: path(img) - output: tuple val(img.baseName), path('dimensions.txt') +process Collage { + container 'minidocks/imagemagick:7' + + input: tuple val(metadatas), val(label), path("inputs/*.png") + output: tuple val(label), path("collage.png") script: """ - identify -format "%wx%h" "$img" > dimensions.txt + magick montage inputs/* \\ + -geometry +10+10 \\ + -background black \\ + +polaroid \\ + -background '#ffbe76' \\ + collage_nolabel.png + magick montage \\ + -pointsize 48 \\ + -label '$label' \\ + -geometry +0+0 \\ + -background "#f0932b" \\ + collage_nolabel.png collage.png """ } +process CombineImages { + container 'minidocks/imagemagick:7' + + input: path "in.*.png" + output: path "collage_all.png" + script: + """ + magick montage \\ + -geometry +10+10 \\ + -quality 05 \\ + -background '#ffbe76' \\ + -border 5 \\ + -bordercolor '#f0932b' \\ + in.*.png \\ + collage_all.png + """ +} + +process Measure { + input: path(img) + output: tuple val(img.baseName), path('dimensions.txt') + script: "identify -format '%wx%h' '$img' > dimensions.txt" +} diff --git a/side-quests/smol-nextflow/outline.md b/side-quests/smol-nextflow/outline.md index 8a128d390..d1f6de172 100644 --- a/side-quests/smol-nextflow/outline.md +++ b/side-quests/smol-nextflow/outline.md @@ -1,20 +1,36 @@ # Outline -What are the Nextflow concepts being introduced at each stage? - ## Introduction -Nextflow offers you a way to iterate over a collection of files, so let's grab some files to iterate over. We're going to write a workflow which produces a gallery of cute and not-so-cute cats. First things we're going to need are some cats. -I've created a small script that pull from the Cat-As-A-Service API to give us some random cats. +Nextflow offers you a way to iterate over a collection of files, so let's grab some files to iterate over. We're going to write a workflow which produces a gallery of good and bad cats. First things we're going to need are some cats. + +We're starting with a (nearly) empty directory. There is a `.stuff` directory that contains some bits and pieces to help us along during the workshop, but you can imagine that we're essentially starting from scratch. + +The first thing we're going to need is some data. I've created a small script that pull from the Cat-As-A-Service API to give us some random cats. ```bash -bin/cat_me.sh --help +.stuff/cat_me.sh --help ``` To start, lets grab 4 cats. By default, the script will save the images to `./data/pics`: ```bash -bin/cat_me.sh --count 4 --prefix data/pics +.stuff/cat_me.sh --count 4 --prefix data/pics +``` + +This will generate some example data. It will look something like this: + +``` +data +└── pics + ├── 5n4MTAC6ld0bVeCe.jpg + ├── 5n4MTAC6ld0bVeCe.txt + ├── IOSNx33kkgkiPfaP.jpg + ├── IOSNx33kkgkiPfaP.txt + ├── IRuDgdPJZFA39dyf.jpg + ├── IRuDgdPJZFA39dyf.txt + ├── uq5KqqiF0qpgTQVA.jpg + └── uq5KqqiF0qpgTQVA.txt ``` Now lets iterate over those images in Nextflow. To start, we'll just create a channel of those images. We're not gong to do anything with them, but to make sure that everything is working, we connect the channel to the `view` operator which takes the things in the channel (files in our case) and prints a String representation of those things to the command line: @@ -26,6 +42,8 @@ workflow { } ``` +TODO: Claude: Brief explanation what the {png,gif,jpg} syntax is doing (multiple glob pattern) + ## Channels Let's try actually doing something with those images. We'll start with something simple - resizing images. We'll need to download some software to do so. Eventually, we'll talk about containers and reproducibility, but just to start, let's download the software to our machine: @@ -51,12 +69,14 @@ workflow { process Resize { input: path(img) output: path("resized-*") - script: "convert ${img} -resize 400x resized-${img}" + script: "magick ${img} -resize ${width}x resized-${img.baseName}.png" } ``` ### Input + The input block describes what we expect to take from the input channel. The "things" in the channel can be have a type. The most common "types" are + - `val` (values Strings, integers, Maps, complex obects), - `path` (paths like directories, files), or - `tuple` (a collection of values and paths). @@ -64,9 +84,11 @@ The input block describes what we expect to take from the input channel. The "th We'll see more of those later, but for the moment, the channel we created in the `workflow` block is a channel of files, so in our process definition, we'll say "I'm gong to supply a channel of paths to this process, and as our process takes thinsg from that channel to spawn a new process, we'll call the thing in channel `img`. ### Output + The output block describes what we want to emit into the process' output channel. Again, we can describe the "type" of thing emitted - `val`, `path`, `tuple` and others. For now, we'll promise to produce a file (or directory) that matches the glob pattern `reseized-*`. ### Script + The script block describes what work we want to do on each of the things in the channel - how we're going to transform each of the things we pull from the input channel into the files or values we promised to emit into the outupt channel. By default, the script block will be rendered into a bash script, but you can use any interpreted language that makes sese to you - python, ruby, R, zsh, closure, whatever. In this introductory workshop, we'll stick with the default bash. @@ -76,7 +98,7 @@ We run the "convert" command from imagemagick which performs many types of manip For example, if the "thing" in the channel is the image `kitten.jpg`, then when Nextflow creates a new Resize task for this file, it will "render" our script block into bash, replacing the `${img}` variables with the path to produce this valid bash: ```bash -convert kitten.jpg -resize 400x resized-kitten.jpg +magick kitten.jpg -resize 400x resized-kitten.jpg ``` Now let's run our workflow! We'll iterate over all of the images in `data/pics` (relative to our current location) and produce a channel of resized pictures that we then pipe into the `view` operator to print the channel contents to stdout. @@ -88,10 +110,11 @@ TODO: Explain that each tasks is run in a separate directory.This is to ensure i TODO: Show the contents of one of the task work directories. ## Harmonization + One of the nice features of the `convert` utility is that it will also do file format conversion for us. It will infer the format from the extension of the final argument. For example, if we execute ```bash -convert kitten.jpg -resize 400x resized-kitten.png +magick kitten.jpg -resize 400x resized-kitten.png ``` The `convert` utility will both resize the image and convert the jpg to png format. Let's say we want to ensure that downstream in our workflow, we'd like to ensure all images are in the png format. How might we modify our `script` block to replace the extension or pull out the file basename so that we can append the `.png` extension? @@ -100,38 +123,40 @@ If you're a bash wizard, you might know that if you have a variable `$myFile` wi ```bash file=kitten.jpg -convert "$file" -resize 400x "${file%.*}.png" +magick "$file" -resize 400x "${file%.*}.png" ``` Or perhaps you use the `basename` utility: ```bash -convert "$file" -resize 400x "$(basename "$file" .${file##*.}).png" +magick "$file" -resize 400x "$(basename "$file" .${file##*.}).png" ``` -I can never remember these syntax. Forgunately for me, when inside the script block the `img` variable is not a bash variable - it's a Nextflow variable, and Nextflow provides some convenience methods for operating on those path objects. +I love bash, but it's easy to forget this syntax or mistype it. Fortunately for us, when inside the script block the `img` variable is not a bash variable - it's a Nextflow variable, and Nextflow provides some convenience methods for operating on those path objects. The full list is available in the [Nextflow stdlib documentation](https://www.nextflow.io/docs/latest/reference/stdlib-types.html#stdlib-types-path), but one handy method is `baseName` We can simply call `${img.baseName}` to retun the file base name. For example: ```nextflow process Resize { input: path(img) output: path("resized-*") - script: "convert ${img} -resize 400x resized-${img.baseName}.png" + script: "magick ${img} -resize 400x resized-${img.baseName}.png" } ``` ## Parameters + What if we want to make our workflow a little more flexible. Let's pull out the width and expose it as a parameter to the user. ```nextflow process Resize { input: path(img) output: path("resized-*") - script: "convert $img -resize ${width}x resized-${img.baseName}.png" + script: "magick $img -resize ${width}x resized-${img.baseName}.png" } ``` Now we can run wth + ```bash nextflow run . --width 300 ``` @@ -151,7 +176,7 @@ process Resize { path(img) val(width) output: path("resized-*") - script: "convert $img -resize ${width}x resized-${img.baseName}.png" + script: "magick $img -resize ${width}x resized-${img.baseName}.png" } ``` @@ -162,9 +187,10 @@ nextflow run . --width 500 ``` ## Extracting an ID + Great, but I'd like a way of retaining the original IDs. -TODO: Explain the map operator and explain closures +TODO: Claude: Explain the `map` operator and explain closures (3 sentences) ```nextflow workflow { @@ -190,20 +216,8 @@ process Resize { input: tuple val(meta), path(img) val(width) - output: path("resized/*") - script: "convert $img -resize ${width}x resized-${img.baseName}.png" -} -``` - -This is better, but now that we have that critical metadata being passed through the channels in our "meta" object, we can stop encoding the id in the filenames. We can also just pass the "meta" object through to the output channel so that the newly resized image stays locked with the meta. - -```nextflow -process Resize { - input: - tuple val(meta), path(img) - val(width) - output: tuple val(meta), path("resized.png") - script: "convert $img -resize ${width}x resized.png" + output: path("resized-*") + script: "magick $img -resize ${width}x resized-${img.baseName}.png" } ``` @@ -215,35 +229,630 @@ nextflow run . ## Classification -We have a little classification script - bin/classify.py, which you can run with: +Let's get the fun part - the cat sorting. We have a little classification script - `classify.py` that I've provided in the `.stuff` directory. In your research sometimes you have small accessory scripts that are useful for your pipeliens. We're using a python script here in this workshop example, but this patttern will hold for scripts written in perl, ruby, R, python, closurescript, or any of the other interpreted languages. + +Let's pull the file out into a new `bin` directory: + +``` +mkdir -p bin +cp .stuff/classify.py bin/ +``` + +The script requires some dependencies. Again, we'll do this the slow/painful way one time before we demonstrate how to use containers to encapsulate the software dependencies. + +We'll grab one more file from our `.stuff` directory - a pyproject.toml file which is a way of describing softare dependencies for Python projects. This is unrelated to Nextflow, but an example of one of the (many) ways in which different languages and frameworks might install software. + +You can install the dependencies and activate the environment with: ```bash -bin/classify.py --help +cp .stuff/pyproject.toml . +uv sync +source .venv/bin/activate ``` +which you can run with: + +```bash +bin/classify.py --help ``` -usage: classify.py [-h] [--image-dir IMAGE_DIR] [--model-path MODEL_PATH] [--labels LABELS [LABELS ...]] [--json] [--architecture {ViT-B-32-quickgelu,ViT-B-16-quickgelu,ViT-L-14-quickgelu,ViT-H-14-quickgelu}] -Classify images using MetaCLIP +```` +usage: classify.py [-h] [--model-path MODEL_PATH] [--labels LABELS [LABELS ...]] [--json] image + +Classify a single image using MetaCLIP + +positional arguments: + image Path to the image file to classify options: -h, --help show this help message and exit - --image-dir IMAGE_DIR - Directory containing images to classify (default: data/pics) --model-path MODEL_PATH Path to MetaCLIP model weights (default: data/models/b32_400m.pt) --labels LABELS [LABELS ...] - Labels for classification (default: ["cute cat", "ugly cat"]) - --json Output results as JSONL (one JSON object per line) to stdout + Labels for classification (default: ["good cat", "bad cat"]) + --json Output result as JSON to stdout --architecture {ViT-B-32-quickgelu,ViT-B-16-quickgelu,ViT-L-14-quickgelu,ViT-H-14-quickgelu} - Model architecture (auto-detected from filename if not specified) -``` + Model architecture (auto-detected from filename if not specified)``` +```` The script takes images, a model, and a set of labels and classifies each of the images according to the labels. To run the script outside of Nextflow, we'll need to download one of the models. Do so with: ```bash -cd data/models -wget https://dl.fbaipublicfiles.com/MMPT/metaclip/b32_400m.pt -cd ../../ +mkdir -p data/models +(cd data/models && wget https://dl.fbaipublicfiles.com/MMPT/metaclip/b32_400m.pt) +``` + +Now let's create a `Classify` process that will take two channels - one channel of images and one channel that supplies the model: + +```nextflow +process Classify { + input: + tuple val(meta), path(img) + path(model) + output: tuple val(meta), stdout + script: "classify.py --model-path $model ${img}" +} +``` + +Note here that we're calling the `classify.py` script directly, even though we can't do that from the command line (we had to provide the relative or absolute path). This is because Nextflow automatically adds the `bin` directory (relative to the main.nf) to the `$PATH` for all Nextflow tasks. This is a very convenient way to bundle accessory scripts and snippets with your workflow. + +Processes can have multiple channels as input or as output. A process will continue to emit tasks as long as it can pull an item from each of the input channels. We could create a new channel for the model, and define a sensible default: + +```nextflow +params.model = "${projectDir}/data/models/b32_400m.pt" + +workflow { + images = channel.fromPath("data/pics/*.{png,gif,jpg}") + | map { img -> [[id: img.baseName], img] } + + model_channel = channel.fromPath(params.model) + // rest of the workflow +} +``` + +... which would return a channel with a single item. Try supplying this channel as input to our Classify process: + +```nextflow +params.model = "${projectDir}/data/models/b32_400m.pt" + +workflow { + images = channel.fromPath("data/pics/*.{png,gif,jpg}") + | map { img -> [[id: img.baseName], img] } + + model_channel = channel.fromPath(params.model) + Classify(images, model_channel) + // rest of the workflow +} +``` + +What happens when you run the workflow? Given what we know about the channels, what might be happening? + +Answers: The Classify process only spawns a single task. This is because after pulling the model path from the second input channel on the first iteration, the channel is empty, so no more Classify tasks can be submitted for execution. + +There are two types of channel in Nextflow - queue channels and value channels. Queue channels are exhaustible - they have a set number of items in the channel and each processes can only take each item in the channel once. The second type of channel is a value channel, which is a channel of only a single item. This item is emiited without exhaustion. + +There are some operators which will alwys return a value channel. Examples are `first`, `collect`, `count`, etc. + +We could also create a value channel using the `channel.value` factory: + +```nextflow +params.model = "${projectDir}/data/models/b32_400m.pt" + +workflow { + images = channel.fromPath("data/pics/*.{png,gif,jpg}") + | map { img -> [[id: img.baseName], img] } + + model_channel = channel.value(file(params.model)) + Classify(images, model_channel) + // rest of the workflow +} +``` + +Note here that we're wrapping the params.model value (a String) in the `file()` function, which turns an ordinary String into an object that Nextflow can use as a path. We've not needed to use this until now because the `channel.fromPath` factory necessarily returns paths, so it automatically does this conversion for us. + +An even simpler solution is to provide the path object directly when calling the process. Any non-channel object will automatically be converted into a value channel for you. + +```nextflow +params.model = "${projectDir}/data/models/b32_400m.pt" + +workflow { + images = channel.fromPath("data/pics/*.{png,gif,jpg}") + | map { img -> [[id: img.baseName], img] } + + Classify(images, file(params.model)) + // rest of the workflow +} +``` + +Now we'd like to take our channel of images and pass them each through this classification program. + +We'd like to combine each of our images with each of our models (only one at the moment). Do do these cross-product operations, we can use the `combine` operator: + +```nextflow +images +| combine(models) +| view +``` + +This will produce a channel that "combines" our images and the reference file. The channel now looks something like: + +``` +[imageMetaData, image, modelMetaData, model] +[imageMetaData, image, modelMetaData, model] +[imageMetaData, image, modelMetaData, model] +[imageMetaData, image, modelMetaData, model] +``` + +We might as well combine these two metadata Maps with a `map` operation: + +```nextflow +images +| combine(models) +| map { imgMeta, img, modelMeta, model -> [imgMeta + modelMeta, model, img]} +| view +``` + +That now gives us a channel that looks like: + +``` +[metaData, image, model] +[metaData, image, model] +[metaData, image, model] +[metaData, image, model] +``` + +Given that shape, let's create a `Classify` process: + +```nextflow +process Classify { + input: + tuple val(meta), path(img) + path(model) + output: tuple val(meta), stdout + script: "classify.py --model-path $model ${img}" +} +``` + +Now we can run this: + +```bash +nextflow run . +``` + +You might find that the process errors out with a 137 exit code. This generally means that we've run out of RAM because we're running too many of these classification jobs at the same time. Let's talk about how we tell Nextflow that a particular process requires more resources. + +## Resources + +Our processes are currently composed of the `input:`, `output:`, and `script:` blocks. In addition to these blocks, processes can use "process directives" which are optional annotations which modify the behaviour of the processes. There are many directives ([documentation](https://www.nextflow.io/docs/latest/reference/process.html#directives)), but we can introduce the concept with two important process directives - `memory` and `cpus`. + +So far, we've been using the local executor to run Nextflow - running on the local machine. There are many other executors targetting different backends, from HPC executors like SLURM and PBS to cloud executors like {AWS,GCP,Azure} Batch. There are more than a dozen supported executors ([documentation](https://www.nextflow.io/docs/latest/executor.html)). + +Each of these have a concept of the resources a particular task will require - resources such as cpus, memory, gpus, disk, etc. + +If not otherwise specified, the defaults are to request 1 cpu, 1 GB of RAM and 0 GPUs for each task. + +When using the local executor, Nextflow scans the machine it is running on and determines how many cpus and how much RAM the system has. It will ensure that (given the resources specified or defaults applied) the running tasks never exceed the available limits. If the system has 16 GB of RAM, for example, and a particular process requires 6 GB of ram, Nextflow will ensure that _at most_ 2 of those tasks are running at any one time. As a task finishes, Nextflow begins the next task in line. + +## Grouping + +Which we can now join up to our channel chain using the `join` operator, which finds pairs of items (one from each channel) that share a key. By default, the `join` operator will use the first element of each item in the channel as the key. In our case, that first item was the image metadata, which occupies the first position in both the Classify process output and the images channel. + +```nextflow +Classify.out +| join(images) +| view +``` + +which produces a channel like: + +``` +[metadata, label, img] +[metadata, label, img] +[metadata, label, img] +[metadata, label, img] +``` + +In order to make a picture of just the good cats and a second picture of just the bad cats, we'll need to group the items in the channel based on the label. We can do this with the `groupTuple` operator. Normally the groupTuple expects that the grouping key will be the first element in each item in the channel. In our case, it is the second item, i.e. index "1" if the first item is index "0". To ask Nextflow to group on the item with index 1, we add a `by: 1` argument to the operator: + +```nextflow +Classify.out +| join(images) +| groupTuple(by: 1) +| view +``` + +which produces a channenl of the form: + +``` +[metadatas, label, images] +[metadatas, label, images] +``` + +## Collage + +Let's create a `collage` process that takes this channel and produces a collage of all of the images for each label. The script block here is a little involved. + +```nextflow +process Collage { + input: tuple val(metadatas), val(label), path("inputs/*.png") + output: tuple val(label), path("collage.png") + script: + """ + magick montage inputs/* \\ + -geometry +10+10 \\ + -background black \\ + +polaroid \\ + -background '#ffbe76' \\ + collage_nolabel.png + magick montage \\ + -pointsize 48 \\ + -label '$label' \\ + -geometry +0+0 \\ + -background "#f0932b" \\ + collage_nolabel.png collage.png + """ +} +``` + +We can then hoook this into our channel chain: + +```nextflow +Classify.out +| join(images) +| groupTuple(by: 1) +| Collage +| view +``` + +Those collage tasks are taking a little too long, but that might be because we're collaging the original full sized images and not our resized images. Because the `images` channel and the output channel from the `Resize` process both have the same shape, we can simply replace them in the workflow: + +```nextflow +Classify.out +| join(Resize.out) +| groupTuple(by: 1) +| Collage +| view +``` + +For our final process, let's combine these two collages together into a single final image. We'll create a process that takes collection of images (we don't care what they are called) and produces a final `collage_all.png` image. + +```nextflow +process CombineImages { + input: path "in.*.png" + output: path "collage_all.png" + script: + """ + magick montage \\ + -geometry +10+10 \\ + -quality 05 \\ + -background '#ffbe76' \\ + -border 5 \\ + -bordercolor '#f0932b' \\ + in.*.png \\ + collage_all.png + """ +} +``` + +The channel coming from the Collage process looks like + +``` +[label, collageImage] +[label, collageImage] +``` + +but we need it to look like: + +``` +[collageImage, collageImage] +``` + +So we'll drop the labels: + +``` +Classify.out +| join(Resize.out) +| groupTuple(by: 1) +| Collage +| map { _label, img -> img } +| view +``` + +to give us a channel that looks like: + +``` +collageImage +collageImage +``` + +and then pass that to `collect` which takes all the items in a channel and then emits them as a single "wide" collection: + +``` +Classify.out +| join(Resize.out) +| groupTuple(by: 1) +| Collage +| map { _label, img -> img } +| collect +| view +``` + +We can now pass this to our new CombineImages process: + +``` +Classify.out +| join(Resize.out) +| groupTuple(by: 1) +| Collage +| map { _label, img -> img } +| collect +| CombineImages +| view +``` + +## Workflow Outputs + +Great! We have a workflow that (arguably cruely) collects our cat into "good" and "bad" groupings! Unfortunately, the final output file is still deep in this work directory in a hostile-looking hash-addressed directory. We'd like to define some final workflow outputs that should be published somewhere safe, outside of the work directory. + +To define the workflow outputs, we'll need to define a `publish:` block in the workflow. We'll also need to put the existing workflow in a `main:` block as shown below: + +```nextflow +workflow { + main: + images = channel.fromPath("data/pics/*.{png,gif,jpg}") + | map { img -> [[id: img.baseName], img] } + + Resize(images, params.width) + + Classify(images, file(params.model)) + + Classify.out + | join(Resize.out) + | groupTuple(by: 1) + | Collage + | map { _label, img -> img } + | collect + | CombineImages + + publish: + // Something to go here +} +``` + +### Publishing the Collage + +In the `publish:` block, we define channels that we'd like to publish. + +```nextflow +workflow { + main: + // workflow here + publish: + collage = CombineImages.out +} + +output { + collage {} +} +``` + +Now when we run, the final collage will be symlinked into `results/collage_all.png` + +We can control the publication mechanism my adding arguments: + +```nextflow +output { + collage { + mode 'copy' + } +} +``` + +... which will now cause Nextflow to copy the output file rather than symlink + +### Publishing the classifications + +The more intereting outputs might be those with more metadata associated with them. For example, we might want to record the classification for each image ID. To publsh metadata-rich outputs, we'll first create a channel that is composed of Maps, e.g. + +```nextflow +workflow { + main: + // workflow here + + Classify.out + | join(Resize.out) + | map { meta, label, image -> meta + [label:label, image:image] } + | set { classifiedMaps } + + publish: + collage = CombineImages.out + classification = classifiedMaps +} + +output { + collage { + mode 'copy' + } + classified { + mode 'copy' + } +} +``` + +This will cause the resized images to also be published in the `results` directory, but it's looking a bit cluttered now. + +``` +results +├── collage_all.png +├── resized-4skdDxHm4yDsSJIr.png +├── resized-4y6Hyu0uzVZcEx89.png +├── resized-6Nb0ipGrHDHqCEmZ.png +└── resized-wfMCf1lHc9YPw455.png +``` + +Let's bring a little bit of order: + +```nextflow +output { + collage { + mode 'copy' + } + classified { + mode 'copy' + path { sample -> "images/${sample.label.replaceAll(/\s+/, '_')}" } + } +} +``` + +``` +results +├── collage_all.png +└── images + ├── good_cat + │ ├── resized-4skdDxHm4yDsSJIr.png + │ └── resized-wfDuHvt6VIn2tM8T.png + └── bad_cat + ├── resized-6Nb0ipGrHDHqCEmZ.png + └── resized-wfMCf1lHc9YPw455.png +``` + +Now we sanitized the label names so that they'd be in more sensibly named directories (no spaces, etc), but this risks corrupting that metadata. Lets ask Nextflow to publish a more digestible samplesheet or "index" of the published outputs, that includes the real, unsanitized labels: + +```nextflow +output { + collage { + mode 'copy' + } + classified { + mode 'copy' + path { sample -> "images/${sample.label.replaceAll(/\s+/, '_')}" } + index { + header true + path 'images/cats.csv' + } + } +} +``` + +which produces a csv at results/images/cats.csv. + +For more structured data, you can also choose yaml or json: + +```nextflow +output { + collage { + mode 'copy' + } + classified { + mode 'copy' + path { sample -> "images/${sample.label.replaceAll(/\s+/, '_')}" } + index { + header true + path 'images/cats.json' + } + } +} +``` + +## Filesystem Independence + +Nextflow speasks many different communication protocols, allowing you to seamlessly move from using data on a local or shared filesystem, to http/https://, to object storage protocols like s3://, az://, gcp:// or even older ftp:// protocols. You can provide support for new protocols yourself via Nextflow's plugin system. + +For example, our current workflow uses the + +```bash +nextflow run . --model https://dl.fbaipublicfiles.com/MMPT/metaclip/b32_400m.pt +``` + +## Containerization + +All of our Nextflow tasks are currently using the software installed on the host operating system. This practice can quickly become a problem for you for a number of reasons: + +- As the workflow grows, and the number of software dependency stacks will also likely grow, and it becomes increasingly likely that the installation of one piece of software accidentally updates a depencency of another piece of software. Similarly, you many end up with incompatible software dependency stacks. +- The analysis becomes tied to a very specific machine or infrastructure, difficult to repdocuce in exactly the same way elsewhere (by yourself or by a colleague) +- Managing software is a thankless and boring task. + +Nextflow provides the opportunity to run each task in an isolated software environment, and can do so via a variety of technologies, including + +- conda +- containers (docker, apptainer/singularity, charliecloud, sarus, shifter, and podman) +- spack + +Let's improve the reproducibility and portability of our workflow + +TODO: Claude: include a brief (3-4 sentence) explanation of containerization (with a focus on Docker) + +You'll remember that we manually installed software two different ways: + +- imagemagick (via `apt-get install`), and +- python packages (via `uv sync`) + +We could use a single container for all of the steps in the workflow, but this might limit the reusability of the containers, and upgrading one piece of software for one task would mean changing the container for all of the tasks. Most researchers prefer (and Nextflow supports) defining container per-process. + +To replace the imagemagick we installed via apt-get, we'll use the public container 'minidocks/imagemagick:7' + +We've already talked about the `memory` and `cpus` process directives, but another useful directive is the `container` directive. We'll use this to add the container to our `Resize`, `Classify`, `Collage`, and `CombineImages` processes, e.g. + +```nextflow +process Resize { + container 'minidocks/imagemagick:7' + //... + +process Classify { + container 'minidocks/imagemagick:7' + //... + +process Collage { + container 'minidocks/imagemagick:7' + //... + +process CombineImages { + container 'minidocks/imagemagick:7' + //... ``` +Our `classify.py` process includes three specific python packages (torch, pillow, and openclip-torch) at specific versions. It's unlikely that there is an existing container that provides these specific packages. We could opt to build our own + +There are a number of ways of building containers, but we'll use the [Seqera Containers](https://seqera.io/containers/) web interface. You can add multiple packages + +![Creating a new container using Seqera Containers](./img/seqera-container-python-00.png) + +## Version Control + +TOOD: Create git repository at project root +TODO: Commit current state, create git tag, and create branch, and then change and re-commit. +TODO: Change directories and then run using revision argument, pointing to branch, tag, and then specific commmit. + +## Cloud Executors + +## Extension Exercise 1 + +Our team is interested in which cat is the custest cat and which cat is the ugliest cat. Can you extend the workflow to identify (for each label) which picture scores the highest? + +Hint 1: You can use the --json flat on the `classify.py` script +Hint 2: You can parse a json file in a closure by using the JsonSlurper class, part of the standard library. It will return a standard + +```nextflow +| map { meta, jsonFile -> new groovy.json.JsonSlurper().parseText(jsonFile.text) } +``` + +Hint 3: You can use the `min` and `max` operators to return a channel containing the minimum or maximum item, and you can pass a closure to those operators to describe how the elements in the channel should be compared ([docs](https://www.nextflow.io/docs/latest/reference/operator.html#min)) + +## Extension Exercise 2 + +We've decided that "bad" and "good" are too cruel a classification system for the cats. Can you modify the workflow to add a `--labels` parameter. The parameter should take a comma-separated list of labels and use those labels in preference to the default "good cat" and "bad cat". E.g. + +``` +nextflow run . --labels 'red cat','orange cat','black cat' +``` + +## Last TODOs + +TODO: explain difference between path(img) and path("inputs/\*.png") +TODO: Add in resouces directive memory, cpus, etc. + + From aae325dc729462b315b533d2ef532d6a0094606c Mon Sep 17 00:00:00 2001 From: Rob Syme Date: Tue, 18 Nov 2025 06:18:06 +0800 Subject: [PATCH 07/22] Better workdir explaination --- --- docs/small_nextflow/01_fundamentals.md | 92 +++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 2 deletions(-) diff --git a/docs/small_nextflow/01_fundamentals.md b/docs/small_nextflow/01_fundamentals.md index 2c608821d..48b42e91a 100644 --- a/docs/small_nextflow/01_fundamentals.md +++ b/docs/small_nextflow/01_fundamentals.md @@ -192,9 +192,97 @@ Let's explore how Nextflow executes each task in isolation. ## 3. Investigate task execution -TODO: Explain that each task is run in a separate directory. This is to ensure independence of each of the tasks - they can't interfere with each other. +Every task in Nextflow is executed in its own unique work directory. +This directory isolation is a fundamental feature that ensures tasks cannot interfere with each other, even when running in parallel. -TODO: Show the contents of one of the task work directories. +### 3.1. Understanding work directories + +The work directory path is calculated by constructing a hash of all task inputs. +This means that if you run the same task with the same inputs, Nextflow will recognize it and can reuse the cached results (we'll explore this with `-resume` later). + +Let's explore where Nextflow actually ran our tasks. +Look at the output from your last workflow run - you'll see something like: + +```console +executor > local (4) +[a0/e7b2d4] Resize (1) | 4 of 4 ✔ +``` + +That `a0/e7b2d4` is the hash prefix for the task directory. +Let's explore what's inside: + +```bash +tree work +``` + +You'll see a directory structure like: + +```console +work +└── a0 + └── e7b2d4a1f3c8e9b0a7f6d5c4b3a2e1f0 + ├── resized-5n4MTAC6ld0bVeCe.png + └── ... +``` + +### 3.2. Exploring task files + +Each work directory contains several hidden files that Nextflow uses to track task execution. +Let's see them all: + +```bash +tree -a work +``` + +Now you'll see additional files: + +```console +work +└── a0 + └── e7b2d4a1f3c8e9b0a7f6d5c4b3a2e1f0 + ├── .command.begin + ├── .command.err + ├── .command.log + ├── .command.out + ├── .command.run + ├── .command.sh + ├── .exitcode + └── resized-5n4MTAC6ld0bVeCe.png +``` + +These files serve different purposes: + +- `.command.sh` - The actual script that was executed (with all variables resolved) +- `.command.run` - The wrapper script that Nextflow uses to execute the task +- `.command.out` - Standard output from the task +- `.command.err` - Standard error from the task +- `.command.log` - Combined stdout and stderr +- `.exitcode` - The exit code from the task (0 = success) +- `.command.begin` - Setup instructions run before the task + +The most useful file for debugging is `.command.sh`. +Let's look at one: + +```bash +cat work/a0/*/command.sh +``` + +You'll see the actual bash script that was executed with all Nextflow variables resolved: + +```bash +magick 5n4MTAC6ld0bVeCe.jpg -resize 400x resized-5n4MTAC6ld0bVeCe.png +``` + +### 3.3. Task isolation and idempotence + +This isolation serves two critical purposes: + +1. **Independence**: Tasks running in parallel cannot accidentally overwrite each other's files or interfere with each other's execution +2. **Idempotence**: Running the same task with the same inputs will produce the same outputs in the same location + +**Idempotence** means that executing a task multiple times with identical inputs produces identical results. +This is crucial for reproducibility and for Nextflow's caching system. +Because the work directory is determined by hashing the inputs, identical inputs always map to the same directory, allowing Nextflow to detect when work can be reused. ### Takeaway From 5694b02bb243d580064f78527068fa9858f4df81 Mon Sep 17 00:00:00 2001 From: Rob Syme Date: Tue, 18 Nov 2025 07:53:00 +0800 Subject: [PATCH 08/22] Explain remote git workflows. --- docs/small_nextflow/04_advanced.md | 163 ++++++++++++++++++++++++++++- 1 file changed, 159 insertions(+), 4 deletions(-) diff --git a/docs/small_nextflow/04_advanced.md b/docs/small_nextflow/04_advanced.md index 9513319fa..c291ac393 100644 --- a/docs/small_nextflow/04_advanced.md +++ b/docs/small_nextflow/04_advanced.md @@ -6,15 +6,170 @@ In this final part, we'll explore version control integration, cloud execution, ## 1. Version control -TODO: Create git repository at project root +One of Nextflow's most powerful features is its deep integration with version control systems. +This allows you to share workflows, track changes, and ensure reproducibility by pinning to specific versions. -TODO: Commit current state, create git tag, and create branch, and then change and re-commit. +### 1.1. Create a GitHub repository -TODO: Change directories and then run using revision argument, pointing to branch, tag, and then specific commit. +First, let's create a new repository on GitHub to store your workflow. + +1. Go to [github.com](https://github.com) and log in +2. Click the "+" icon in the top right and select "New repository" +3. Name it something like `cat-classifier` (or any name you prefer) +4. Make it **public** (so Nextflow can access it easily) +5. **Don't** initialize with a README, .gitignore, or license +6. Click "Create repository" + +GitHub will show you some commands to push an existing repository. +Keep this page open - we'll use those commands in a moment. + +### 1.2. Initialize and push your workflow + +Now let's version control your workflow. +From your workshop directory: + +```bash +# Initialize a git repository +git init + +# Add your workflow files +git add main.nf +git add bin/ + +# If you have a nextflow.config, add that too +git add nextflow.config + +# Create your first commit +git commit -m "Initial commit of cat classifier workflow" + +# Connect to your GitHub repository (replace with your username and repo name) +git remote add origin https://github.com/YOUR-USERNAME/cat-classifier.git + +# Push to GitHub +git branch -M main +git push -u origin main +``` + +Your workflow is now on GitHub! +Visit your repository URL to see your code online. + +### 1.3. Running remote workflows + +Here's where it gets interesting: **you don't need a local copy of a workflow to run it**. + +Nextflow can pull workflows directly from GitHub and run them. +For example, to run the nf-core RNA-seq pipeline: + +```bash +nextflow run nf-core/rnaseq --help +``` + +This command pulls the workflow from `github.com/nf-core/rnaseq` (the `nf-core` organization, `rnaseq` repository), downloads it to `$HOME/.nextflow/assets/`, and runs it. + +Let's try a simpler example: + +```bash +nextflow run hello +``` + +This pulls and runs `github.com/nextflow-io/hello`. +Notice we didn't specify the full path - Nextflow uses sensible defaults: + +- If no provider is specified, it defaults to `github.com` +- If no organization is specified, it defaults to `nextflow-io` + +!!! tip "Other Git providers" + + Nextflow also supports: + + - **GitLab**: `nextflow run gitlab.com/user/repo` + - **Bitbucket**: `nextflow run bitbucket.org/user/repo` + - **Gitea**: With custom configuration + - **Azure Repos**: With custom configuration + - **AWS CodeCommit**: With custom configuration + +### 1.4. Running specific versions with revisions + +Now you can run your own workflow from anywhere: + +```bash +# Run from GitHub (replace with your username and repo name) +nextflow run YOUR-USERNAME/cat-classifier +``` + +But what about version control? +What if you want to continue developing while also maintaining a stable version? + +Nextflow allows you to specify a **revision** - a specific branch, tag, or commit: + +```bash +# Run a specific branch +nextflow run YOUR-USERNAME/cat-classifier -revision dev-branch + +# Or use the short form +nextflow run YOUR-USERNAME/cat-classifier -r dev-branch +``` + +### 1.5. Using Git tags for stable versions + +**Git tags** are named references to specific commits, typically used to mark release versions. +They're like bookmarks in your repository's history - they don't change, making them perfect for reproducible pipelines. + +Let's create a `1.0` tag for your workflow: + +```bash +# Create an annotated tag +git tag -a 1.0 -m "First stable release of cat classifier" + +# Push the tag to GitHub +git push origin 1.0 +``` + +Now you can run this exact version forever: + +```bash +nextflow run YOUR-USERNAME/cat-classifier -r 1.0 +``` + +This will always run the code as it existed when you created the tag, even if you continue developing on the `main` branch. + +### 1.6. Testing with different revisions + +Let's see this in action. +Create a new branch and make a change: + +```bash +# Create and switch to a new branch +git checkout -b experimental + +# Make a small change (e.g., modify a parameter default in main.nf) +# Then commit it +git add main.nf +git commit -m "Experimental feature" +git push origin experimental +``` + +Now you can run different versions: + +```bash +# Run the stable 1.0 release +nextflow run YOUR-USERNAME/cat-classifier -r 1.0 + +# Run the main branch +nextflow run YOUR-USERNAME/cat-classifier -r main + +# Run the experimental branch +nextflow run YOUR-USERNAME/cat-classifier -r experimental + +# Run a specific commit (use any commit hash from git log) +nextflow run YOUR-USERNAME/cat-classifier -r abc123def +``` + +This is incredibly powerful: you can have a stable, reproducible pipeline (using a tag) while actively developing new features (on branches), all from the same repository. ### Takeaway -Nextflow's git integration allows you to version your workflows and run specific commits, branches, or tags from anywhere. +Nextflow's git integration allows you to version your workflows, share them easily, and run specific commits, branches, or tags from anywhere - no local copy required. ### What's next? From 362106de786e2e076ae3131739fb380dd5f621b6 Mon Sep 17 00:00:00 2001 From: Rob Syme Date: Tue, 18 Nov 2025 08:07:06 +0800 Subject: [PATCH 09/22] Adjust open in codespaces link --- docs/small_nextflow/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/small_nextflow/index.md b/docs/small_nextflow/index.md index 1e098434b..287107a81 100644 --- a/docs/small_nextflow/index.md +++ b/docs/small_nextflow/index.md @@ -15,7 +15,7 @@ By starting with an empty directory and building up piece by piece, you'll gain Let's get started! -[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/nextflow-io/training?quickstart=1&ref=master) +[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/nextflow-io/training/tree/smol-nextflow?quickstart=1&ref=master) ## Learning objectives From 07d5bccdae263d073e96b35c99ddd75ded2ad8d6 Mon Sep 17 00:00:00 2001 From: Rob Syme Date: Tue, 18 Nov 2025 08:17:55 +0800 Subject: [PATCH 10/22] Add scaling step --- docs/small_nextflow/00_orientation.md | 2 +- docs/small_nextflow/02_data_transformation.md | 45 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/docs/small_nextflow/00_orientation.md b/docs/small_nextflow/00_orientation.md index c35b4e7f3..771040c86 100644 --- a/docs/small_nextflow/00_orientation.md +++ b/docs/small_nextflow/00_orientation.md @@ -3,7 +3,7 @@ This orientation assumes you have already opened the training environment by clicking on the "Open in GitHub Codespaces" button. If not, please do so now, ideally in a second browser window or tab so you can refer back to these instructions. -[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/nextflow-io/training?quickstart=1&ref=master) +[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/nextflow-io/training/tree/smol-nextflow?quickstart=1&ref=master) ## GitHub Codespaces diff --git a/docs/small_nextflow/02_data_transformation.md b/docs/small_nextflow/02_data_transformation.md index 4958f3e9d..a001c71f6 100644 --- a/docs/small_nextflow/02_data_transformation.md +++ b/docs/small_nextflow/02_data_transformation.md @@ -461,9 +461,54 @@ Run the complete workflow: nextflow run main.nf ``` +### 10.6. Scaling up without code changes + +One of Nextflow's key strengths is automatic scalability. +Let's see this in action by adding more data to our analysis! + +While your workflow is still running (or right after it completes), open a new terminal and add more cat images: + +```bash +# Add 20 more cats to our dataset +.stuff/cat_me.sh --count 20 --prefix data/pics +``` + +This brings our total from 4 cats to 24 cats. +Now run the workflow again with `-resume`: + +```bash +nextflow run main.nf -resume +``` + +Notice what happens in the output: + +- Tasks for the original 4 images show as **[cached]** in gray +- Only the 20 new images are processed through Resize and Classify +- The groupTuple, Collage, and CombineImages steps run again (because their inputs changed) +- The final collage now includes all 24 cats + +**You didn't change a single line of code** - the workflow automatically: + +- Detected the new input files via the glob pattern `data/pics/*.{png,gif,jpg}` +- Processed only the new images that hadn't been seen before +- Reused cached results for the original 4 images +- Scaled the grouping and collage operations to handle more data + +This is the power of Nextflow's declarative approach: you describe **what** you want to do, and Nextflow figures out **how** to do it efficiently, whether you have 4 files or 4,000 files. + +!!! tip "Scalability in practice" + + This same pattern works at any scale: + + - **Local development**: Test with 4 samples + - **Pilot study**: Scale to 24 samples with no code changes + - **Production**: Process thousands of samples with the same workflow + - **HPC/Cloud**: Nextflow automatically distributes tasks across available resources + ### Takeaway You can chain together multiple processes and operators to build sophisticated multi-step workflows that transform and aggregate data. +Nextflow automatically scales your workflow as your data grows, without requiring any code changes. ### What's next? From 23529f15aed67855f4e26b32755efeabc4035ebf Mon Sep 17 00:00:00 2001 From: Rob Syme Date: Tue, 18 Nov 2025 08:20:23 +0800 Subject: [PATCH 11/22] Clearer orientation --- docs/small_nextflow/00_orientation.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/small_nextflow/00_orientation.md b/docs/small_nextflow/00_orientation.md index 771040c86..a7dbc3c39 100644 --- a/docs/small_nextflow/00_orientation.md +++ b/docs/small_nextflow/00_orientation.md @@ -48,10 +48,9 @@ If you run this inside `small_nextflow`, you should see a minimal directory stru ```console title="Directory contents" . ├── .stuff/ -│ ├── cat_me.sh -│ ├── classify.py -│ └── pyproject.toml -└── main.nf + ├── cat_me.sh + ├── classify.py + └── pyproject.toml ``` **Here's a summary of what you should know to get started:** @@ -59,9 +58,6 @@ If you run this inside `small_nextflow`, you should see a minimal directory stru - **The `.stuff/` directory** contains helper scripts and configuration files we'll use throughout the workshop. You can think of this as a toolbox we'll pull from as we build our workflow. -- **The file `main.nf`** is where we'll write our Nextflow workflow. - It starts nearly empty, and we'll build it up step by step. - - **The `cat_me.sh` script** fetches random cat images from an API for our workflow to process. - **The `classify.py` script** is a Python program that uses machine learning to classify images. @@ -70,4 +66,10 @@ If you run this inside `small_nextflow`, you should see a minimal directory stru Throughout this workshop, we'll start with this minimal setup and progressively build a complete image classification workflow. +Let's get started by creating a fresh, empty `main.nf`: + +```bash +code main.nf +``` + **Now, to begin the course, click on the arrow in the bottom right corner of this page.** From 4d49d64878caf52fad5773665e608056a6875725 Mon Sep 17 00:00:00 2001 From: Rob Syme Date: Tue, 18 Nov 2025 08:24:22 +0800 Subject: [PATCH 12/22] Trying removing heading numbering --- docs/small_nextflow/02_data_transformation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/small_nextflow/02_data_transformation.md b/docs/small_nextflow/02_data_transformation.md index a001c71f6..91251fa73 100644 --- a/docs/small_nextflow/02_data_transformation.md +++ b/docs/small_nextflow/02_data_transformation.md @@ -4,14 +4,14 @@ In this part, we'll build a multi-step workflow that classifies images using mac --- -## 1. Classification +## Classification Let's get to the fun part - the cat sorting! We have a little classification script - `classify.py` that I've provided in the `.stuff` directory. In your research sometimes you have small accessory scripts that are useful for your pipelines. We're using a python script here in this workshop example, but this pattern will hold for scripts written in perl, ruby, R, python, closurescript, or any of the other interpreted languages. -### 7.1. Set up the classification script +### Set up the classification script Let's pull the file out into a new `bin` directory: From ae22c46deeae3932b160eb65354577163be36f53 Mon Sep 17 00:00:00 2001 From: Rob Syme Date: Tue, 18 Nov 2025 08:37:31 +0800 Subject: [PATCH 13/22] Fix numbering --- docs/small_nextflow/01_fundamentals.md | 48 +++++++++---------- docs/small_nextflow/02_data_transformation.md | 38 +++++++-------- .../03_publishing_portability.md | 30 ++++++------ docs/small_nextflow/04_advanced.md | 44 ++++++++--------- 4 files changed, 80 insertions(+), 80 deletions(-) diff --git a/docs/small_nextflow/01_fundamentals.md b/docs/small_nextflow/01_fundamentals.md index 48b42e91a..ae9821753 100644 --- a/docs/small_nextflow/01_fundamentals.md +++ b/docs/small_nextflow/01_fundamentals.md @@ -5,7 +5,7 @@ We're going to build a workflow that produces a gallery of classified cat images --- -## 1. Introduction +## Introduction Nextflow offers you a way to iterate over a collection of files, so let's grab some files to iterate over. We're going to write a workflow which produces a gallery of good and bad cats. @@ -14,7 +14,7 @@ First things we're going to need are some cats. We're starting with a (nearly) empty directory. There is a `.stuff` directory that contains some bits and pieces to help us along during the workshop, but you can imagine that we're essentially starting from scratch. -### 1.1. Fetch some cat images +### Fetch some cat images The first thing we're going to need is some data. I've created a small script that pulls from the Cat-As-A-Service API to give us some random cats. @@ -48,7 +48,7 @@ data └── uq5KqqiF0qpgTQVA.txt ``` -### 1.2. Create a channel of images +### Create a channel of images Now let's iterate over those images in Nextflow. To start, we'll just create a channel of those images. @@ -90,7 +90,7 @@ Let's actually do something with those images by creating our first process. --- -## 2. Channels and processes +## Channels and processes Let's try actually doing something with those images. We'll start with something simple - resizing images. @@ -102,7 +102,7 @@ sudo apt update sudo apt-get install -y imagemagick ``` -### 2.1. Understanding process structure +### Understanding process structure We'll create a new "process" for our resize operation. You can think of these processes as templates, or classes. @@ -111,7 +111,7 @@ We'll connect the process to a channel and fire off a new "task" for each thing In our Resize process definition (indeed in most process definitions), there are three blocks - `input`, `output`, and `script`. We connect these processes by channels and these blocks describe what we expect to get, what we expect to emit, and the work we want to do in-between. -### 2.2. Create the Resize process +### Create the Resize process Update your `main.nf` to add a Resize process: @@ -133,7 +133,7 @@ process Resize { } ``` -### 2.3. Understanding the input block +### Understanding the input block The input block describes what we expect to take from the input channel. The "things" in the channel can have a type. @@ -145,13 +145,13 @@ The most common "types" are: We'll see more of those later, but for the moment, the channel we created in the `workflow` block is a channel of files, so in our process definition, we'll say "I'm going to supply a channel of paths to this process, and as our process takes things from that channel to spawn a new task, we'll call the thing in the channel `img`." -### 2.4. Understanding the output block +### Understanding the output block The output block describes what we want to emit into the process' output channel. Again, we can describe the "type" of thing emitted - `val`, `path`, `tuple` and others. For now, we'll promise to produce a file (or directory) that matches the glob pattern `resized-*`. -### 2.5. Understanding the script block +### Understanding the script block The script block describes what work we want to do on each of the things in the channel - how we're going to transform each of the things we pull from the input channel into the files or values we promised to emit into the output channel. @@ -171,7 +171,7 @@ For example, if the "thing" in the channel is the image `kitten.jpg`, then when magick kitten.jpg -resize 400x resized-kitten.jpg ``` -### 2.6. Run the workflow +### Run the workflow Now let's run our workflow! We'll iterate over all of the images in `data/pics` (relative to our current location) and produce a channel of resized pictures that we then pipe into the `view` operator to print the channel contents to stdout. @@ -190,12 +190,12 @@ Let's explore how Nextflow executes each task in isolation. --- -## 3. Investigate task execution +## Investigate task execution Every task in Nextflow is executed in its own unique work directory. This directory isolation is a fundamental feature that ensures tasks cannot interfere with each other, even when running in parallel. -### 3.1. Understanding work directories +### Understanding work directories The work directory path is calculated by constructing a hash of all task inputs. This means that if you run the same task with the same inputs, Nextflow will recognize it and can reuse the cached results (we'll explore this with `-resume` later). @@ -225,7 +225,7 @@ work └── ... ``` -### 3.2. Exploring task files +### Exploring task files Each work directory contains several hidden files that Nextflow uses to track task execution. Let's see them all: @@ -273,7 +273,7 @@ You'll see the actual bash script that was executed with all Nextflow variables magick 5n4MTAC6ld0bVeCe.jpg -resize 400x resized-5n4MTAC6ld0bVeCe.png ``` -### 3.3. Task isolation and idempotence +### Task isolation and idempotence This isolation serves two critical purposes: @@ -294,7 +294,7 @@ Let's learn some convenient methods for working with file paths. --- -## 4. Harmonization +## Harmonization One of the nice features of the `magick` utility is that it will also do file format conversion for us. It will infer the format from the extension of the final argument. @@ -308,7 +308,7 @@ The `magick` utility will both resize the image and convert the jpg to png forma Let's say we want to ensure that downstream in our workflow, we'd like to ensure all images are in the png format. How might we modify our `script` block to replace the extension or pull out the file basename so that we can append the `.png` extension? -### 4.1. The bash way (harder) +### The bash way (harder) If you're a bash wizard, you might know that if you have a variable `$myFile` with the path to our file, you can replace the extension with this arcane incantation: @@ -325,7 +325,7 @@ magick "$file" -resize 400x "$(basename "$file" .${file##*.}).png" I love bash, but it's easy to forget this syntax or mistype it. -### 4.2. The Nextflow way (easier) +### The Nextflow way (easier) Fortunately for us, when inside the script block the `img` variable is not a bash variable - it's a Nextflow variable, and Nextflow provides some convenience methods for operating on those path objects. The full list is available in the [Nextflow stdlib documentation](https://www.nextflow.io/docs/latest/reference/stdlib-types.html#stdlib-types-path), but one handy method is `baseName`. @@ -367,12 +367,12 @@ Let's make our workflow more flexible by adding parameters. --- -## 5. Parameters +## Parameters What if we want to make our workflow a little more flexible? Let's pull out the width and expose it as a parameter to the user. -### 5.1. Using parameters directly in the script +### Using parameters directly in the script We could reference a parameter directly in the script block: @@ -400,7 +400,7 @@ Now we can run with: nextflow run main.nf --width 300 ``` -### 5.2. Making inputs explicit (best practice) +### Making inputs explicit (best practice) This works, but it's considered best practice (and we'll see why in a bit) to make the inputs to a process explicit. We can do this by adding a second input channel: @@ -440,11 +440,11 @@ Let's learn how to attach metadata to our files using tuples and maps. --- -## 6. Extracting an ID +## Extracting an ID Great, but I'd like a way of retaining the original IDs. -### 6.1. Understanding the map operator +### Understanding the map operator The `map` operator is one of the most powerful tools in Nextflow. It takes a collection of items in a channel and transforms them into a new collection of items. @@ -471,7 +471,7 @@ Run this to see the structure: nextflow run main.nf ``` -### 6.2. Using a metadata map +### Using a metadata map Better - we have the id extracted as a String. What if we want to add other metadata later? @@ -487,7 +487,7 @@ workflow { } ``` -### 6.3. Update the process to handle tuples +### Update the process to handle tuples Now we've changed the "shape" of the items in the channel, so we'll update the downstream process: diff --git a/docs/small_nextflow/02_data_transformation.md b/docs/small_nextflow/02_data_transformation.md index 91251fa73..2f43b4e67 100644 --- a/docs/small_nextflow/02_data_transformation.md +++ b/docs/small_nextflow/02_data_transformation.md @@ -59,7 +59,7 @@ options: Model architecture (auto-detected from filename if not specified) ``` -### 7.2. Download the classification model +### Download the classification model The script takes images, a model, and a set of labels and classifies each of the images according to the labels. To run the script outside of Nextflow, we'll need to download one of the models. @@ -70,7 +70,7 @@ mkdir -p data/models (cd data/models && wget https://dl.fbaipublicfiles.com/MMPT/metaclip/b32_400m.pt) ``` -### 7.3. Create the Classify process +### Create the Classify process Now let's create a `Classify` process that will take two channels - one channel of images and one channel that supplies the model: @@ -88,7 +88,7 @@ Note here that we're calling the `classify.py` script directly, even though we c This is because Nextflow automatically adds the `bin` directory (relative to the main.nf) to the `$PATH` for all Nextflow tasks. This is a very convenient way to bundle accessory scripts and snippets with your workflow. -### 7.4. Understanding queue vs. value channels +### Understanding queue vs. value channels Processes can have multiple channels as input or as output. A process will continue to emit tasks as long as it can pull an item from each of the input channels. @@ -117,7 +117,7 @@ Queue channels are exhaustible - they have a set number of items in the channel The second type of channel is a value channel, which is a channel of only a single item. This item is emitted without exhaustion. -### 7.5. Using value channels +### Using value channels There are some operators which will always return a value channel. Examples are `first`, `collect`, `count`, etc. @@ -139,7 +139,7 @@ workflow { Note here that we're wrapping the params.model value (a String) in the `file()` function, which turns an ordinary String into an object that Nextflow can use as a path. We've not needed to use this until now because the `channel.fromPath` factory necessarily returns paths, so it automatically does this conversion for us. -### 7.6. Implicit value channels +### Implicit value channels An even simpler solution is to provide the path object directly when calling the process. Any non-channel object will automatically be converted into a value channel for you: @@ -180,13 +180,13 @@ Let's learn how to manage computational resources for our processes. --- -## 2. Resources +## Resources Our processes are currently composed of the `input:`, `output:`, and `script:` blocks. In addition to these blocks, processes can use "process directives" which are optional annotations which modify the behaviour of the processes. There are many directives ([documentation](https://www.nextflow.io/docs/latest/reference/process.html#directives)), but we can introduce the concept with two important process directives - `memory` and `cpus`. -### 8.1. Understanding executors +### Understanding executors So far, we've been using the local executor to run Nextflow - running on the local machine. There are many other executors targeting different backends, from HPC executors like SLURM and PBS to cloud executors like AWS Batch, Google Batch, and Azure Batch. @@ -194,7 +194,7 @@ There are more than a dozen supported executors ([documentation](https://www.nex Each of these have a concept of the resources a particular task will require - resources such as cpus, memory, gpus, disk, etc. -### 8.2. Resource defaults and management +### Resource defaults and management If not otherwise specified, the defaults are to request 1 cpu, 1 GB of RAM and 0 GPUs for each task. @@ -203,7 +203,7 @@ It will ensure that (given the resources specified or defaults applied) the runn If the system has 16 GB of RAM, for example, and a particular process requires 6 GB of ram, Nextflow will ensure that _at most_ 2 of those tasks are running at any one time. As a task finishes, Nextflow begins the next task in line. -### 8.3. Add resource directives +### Add resource directives Update your Classify process to request more memory: @@ -235,14 +235,14 @@ Let's learn how to combine related data using the join and groupTuple operators. --- -## 3. Grouping +## Grouping Now we want to combine our classification results with our resized images. We can use the `join` operator, which finds pairs of items (one from each channel) that share a key. By default, the `join` operator will use the first element of each item in the channel as the key. In our case, that first item was the image metadata, which occupies the first position in both the Classify process output and the Resize process output. -### 9.1. Join classification results with images +### Join classification results with images Update your workflow to join the channels: @@ -272,7 +272,7 @@ This produces a channel like: [metadata, label, img] ``` -### 9.2. Group items by label +### Group items by label In order to make a picture of just the good cats and a second picture of just the bad cats, we'll need to group the items in the channel based on the label. We can do this with the `groupTuple` operator. @@ -315,12 +315,12 @@ Let's create visual collages for each group of classified images. --- -## 4. Collage +## Collage Let's create a `Collage` process that takes this channel and produces a collage of all of the images for each label. The script block here is a little involved, but it uses ImageMagick's montage command to arrange images into a grid. -### 10.1. Create the Collage process +### Create the Collage process ```groovy title="Collage process" linenums="1" process Collage { @@ -344,7 +344,7 @@ process Collage { } ``` -### 10.2. Connect to the workflow +### Connect to the workflow We can then hook this into our channel chain: @@ -367,7 +367,7 @@ workflow { } ``` -### 10.3. Optimize with resized images +### Optimize with resized images Those collage tasks are taking a little too long, but that might be because we're collaging the original full-sized images and not our resized images. Because the `images` channel and the output channel from the `Resize` process both have the same shape, we can simply replace them in the workflow: @@ -391,7 +391,7 @@ workflow { } ``` -### 10.4. Combine all collages +### Combine all collages For our final process, let's combine these two collages together into a single final image. We'll create a process that takes a collection of images (we don't care what they are called) and produces a final `collage_all.png` image: @@ -414,7 +414,7 @@ process CombineImages { } ``` -### 10.5. Transform the channel +### Transform the channel The channel coming from the Collage process looks like: @@ -461,7 +461,7 @@ Run the complete workflow: nextflow run main.nf ``` -### 10.6. Scaling up without code changes +### Scaling up without code changes One of Nextflow's key strengths is automatic scalability. Let's see this in action by adding more data to our analysis! diff --git a/docs/small_nextflow/03_publishing_portability.md b/docs/small_nextflow/03_publishing_portability.md index 0eafe84c7..060be8304 100644 --- a/docs/small_nextflow/03_publishing_portability.md +++ b/docs/small_nextflow/03_publishing_portability.md @@ -4,13 +4,13 @@ In this part, we'll make our workflow production-ready by publishing organized o --- -## 1. Workflow outputs +## Workflow outputs Great! We have a workflow that (arguably cruelly) collects our cats into "good" and "bad" groupings! Unfortunately, the final output file is still deep in the work directory in a hostile-looking hash-addressed directory. We'd like to define some final workflow outputs that should be published somewhere safe, outside of the work directory. -### 11.1. Understanding workflow output blocks +### Understanding workflow output blocks To define the workflow outputs, we'll need to define a `publish:` block in the workflow. We'll also need to put the existing workflow in a `main:` block as shown below: @@ -38,7 +38,7 @@ workflow { } ``` -### 11.2. Publishing the collage +### Publishing the collage In the `publish:` block, we define channels that we'd like to publish: @@ -76,7 +76,7 @@ Now when we run, the final collage will be copied into `results/collage_all.png` We can control the publication mechanism by adding arguments. The `mode 'copy'` directive tells Nextflow to copy the output file rather than create a symlink (the default). -### 11.3. Publishing the classifications +### Publishing the classifications The more interesting outputs might be those with more metadata associated with them. For example, we might want to record the classification for each image ID. @@ -131,7 +131,7 @@ results └── resized-wfMCf1lHc9YPw455.png ``` -### 11.4. Organizing outputs with path directives +### Organizing outputs with path directives Let's bring a little bit of order by organizing images into subdirectories by their label: @@ -161,7 +161,7 @@ results └── resized-wfMCf1lHc9YPw455.png ``` -### 11.5. Creating index files +### Creating index files Now we sanitized the label names so that they'd be in more sensibly named directories (no spaces, etc), but this risks corrupting that metadata. Let's ask Nextflow to publish a more digestible samplesheet or "index" of the published outputs, that includes the real, unsanitized labels: @@ -218,12 +218,12 @@ Let's make our workflow portable across different storage systems. --- -## 2. Filesystem independence +## Filesystem independence Nextflow speaks many different communication protocols, allowing you to seamlessly move from using data on a local or shared filesystem, to `http://`/`https://`, to object storage protocols like `s3://`, `az://`, `gs://` or even older `ftp://` protocols. You can provide support for new protocols yourself via Nextflow's plugin system. -### 12.1. Using remote files +### Using remote files For example, our current workflow uses a local model file. But we can easily switch to using a remote model from the web: @@ -235,7 +235,7 @@ nextflow run main.nf --model https://dl.fbaipublicfiles.com/MMPT/metaclip/b32_40 Nextflow will automatically download the file and make it available to the process. This works for input files too - you could provide image URLs instead of local paths! -### 12.2. Cloud storage support +### Cloud storage support Similarly, if you're working in the cloud, you can use cloud storage URLs: @@ -265,7 +265,7 @@ Let's containerize our workflow to ensure it runs reliably anywhere. --- -## 3. Containerization +## Containerization All of our Nextflow tasks are currently using the software installed on the host operating system. This practice can quickly become a problem for you for a number of reasons: @@ -274,7 +274,7 @@ This practice can quickly become a problem for you for a number of reasons: - The analysis becomes tied to a very specific machine or infrastructure, difficult to reproduce in exactly the same way elsewhere (by yourself or by a colleague). - Managing software is a thankless and boring task. -### 13.1. Understanding containerization +### Understanding containerization Containers are lightweight, standalone packages that include everything needed to run a piece of software: code, runtime, system tools, and libraries. Docker is the most popular container technology, and it works by packaging your software and dependencies into an "image" that can run consistently anywhere Docker is installed. @@ -282,7 +282,7 @@ Docker is the most popular container technology, and it works by packaging your When you run a containerized task, Docker creates an isolated environment with exactly the software versions specified in the container image, completely independent of what's installed on the host system. This ensures that your workflow produces identical results whether you run it on your laptop, an HPC cluster, or in the cloud. -### 13.2. Container technologies in Nextflow +### Container technologies in Nextflow Nextflow provides the opportunity to run each task in an isolated software environment, and can do so via a variety of technologies, including: @@ -292,7 +292,7 @@ Nextflow provides the opportunity to run each task in an isolated software envir Let's improve the reproducibility and portability of our workflow. -### 13.3. Containerizing processes +### Containerizing processes You'll remember that we manually installed software two different ways: @@ -329,7 +329,7 @@ process CombineImages { } ``` -### 13.4. Building custom containers +### Building custom containers Our `classify.py` process includes three specific python packages (torch, pillow, and openclip-torch) at specific versions. It's unlikely that there is an existing container that provides these specific packages. @@ -355,7 +355,7 @@ process Classify { } ``` -### 13.5. Enable container execution +### Enable container execution To actually use containers, you need to enable Docker (or another container engine) in your Nextflow configuration. Create or update `nextflow.config`: diff --git a/docs/small_nextflow/04_advanced.md b/docs/small_nextflow/04_advanced.md index c291ac393..6deff7988 100644 --- a/docs/small_nextflow/04_advanced.md +++ b/docs/small_nextflow/04_advanced.md @@ -4,12 +4,12 @@ In this final part, we'll explore version control integration, cloud execution, --- -## 1. Version control +## Version control One of Nextflow's most powerful features is its deep integration with version control systems. This allows you to share workflows, track changes, and ensure reproducibility by pinning to specific versions. -### 1.1. Create a GitHub repository +### Create a GitHub repository First, let's create a new repository on GitHub to store your workflow. @@ -23,7 +23,7 @@ First, let's create a new repository on GitHub to store your workflow. GitHub will show you some commands to push an existing repository. Keep this page open - we'll use those commands in a moment. -### 1.2. Initialize and push your workflow +### Initialize and push your workflow Now let's version control your workflow. From your workshop directory: @@ -53,7 +53,7 @@ git push -u origin main Your workflow is now on GitHub! Visit your repository URL to see your code online. -### 1.3. Running remote workflows +### Running remote workflows Here's where it gets interesting: **you don't need a local copy of a workflow to run it**. @@ -88,7 +88,7 @@ Notice we didn't specify the full path - Nextflow uses sensible defaults: - **Azure Repos**: With custom configuration - **AWS CodeCommit**: With custom configuration -### 1.4. Running specific versions with revisions +### Running specific versions with revisions Now you can run your own workflow from anywhere: @@ -110,7 +110,7 @@ nextflow run YOUR-USERNAME/cat-classifier -revision dev-branch nextflow run YOUR-USERNAME/cat-classifier -r dev-branch ``` -### 1.5. Using Git tags for stable versions +### Using Git tags for stable versions **Git tags** are named references to specific commits, typically used to mark release versions. They're like bookmarks in your repository's history - they don't change, making them perfect for reproducible pipelines. @@ -133,7 +133,7 @@ nextflow run YOUR-USERNAME/cat-classifier -r 1.0 This will always run the code as it existed when you created the tag, even if you continue developing on the `main` branch. -### 1.6. Testing with different revisions +### Testing with different revisions Let's see this in action. Create a new branch and make a change: @@ -177,11 +177,11 @@ Let's explore running workflows on cloud infrastructure. --- -## 2. Cloud executors +## Cloud executors Nextflow supports running workflows on cloud infrastructure through various executors including AWS Batch, Google Cloud Batch, and Azure Batch. -### 15.1. Benefits of cloud execution +### Benefits of cloud execution Running workflows in the cloud offers several advantages: @@ -190,7 +190,7 @@ Running workflows in the cloud offers several advantages: - **No local infrastructure**: No need to maintain local HPC clusters - **Global accessibility**: Run workflows from anywhere with internet access -### 15.2. Cloud executor configuration +### Cloud executor configuration Each cloud provider has its own executor configuration. Here's a basic example for AWS Batch: @@ -210,7 +210,7 @@ aws { } ``` -### 15.3. Data staging with cloud storage +### Data staging with cloud storage When running in the cloud, you'll typically stage your data in cloud storage: @@ -236,12 +236,12 @@ Try the extension exercises to practice your new skills! --- -## 3. Extension Exercise 1: Find extreme scores +## Extension Exercise 1: Find extreme scores Our team is interested in which cat is the cutest cat and which cat is the ugliest cat. Can you extend the workflow to identify (for each label) which picture scores the highest? -### 16.1. Hints +### Hints **Hint 1:** You can use the `--json` flag on the `classify.py` script to output structured data instead of plain text. @@ -253,7 +253,7 @@ Can you extend the workflow to identify (for each label) which picture scores th **Hint 3:** You can use the `min` and `max` operators to return a channel containing the minimum or maximum item, and you can pass a closure to those operators to describe how the elements in the channel should be compared ([docs](https://www.nextflow.io/docs/latest/reference/operator.html#min)). -### 16.2. Exercise goals +### Exercise goals - Modify the Classify process to output JSON - Parse the JSON to extract scores @@ -270,19 +270,19 @@ Try making the classification labels configurable! --- -## 4. Extension Exercise 2: Configurable labels +## Extension Exercise 2: Configurable labels We've decided that "bad" and "good" are too cruel a classification system for the cats. Can you modify the workflow to add a `--labels` parameter? The parameter should take a comma-separated list of labels and use those labels in preference to the default "good cat" and "bad cat". -### 17.1. Example usage +### Example usage ```bash nextflow run main.nf --labels 'red cat','orange cat','black cat' ``` -### 17.2. Hints +### Hints **Hint 1:** You'll need to modify how the labels are passed to the `classify.py` script. @@ -299,7 +299,7 @@ params.labels = 'good cat,bad cat' labels = params.labels.split(',') ``` -### 17.3. Exercise goals +### Exercise goals - Add a `--labels` parameter to the workflow - Parse the comma-separated labels @@ -316,15 +316,15 @@ Congratulations! You've completed the Small Nextflow workshop. --- -## 5. Final notes +## Final notes -### 18.1. Additional topics to explore +### Additional topics to explore TODO: explain difference between `path(img)` and `path("inputs/*.png")` TODO: Add in resources directive memory, cpus, etc. (note: this is partially covered in section 8) -### 18.2. What you've learned +### What you've learned Congratulations on completing the Small Nextflow workshop! You've built a complete image classification workflow from scratch and learned: @@ -335,7 +335,7 @@ You've built a complete image classification workflow from scratch and learned: - **Portability**: Filesystem independence and containerization - **Advanced patterns**: Version control and cloud execution -### 18.3. Where to go from here +### Where to go from here Now that you understand the basics, you can: From b2e043f4b6742057cb88b90b7b454360aa8a871c Mon Sep 17 00:00:00 2001 From: Rob Syme Date: Tue, 18 Nov 2025 09:00:52 +0800 Subject: [PATCH 14/22] Add correct container url --- docs/small_nextflow/03_publishing_portability.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/small_nextflow/03_publishing_portability.md b/docs/small_nextflow/03_publishing_portability.md index 060be8304..da0712059 100644 --- a/docs/small_nextflow/03_publishing_portability.md +++ b/docs/small_nextflow/03_publishing_portability.md @@ -344,8 +344,8 @@ Once you have your container image, add it to the Classify process: ```groovy title="Classify with custom container" hl_lines="2" linenums="1" process Classify { - container 'your-registry/your-classify-container:latest' - memory '4 GB' + container 'community.wave.seqera.io/library/pip_open-clip-torch_pillow_torch:83edd876e95b9a8e' + memory '13 GB' input: tuple val(meta), path(img) From 18665abc15e2226a97ae1bebb08b3377f101c313 Mon Sep 17 00:00:00 2001 From: Rob Syme Date: Tue, 18 Nov 2025 15:21:49 +0800 Subject: [PATCH 15/22] remove redundant side-quest --- side-quests/smol-nextflow/bin/cat_me.sh | 114 --- side-quests/smol-nextflow/bin/download.sh | 47 - .../smol-nextflow/bin/inspect_model.py | 77 -- side-quests/smol-nextflow/main.nf | 122 --- side-quests/smol-nextflow/outline.md | 858 ------------------ 5 files changed, 1218 deletions(-) delete mode 100755 side-quests/smol-nextflow/bin/cat_me.sh delete mode 100755 side-quests/smol-nextflow/bin/download.sh delete mode 100755 side-quests/smol-nextflow/bin/inspect_model.py delete mode 100644 side-quests/smol-nextflow/main.nf delete mode 100644 side-quests/smol-nextflow/outline.md diff --git a/side-quests/smol-nextflow/bin/cat_me.sh b/side-quests/smol-nextflow/bin/cat_me.sh deleted file mode 100755 index fd2ef1247..000000000 --- a/side-quests/smol-nextflow/bin/cat_me.sh +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -# Show help message -show_help() { - cat << EOF -Usage: $0 [-n|--count NUM] [-p|--prefix PATH] [-h|--help] - -Download random cat images from cataas.com along with their tags. - -Options: - -c, --count NUM Number of cats to download (default: 1) - -p, --prefix PATH Directory to download files to (default: current directory) - -h, --help Show this help message and exit - -Examples: - $0 # Download 1 cat to current directory - $0 -n 5 # Download 5 cats to current directory - $0 -p cats -n 10 # Download 10 cats to ./cats directory - $0 --prefix data/cats # Download 1 cat to ./data/cats directory - -Output: - For each cat, creates two files: - - .jpg The cat image - - .txt The tags (one per line) - -EOF -} - -# Default values -num_downloads=1 -prefix="." - -# Parse arguments -while [[ $# -gt 0 ]]; do - case $1 in - -h|--help) - show_help - exit 0 - ;; - -c|--count) - num_downloads="$2" - shift 2 - ;; - -p|--prefix) - prefix="$2" - shift 2 - ;; - *) - echo "Unknown option: $1" - echo "Use --help for usage information" - exit 1 - ;; - esac -done - -# Validate number -if ! [[ "$num_downloads" =~ ^[0-9]+$ ]] || [ "$num_downloads" -lt 1 ]; then - echo "Error: Number must be a positive integer" - exit 1 -fi - -# Create output directory if it doesn't exist -mkdir -p "$prefix" -echo "Downloading $num_downloads cat(s) to $prefix/" -echo "" - -# Download loop -for i in $(seq 1 "$num_downloads"); do - echo "[$i/$num_downloads]" - - # Get the JSON metadata - json=$(curl -s 'https://cataas.com/cat?json=true') - - # Extract fields using jq - cat_id=$(echo "$json" | jq -r '.id') - mimetype=$(echo "$json" | jq -r '.mimetype') - url=$(echo "$json" | jq -r '.url') - tags=$(echo "$json" | jq -r '.tags[]') # Extract tags, one per line - - # Map mimetype to extension - only accept jpg, png, gif - case "$mimetype" in - image/jpeg|image/jpg) - ext="jpg" - ;; - image/png) - ext="png" - ;; - image/gif) - ext="gif" - ;; - *) - echo "✗ Skipping unsupported type: $mimetype" - echo "" - continue - ;; - esac - - # Build filenames with prefix - filename="${prefix}/${cat_id}.${ext}" - tagfile="${prefix}/${cat_id}.txt" - - # Download the image - curl -s "$url" -o "$filename" - echo "✓ Saved as $filename" - - # Save tags to text file - echo "$tags" > "$tagfile" - echo "✓ Saved tags to $tagfile" - echo "" -done - -echo "Download complete! Downloaded $num_downloads cat(s) to $prefix/" diff --git a/side-quests/smol-nextflow/bin/download.sh b/side-quests/smol-nextflow/bin/download.sh deleted file mode 100755 index b4220608c..000000000 --- a/side-quests/smol-nextflow/bin/download.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -# Check if CSV file is provided -if [ $# -eq 0 ]; then - echo "Usage: $0 " - exit 1 -fi - -csv_file="$1" - -# Check if file exists -if [ ! -f "$csv_file" ]; then - echo "Error: File '$csv_file' not found" - exit 1 -fi - -# Skip header if present and process each line -tail -n +2 "$csv_file" | while IFS=, read -r url attribution; do - # Remove leading/trailing whitespace and quotes - url=$(echo "$url" | xargs) - attribution=$(echo "$attribution" | xargs | sed 's/^"//;s/"$//') - - # Extract photo ID from URL (the part after /photos/) - photo_id=$(echo "$url" | grep -oP '(?<=photos/)[^/]+' || echo "unknown") - - # Default to jpg for Unsplash images - ext="jpg" - - echo "Downloading: $photo_id" - - # Download the image with wget (quiet mode) - if wget -q -O "${photo_id}.${ext}" "$url"; then - echo "✓ Saved image: ${photo_id}.${ext}" - else - echo "✗ Failed to download: $url" - continue - fi - - # Save attribution to text file - echo "$attribution" > "${photo_id}.txt" - echo "✓ Saved attribution: ${photo_id}.txt" - echo "" -done - -echo "Download complete!" diff --git a/side-quests/smol-nextflow/bin/inspect_model.py b/side-quests/smol-nextflow/bin/inspect_model.py deleted file mode 100755 index 3a50c728a..000000000 --- a/side-quests/smol-nextflow/bin/inspect_model.py +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env -S uv run -# /// script -# dependencies = [ -# "torch>=2.0.0", -# "numpy>=1.24.0", -# ] -# /// -"""Inspect MetaCLIP model checkpoint metadata.""" - -import argparse -from pathlib import Path -import torch - -def inspect_checkpoint(checkpoint_path: Path): - """Load and inspect a PyTorch checkpoint file.""" - - print(f"Loading checkpoint: {checkpoint_path}") - print(f"File size: {checkpoint_path.stat().st_size / (1024**2):.2f} MB\n") - - # Load the checkpoint - checkpoint = torch.load(checkpoint_path, map_location='cpu', weights_only=False) - - # Check what type of object it is - print(f"Checkpoint type: {type(checkpoint)}") - print() - - # If it's a dict, explore its keys - if isinstance(checkpoint, dict): - print("Top-level keys:") - for key in checkpoint.keys(): - print(f" - {key}") - print() - - # Look for common metadata keys - metadata_keys = ['epoch', 'metadata', 'config', 'args', 'model_config', - 'train_config', 'version', 'architecture'] - - print("Metadata found:") - for key in metadata_keys: - if key in checkpoint: - print(f"\n{key}:") - print(f" {checkpoint[key]}") - - # Check state_dict structure - if 'state_dict' in checkpoint: - state_dict = checkpoint['state_dict'] - print(f"\nstate_dict contains {len(state_dict)} tensors") - print("First 10 tensor names:") - for i, name in enumerate(list(state_dict.keys())[:10]): - shape = state_dict[name].shape - print(f" {name}: {shape}") - - # If checkpoint IS the state_dict directly - elif all(isinstance(v, torch.Tensor) for v in list(checkpoint.values())[:5]): - print(f"\nCheckpoint appears to be a state_dict with {len(checkpoint)} tensors") - print("First 10 tensor names:") - for i, (name, tensor) in enumerate(list(checkpoint.items())[:10]): - print(f" {name}: {tensor.shape}") - - else: - print(f"Checkpoint is not a dict, it's: {checkpoint}") - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Inspect PyTorch checkpoint metadata") - parser.add_argument( - 'checkpoint', - type=Path, - help='Path to .pt checkpoint file' - ) - - args = parser.parse_args() - - if not args.checkpoint.exists(): - print(f"Error: {args.checkpoint} does not exist") - exit(1) - - inspect_checkpoint(args.checkpoint) diff --git a/side-quests/smol-nextflow/main.nf b/side-quests/smol-nextflow/main.nf deleted file mode 100644 index 0ddac5bc0..000000000 --- a/side-quests/smol-nextflow/main.nf +++ /dev/null @@ -1,122 +0,0 @@ -params.model = "${projectDir}/data/models/b32_400m.pt" -params.width = 400 - -workflow { - main: - images = channel.fromPath("data/pics/*.{png,gif,jpg}") - | map { img -> [[id: img.baseName], img] } - - Resize(images, params.width) - - Classify(images, file(params.model)) - - Classify.out - | join(Resize.out) - | groupTuple(by: 1) - | Collage - | map { _label, img -> img } - | collect - | CombineImages - - Classify.out - | join(Resize.out) - | map { meta, label, image -> meta + [label:label, image:image] } - | set { classifiedMaps } - - publish: - collage = CombineImages.out - classified = classifiedMaps -} - -output { - collage { - mode 'copy' - } - classified { - mode 'copy' - path { sample -> "images/${sample.label.replaceAll(/\s+/, '_')}" } - index { - header true - path 'images/cats.json' - } - } -} - - -process Resize { - container 'minidocks/imagemagick:7' - - input: - tuple val(meta), path(img) - val(width) - output: tuple val(meta), path("resized-*") - script: "magick ${img} -resize ${width}x resized-${img.baseName}.png" -} - -process Classify { - container 'community.wave.seqera.io/library/pip_open-clip-torch_pillow_torch:83edd876e95b9a8e' - memory '13G' - - input: - tuple val(meta), path(img) - path(model) - output: tuple val(meta), stdout - script: "classify.py --model-path $model ${img}" -} - -process ClassifyJson { - memory '8G' - container 'community.wave.seqera.io/library/pip_open-clip-torch_pillow_torch:83edd876e95b9a8e' - - input: - tuple val(meta), path(img) - path(model) - output: tuple val(meta), path("*.json") - script: "classify.py --model-path $model ${img} --json > out.json" -} - -process Collage { - container 'minidocks/imagemagick:7' - - input: tuple val(metadatas), val(label), path("inputs/*.png") - output: tuple val(label), path("collage.png") - script: - """ - magick montage inputs/* \\ - -geometry +10+10 \\ - -background black \\ - +polaroid \\ - -background '#ffbe76' \\ - collage_nolabel.png - magick montage \\ - -pointsize 48 \\ - -label '$label' \\ - -geometry +0+0 \\ - -background "#f0932b" \\ - collage_nolabel.png collage.png - """ -} - -process CombineImages { - container 'minidocks/imagemagick:7' - - input: path "in.*.png" - output: path "collage_all.png" - script: - """ - magick montage \\ - -geometry +10+10 \\ - -quality 05 \\ - -background '#ffbe76' \\ - -border 5 \\ - -bordercolor '#f0932b' \\ - in.*.png \\ - collage_all.png - """ -} - -process Measure { - input: path(img) - output: tuple val(img.baseName), path('dimensions.txt') - script: "identify -format '%wx%h' '$img' > dimensions.txt" -} diff --git a/side-quests/smol-nextflow/outline.md b/side-quests/smol-nextflow/outline.md deleted file mode 100644 index d1f6de172..000000000 --- a/side-quests/smol-nextflow/outline.md +++ /dev/null @@ -1,858 +0,0 @@ -# Outline - -## Introduction - -Nextflow offers you a way to iterate over a collection of files, so let's grab some files to iterate over. We're going to write a workflow which produces a gallery of good and bad cats. First things we're going to need are some cats. - -We're starting with a (nearly) empty directory. There is a `.stuff` directory that contains some bits and pieces to help us along during the workshop, but you can imagine that we're essentially starting from scratch. - -The first thing we're going to need is some data. I've created a small script that pull from the Cat-As-A-Service API to give us some random cats. - -```bash -.stuff/cat_me.sh --help -``` - -To start, lets grab 4 cats. By default, the script will save the images to `./data/pics`: - -```bash -.stuff/cat_me.sh --count 4 --prefix data/pics -``` - -This will generate some example data. It will look something like this: - -``` -data -└── pics - ├── 5n4MTAC6ld0bVeCe.jpg - ├── 5n4MTAC6ld0bVeCe.txt - ├── IOSNx33kkgkiPfaP.jpg - ├── IOSNx33kkgkiPfaP.txt - ├── IRuDgdPJZFA39dyf.jpg - ├── IRuDgdPJZFA39dyf.txt - ├── uq5KqqiF0qpgTQVA.jpg - └── uq5KqqiF0qpgTQVA.txt -``` - -Now lets iterate over those images in Nextflow. To start, we'll just create a channel of those images. We're not gong to do anything with them, but to make sure that everything is working, we connect the channel to the `view` operator which takes the things in the channel (files in our case) and prints a String representation of those things to the command line: - -```nextflow -workflow { - channel.fromPath("data/pics/*.{png,gif,jpg}") - | view -} -``` - -TODO: Claude: Brief explanation what the {png,gif,jpg} syntax is doing (multiple glob pattern) - -## Channels - -Let's try actually doing something with those images. We'll start with something simple - resizing images. We'll need to download some software to do so. Eventually, we'll talk about containers and reproducibility, but just to start, let's download the software to our machine: - -```bash -sudo apt update -sudo apt-get install -Y imagemagick -``` - -We'll create a new "process" for our resize operation. You can think of these processes as templates, or a classes. We'll connect the process to a channel and fire of a new "task" for each thing in the channel. - -In our Resize process definition (indeed in most process definitions), there are three blocks - input, output, and script. We connect these processes by channels and these blocks describe what we expect to get, what we expect to emit, and the work we want to do in-between. - -```nextflow -workflow { - images = channel.fromPath("data/pics/*.{png,gif,jpg}") - - images - | Resize - | view -} - -process Resize { - input: path(img) - output: path("resized-*") - script: "magick ${img} -resize ${width}x resized-${img.baseName}.png" -} -``` - -### Input - -The input block describes what we expect to take from the input channel. The "things" in the channel can be have a type. The most common "types" are - -- `val` (values Strings, integers, Maps, complex obects), -- `path` (paths like directories, files), or -- `tuple` (a collection of values and paths). - -We'll see more of those later, but for the moment, the channel we created in the `workflow` block is a channel of files, so in our process definition, we'll say "I'm gong to supply a channel of paths to this process, and as our process takes thinsg from that channel to spawn a new process, we'll call the thing in channel `img`. - -### Output - -The output block describes what we want to emit into the process' output channel. Again, we can describe the "type" of thing emitted - `val`, `path`, `tuple` and others. For now, we'll promise to produce a file (or directory) that matches the glob pattern `reseized-*`. - -### Script - -The script block describes what work we want to do on each of the things in the channel - how we're going to transform each of the things we pull from the input channel into the files or values we promised to emit into the outupt channel. - -By default, the script block will be rendered into a bash script, but you can use any interpreted language that makes sese to you - python, ruby, R, zsh, closure, whatever. In this introductory workshop, we'll stick with the default bash. - -We run the "convert" command from imagemagick which performs many types of manipulation. In our case, we'll use the `-resize` argument to resize the image to a width of 400 pixels. We also supply an output filename. You'll notice that we use the `${img}` varliable twice in our script block. This `${img}` is the varibale we defined in the input block. For each iteration of our process (each task), the variable will be the path to our individual image. - -For example, if the "thing" in the channel is the image `kitten.jpg`, then when Nextflow creates a new Resize task for this file, it will "render" our script block into bash, replacing the `${img}` variables with the path to produce this valid bash: - -```bash -magick kitten.jpg -resize 400x resized-kitten.jpg -``` - -Now let's run our workflow! We'll iterate over all of the images in `data/pics` (relative to our current location) and produce a channel of resized pictures that we then pipe into the `view` operator to print the channel contents to stdout. - -## Investigate .command files - -TODO: Explain that each tasks is run in a separate directory.This is to ensure independence of each of the tasks - they can't interfere with each other. - -TODO: Show the contents of one of the task work directories. - -## Harmonization - -One of the nice features of the `convert` utility is that it will also do file format conversion for us. It will infer the format from the extension of the final argument. For example, if we execute - -```bash -magick kitten.jpg -resize 400x resized-kitten.png -``` - -The `convert` utility will both resize the image and convert the jpg to png format. Let's say we want to ensure that downstream in our workflow, we'd like to ensure all images are in the png format. How might we modify our `script` block to replace the extension or pull out the file basename so that we can append the `.png` extension? - -If you're a bash wizard, you might know that if you have a variable `$myFile` with the path to our file, you can replace the extension with this arcane incantation: - -```bash -file=kitten.jpg -magick "$file" -resize 400x "${file%.*}.png" -``` - -Or perhaps you use the `basename` utility: - -```bash -magick "$file" -resize 400x "$(basename "$file" .${file##*.}).png" -``` - -I love bash, but it's easy to forget this syntax or mistype it. Fortunately for us, when inside the script block the `img` variable is not a bash variable - it's a Nextflow variable, and Nextflow provides some convenience methods for operating on those path objects. The full list is available in the [Nextflow stdlib documentation](https://www.nextflow.io/docs/latest/reference/stdlib-types.html#stdlib-types-path), but one handy method is `baseName` -We can simply call `${img.baseName}` to retun the file base name. For example: - -```nextflow -process Resize { - input: path(img) - output: path("resized-*") - script: "magick ${img} -resize 400x resized-${img.baseName}.png" -} -``` - -## Parameters - -What if we want to make our workflow a little more flexible. Let's pull out the width and expose it as a parameter to the user. - -```nextflow -process Resize { - input: path(img) - output: path("resized-*") - script: "magick $img -resize ${width}x resized-${img.baseName}.png" -} -``` - -Now we can run wth - -```bash -nextflow run . --width 300 -``` - -This is great, but it's considered best practice (and we'll see why in a bit) to make the inputs to a process explicit. We can do this by adding a second channel as input: - -```nextflow -workflow { - images = channel.fromPath("data/pics/*.{png,gif,jpg}") - - Resize(images, params.width) - | view -} - -process Resize { - input: - path(img) - val(width) - output: path("resized-*") - script: "magick $img -resize ${width}x resized-${img.baseName}.png" -} -``` - -The params object still works in the same way: - -```bash -nextflow run . --width 500 -``` - -## Extracting an ID - -Great, but I'd like a way of retaining the original IDs. - -TODO: Claude: Explain the `map` operator and explain closures (3 sentences) - -```nextflow -workflow { - images = channel.fromPath("data/pics/*.{png,gif,jpg}") - | map { img -> [img.baseName, img] } - | view -} -``` - -Better - we have the id extracted as a String. What if we want to add other metadata later? Let's turn it into a Map - -```nextflow -workflow { - images = channel.fromPath("data/pics/*.{png,gif,jpg}") - | map { img -> [[id: img.baseName], img] } -} -``` - -Now we've change the "shape" of the items in the channel, so we'll update the downstream process: - -```nextflow -process Resize { - input: - tuple val(meta), path(img) - val(width) - output: path("resized-*") - script: "magick $img -resize ${width}x resized-${img.baseName}.png" -} -``` - -Run the workflow and view the output: - -```bash -nextflow run . -``` - -## Classification - -Let's get the fun part - the cat sorting. We have a little classification script - `classify.py` that I've provided in the `.stuff` directory. In your research sometimes you have small accessory scripts that are useful for your pipeliens. We're using a python script here in this workshop example, but this patttern will hold for scripts written in perl, ruby, R, python, closurescript, or any of the other interpreted languages. - -Let's pull the file out into a new `bin` directory: - -``` -mkdir -p bin -cp .stuff/classify.py bin/ -``` - -The script requires some dependencies. Again, we'll do this the slow/painful way one time before we demonstrate how to use containers to encapsulate the software dependencies. - -We'll grab one more file from our `.stuff` directory - a pyproject.toml file which is a way of describing softare dependencies for Python projects. This is unrelated to Nextflow, but an example of one of the (many) ways in which different languages and frameworks might install software. - -You can install the dependencies and activate the environment with: - -```bash -cp .stuff/pyproject.toml . -uv sync -source .venv/bin/activate -``` - -which you can run with: - -```bash -bin/classify.py --help -``` - -```` -usage: classify.py [-h] [--model-path MODEL_PATH] [--labels LABELS [LABELS ...]] [--json] image - -Classify a single image using MetaCLIP - -positional arguments: - image Path to the image file to classify - -options: - -h, --help show this help message and exit - --model-path MODEL_PATH - Path to MetaCLIP model weights (default: data/models/b32_400m.pt) - --labels LABELS [LABELS ...] - Labels for classification (default: ["good cat", "bad cat"]) - --json Output result as JSON to stdout - --architecture {ViT-B-32-quickgelu,ViT-B-16-quickgelu,ViT-L-14-quickgelu,ViT-H-14-quickgelu} - Model architecture (auto-detected from filename if not specified)``` -```` - -The script takes images, a model, and a set of labels and classifies each of the images according to the labels. To run the script outside of Nextflow, we'll need to download one of the models. Do so with: - -```bash -mkdir -p data/models -(cd data/models && wget https://dl.fbaipublicfiles.com/MMPT/metaclip/b32_400m.pt) -``` - -Now let's create a `Classify` process that will take two channels - one channel of images and one channel that supplies the model: - -```nextflow -process Classify { - input: - tuple val(meta), path(img) - path(model) - output: tuple val(meta), stdout - script: "classify.py --model-path $model ${img}" -} -``` - -Note here that we're calling the `classify.py` script directly, even though we can't do that from the command line (we had to provide the relative or absolute path). This is because Nextflow automatically adds the `bin` directory (relative to the main.nf) to the `$PATH` for all Nextflow tasks. This is a very convenient way to bundle accessory scripts and snippets with your workflow. - -Processes can have multiple channels as input or as output. A process will continue to emit tasks as long as it can pull an item from each of the input channels. We could create a new channel for the model, and define a sensible default: - -```nextflow -params.model = "${projectDir}/data/models/b32_400m.pt" - -workflow { - images = channel.fromPath("data/pics/*.{png,gif,jpg}") - | map { img -> [[id: img.baseName], img] } - - model_channel = channel.fromPath(params.model) - // rest of the workflow -} -``` - -... which would return a channel with a single item. Try supplying this channel as input to our Classify process: - -```nextflow -params.model = "${projectDir}/data/models/b32_400m.pt" - -workflow { - images = channel.fromPath("data/pics/*.{png,gif,jpg}") - | map { img -> [[id: img.baseName], img] } - - model_channel = channel.fromPath(params.model) - Classify(images, model_channel) - // rest of the workflow -} -``` - -What happens when you run the workflow? Given what we know about the channels, what might be happening? - -Answers: The Classify process only spawns a single task. This is because after pulling the model path from the second input channel on the first iteration, the channel is empty, so no more Classify tasks can be submitted for execution. - -There are two types of channel in Nextflow - queue channels and value channels. Queue channels are exhaustible - they have a set number of items in the channel and each processes can only take each item in the channel once. The second type of channel is a value channel, which is a channel of only a single item. This item is emiited without exhaustion. - -There are some operators which will alwys return a value channel. Examples are `first`, `collect`, `count`, etc. - -We could also create a value channel using the `channel.value` factory: - -```nextflow -params.model = "${projectDir}/data/models/b32_400m.pt" - -workflow { - images = channel.fromPath("data/pics/*.{png,gif,jpg}") - | map { img -> [[id: img.baseName], img] } - - model_channel = channel.value(file(params.model)) - Classify(images, model_channel) - // rest of the workflow -} -``` - -Note here that we're wrapping the params.model value (a String) in the `file()` function, which turns an ordinary String into an object that Nextflow can use as a path. We've not needed to use this until now because the `channel.fromPath` factory necessarily returns paths, so it automatically does this conversion for us. - -An even simpler solution is to provide the path object directly when calling the process. Any non-channel object will automatically be converted into a value channel for you. - -```nextflow -params.model = "${projectDir}/data/models/b32_400m.pt" - -workflow { - images = channel.fromPath("data/pics/*.{png,gif,jpg}") - | map { img -> [[id: img.baseName], img] } - - Classify(images, file(params.model)) - // rest of the workflow -} -``` - -Now we'd like to take our channel of images and pass them each through this classification program. - -We'd like to combine each of our images with each of our models (only one at the moment). Do do these cross-product operations, we can use the `combine` operator: - -```nextflow -images -| combine(models) -| view -``` - -This will produce a channel that "combines" our images and the reference file. The channel now looks something like: - -``` -[imageMetaData, image, modelMetaData, model] -[imageMetaData, image, modelMetaData, model] -[imageMetaData, image, modelMetaData, model] -[imageMetaData, image, modelMetaData, model] -``` - -We might as well combine these two metadata Maps with a `map` operation: - -```nextflow -images -| combine(models) -| map { imgMeta, img, modelMeta, model -> [imgMeta + modelMeta, model, img]} -| view -``` - -That now gives us a channel that looks like: - -``` -[metaData, image, model] -[metaData, image, model] -[metaData, image, model] -[metaData, image, model] -``` - -Given that shape, let's create a `Classify` process: - -```nextflow -process Classify { - input: - tuple val(meta), path(img) - path(model) - output: tuple val(meta), stdout - script: "classify.py --model-path $model ${img}" -} -``` - -Now we can run this: - -```bash -nextflow run . -``` - -You might find that the process errors out with a 137 exit code. This generally means that we've run out of RAM because we're running too many of these classification jobs at the same time. Let's talk about how we tell Nextflow that a particular process requires more resources. - -## Resources - -Our processes are currently composed of the `input:`, `output:`, and `script:` blocks. In addition to these blocks, processes can use "process directives" which are optional annotations which modify the behaviour of the processes. There are many directives ([documentation](https://www.nextflow.io/docs/latest/reference/process.html#directives)), but we can introduce the concept with two important process directives - `memory` and `cpus`. - -So far, we've been using the local executor to run Nextflow - running on the local machine. There are many other executors targetting different backends, from HPC executors like SLURM and PBS to cloud executors like {AWS,GCP,Azure} Batch. There are more than a dozen supported executors ([documentation](https://www.nextflow.io/docs/latest/executor.html)). - -Each of these have a concept of the resources a particular task will require - resources such as cpus, memory, gpus, disk, etc. - -If not otherwise specified, the defaults are to request 1 cpu, 1 GB of RAM and 0 GPUs for each task. - -When using the local executor, Nextflow scans the machine it is running on and determines how many cpus and how much RAM the system has. It will ensure that (given the resources specified or defaults applied) the running tasks never exceed the available limits. If the system has 16 GB of RAM, for example, and a particular process requires 6 GB of ram, Nextflow will ensure that _at most_ 2 of those tasks are running at any one time. As a task finishes, Nextflow begins the next task in line. - -## Grouping - -Which we can now join up to our channel chain using the `join` operator, which finds pairs of items (one from each channel) that share a key. By default, the `join` operator will use the first element of each item in the channel as the key. In our case, that first item was the image metadata, which occupies the first position in both the Classify process output and the images channel. - -```nextflow -Classify.out -| join(images) -| view -``` - -which produces a channel like: - -``` -[metadata, label, img] -[metadata, label, img] -[metadata, label, img] -[metadata, label, img] -``` - -In order to make a picture of just the good cats and a second picture of just the bad cats, we'll need to group the items in the channel based on the label. We can do this with the `groupTuple` operator. Normally the groupTuple expects that the grouping key will be the first element in each item in the channel. In our case, it is the second item, i.e. index "1" if the first item is index "0". To ask Nextflow to group on the item with index 1, we add a `by: 1` argument to the operator: - -```nextflow -Classify.out -| join(images) -| groupTuple(by: 1) -| view -``` - -which produces a channenl of the form: - -``` -[metadatas, label, images] -[metadatas, label, images] -``` - -## Collage - -Let's create a `collage` process that takes this channel and produces a collage of all of the images for each label. The script block here is a little involved. - -```nextflow -process Collage { - input: tuple val(metadatas), val(label), path("inputs/*.png") - output: tuple val(label), path("collage.png") - script: - """ - magick montage inputs/* \\ - -geometry +10+10 \\ - -background black \\ - +polaroid \\ - -background '#ffbe76' \\ - collage_nolabel.png - magick montage \\ - -pointsize 48 \\ - -label '$label' \\ - -geometry +0+0 \\ - -background "#f0932b" \\ - collage_nolabel.png collage.png - """ -} -``` - -We can then hoook this into our channel chain: - -```nextflow -Classify.out -| join(images) -| groupTuple(by: 1) -| Collage -| view -``` - -Those collage tasks are taking a little too long, but that might be because we're collaging the original full sized images and not our resized images. Because the `images` channel and the output channel from the `Resize` process both have the same shape, we can simply replace them in the workflow: - -```nextflow -Classify.out -| join(Resize.out) -| groupTuple(by: 1) -| Collage -| view -``` - -For our final process, let's combine these two collages together into a single final image. We'll create a process that takes collection of images (we don't care what they are called) and produces a final `collage_all.png` image. - -```nextflow -process CombineImages { - input: path "in.*.png" - output: path "collage_all.png" - script: - """ - magick montage \\ - -geometry +10+10 \\ - -quality 05 \\ - -background '#ffbe76' \\ - -border 5 \\ - -bordercolor '#f0932b' \\ - in.*.png \\ - collage_all.png - """ -} -``` - -The channel coming from the Collage process looks like - -``` -[label, collageImage] -[label, collageImage] -``` - -but we need it to look like: - -``` -[collageImage, collageImage] -``` - -So we'll drop the labels: - -``` -Classify.out -| join(Resize.out) -| groupTuple(by: 1) -| Collage -| map { _label, img -> img } -| view -``` - -to give us a channel that looks like: - -``` -collageImage -collageImage -``` - -and then pass that to `collect` which takes all the items in a channel and then emits them as a single "wide" collection: - -``` -Classify.out -| join(Resize.out) -| groupTuple(by: 1) -| Collage -| map { _label, img -> img } -| collect -| view -``` - -We can now pass this to our new CombineImages process: - -``` -Classify.out -| join(Resize.out) -| groupTuple(by: 1) -| Collage -| map { _label, img -> img } -| collect -| CombineImages -| view -``` - -## Workflow Outputs - -Great! We have a workflow that (arguably cruely) collects our cat into "good" and "bad" groupings! Unfortunately, the final output file is still deep in this work directory in a hostile-looking hash-addressed directory. We'd like to define some final workflow outputs that should be published somewhere safe, outside of the work directory. - -To define the workflow outputs, we'll need to define a `publish:` block in the workflow. We'll also need to put the existing workflow in a `main:` block as shown below: - -```nextflow -workflow { - main: - images = channel.fromPath("data/pics/*.{png,gif,jpg}") - | map { img -> [[id: img.baseName], img] } - - Resize(images, params.width) - - Classify(images, file(params.model)) - - Classify.out - | join(Resize.out) - | groupTuple(by: 1) - | Collage - | map { _label, img -> img } - | collect - | CombineImages - - publish: - // Something to go here -} -``` - -### Publishing the Collage - -In the `publish:` block, we define channels that we'd like to publish. - -```nextflow -workflow { - main: - // workflow here - publish: - collage = CombineImages.out -} - -output { - collage {} -} -``` - -Now when we run, the final collage will be symlinked into `results/collage_all.png` - -We can control the publication mechanism my adding arguments: - -```nextflow -output { - collage { - mode 'copy' - } -} -``` - -... which will now cause Nextflow to copy the output file rather than symlink - -### Publishing the classifications - -The more intereting outputs might be those with more metadata associated with them. For example, we might want to record the classification for each image ID. To publsh metadata-rich outputs, we'll first create a channel that is composed of Maps, e.g. - -```nextflow -workflow { - main: - // workflow here - - Classify.out - | join(Resize.out) - | map { meta, label, image -> meta + [label:label, image:image] } - | set { classifiedMaps } - - publish: - collage = CombineImages.out - classification = classifiedMaps -} - -output { - collage { - mode 'copy' - } - classified { - mode 'copy' - } -} -``` - -This will cause the resized images to also be published in the `results` directory, but it's looking a bit cluttered now. - -``` -results -├── collage_all.png -├── resized-4skdDxHm4yDsSJIr.png -├── resized-4y6Hyu0uzVZcEx89.png -├── resized-6Nb0ipGrHDHqCEmZ.png -└── resized-wfMCf1lHc9YPw455.png -``` - -Let's bring a little bit of order: - -```nextflow -output { - collage { - mode 'copy' - } - classified { - mode 'copy' - path { sample -> "images/${sample.label.replaceAll(/\s+/, '_')}" } - } -} -``` - -``` -results -├── collage_all.png -└── images - ├── good_cat - │ ├── resized-4skdDxHm4yDsSJIr.png - │ └── resized-wfDuHvt6VIn2tM8T.png - └── bad_cat - ├── resized-6Nb0ipGrHDHqCEmZ.png - └── resized-wfMCf1lHc9YPw455.png -``` - -Now we sanitized the label names so that they'd be in more sensibly named directories (no spaces, etc), but this risks corrupting that metadata. Lets ask Nextflow to publish a more digestible samplesheet or "index" of the published outputs, that includes the real, unsanitized labels: - -```nextflow -output { - collage { - mode 'copy' - } - classified { - mode 'copy' - path { sample -> "images/${sample.label.replaceAll(/\s+/, '_')}" } - index { - header true - path 'images/cats.csv' - } - } -} -``` - -which produces a csv at results/images/cats.csv. - -For more structured data, you can also choose yaml or json: - -```nextflow -output { - collage { - mode 'copy' - } - classified { - mode 'copy' - path { sample -> "images/${sample.label.replaceAll(/\s+/, '_')}" } - index { - header true - path 'images/cats.json' - } - } -} -``` - -## Filesystem Independence - -Nextflow speasks many different communication protocols, allowing you to seamlessly move from using data on a local or shared filesystem, to http/https://, to object storage protocols like s3://, az://, gcp:// or even older ftp:// protocols. You can provide support for new protocols yourself via Nextflow's plugin system. - -For example, our current workflow uses the - -```bash -nextflow run . --model https://dl.fbaipublicfiles.com/MMPT/metaclip/b32_400m.pt -``` - -## Containerization - -All of our Nextflow tasks are currently using the software installed on the host operating system. This practice can quickly become a problem for you for a number of reasons: - -- As the workflow grows, and the number of software dependency stacks will also likely grow, and it becomes increasingly likely that the installation of one piece of software accidentally updates a depencency of another piece of software. Similarly, you many end up with incompatible software dependency stacks. -- The analysis becomes tied to a very specific machine or infrastructure, difficult to repdocuce in exactly the same way elsewhere (by yourself or by a colleague) -- Managing software is a thankless and boring task. - -Nextflow provides the opportunity to run each task in an isolated software environment, and can do so via a variety of technologies, including - -- conda -- containers (docker, apptainer/singularity, charliecloud, sarus, shifter, and podman) -- spack - -Let's improve the reproducibility and portability of our workflow - -TODO: Claude: include a brief (3-4 sentence) explanation of containerization (with a focus on Docker) - -You'll remember that we manually installed software two different ways: - -- imagemagick (via `apt-get install`), and -- python packages (via `uv sync`) - -We could use a single container for all of the steps in the workflow, but this might limit the reusability of the containers, and upgrading one piece of software for one task would mean changing the container for all of the tasks. Most researchers prefer (and Nextflow supports) defining container per-process. - -To replace the imagemagick we installed via apt-get, we'll use the public container 'minidocks/imagemagick:7' - -We've already talked about the `memory` and `cpus` process directives, but another useful directive is the `container` directive. We'll use this to add the container to our `Resize`, `Classify`, `Collage`, and `CombineImages` processes, e.g. - -```nextflow -process Resize { - container 'minidocks/imagemagick:7' - //... - -process Classify { - container 'minidocks/imagemagick:7' - //... - -process Collage { - container 'minidocks/imagemagick:7' - //... - -process CombineImages { - container 'minidocks/imagemagick:7' - //... -``` - -Our `classify.py` process includes three specific python packages (torch, pillow, and openclip-torch) at specific versions. It's unlikely that there is an existing container that provides these specific packages. We could opt to build our own - -There are a number of ways of building containers, but we'll use the [Seqera Containers](https://seqera.io/containers/) web interface. You can add multiple packages - -![Creating a new container using Seqera Containers](./img/seqera-container-python-00.png) - -## Version Control - -TOOD: Create git repository at project root -TODO: Commit current state, create git tag, and create branch, and then change and re-commit. -TODO: Change directories and then run using revision argument, pointing to branch, tag, and then specific commmit. - -## Cloud Executors - -## Extension Exercise 1 - -Our team is interested in which cat is the custest cat and which cat is the ugliest cat. Can you extend the workflow to identify (for each label) which picture scores the highest? - -Hint 1: You can use the --json flat on the `classify.py` script -Hint 2: You can parse a json file in a closure by using the JsonSlurper class, part of the standard library. It will return a standard - -```nextflow -| map { meta, jsonFile -> new groovy.json.JsonSlurper().parseText(jsonFile.text) } -``` - -Hint 3: You can use the `min` and `max` operators to return a channel containing the minimum or maximum item, and you can pass a closure to those operators to describe how the elements in the channel should be compared ([docs](https://www.nextflow.io/docs/latest/reference/operator.html#min)) - -## Extension Exercise 2 - -We've decided that "bad" and "good" are too cruel a classification system for the cats. Can you modify the workflow to add a `--labels` parameter. The parameter should take a comma-separated list of labels and use those labels in preference to the default "good cat" and "bad cat". E.g. - -``` -nextflow run . --labels 'red cat','orange cat','black cat' -``` - -## Last TODOs - -TODO: explain difference between path(img) and path("inputs/\*.png") -TODO: Add in resouces directive memory, cpus, etc. - - From 6da360af31a9ea22a7ee68bc1ba883fb213b89d3 Mon Sep 17 00:00:00 2001 From: Rob Syme Date: Tue, 18 Nov 2025 15:26:00 +0800 Subject: [PATCH 16/22] Revert to older imagemagick syntax --- docs/small_nextflow/01_fundamentals.md | 20 +++++++++---------- .../03_publishing_portability.md | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/small_nextflow/01_fundamentals.md b/docs/small_nextflow/01_fundamentals.md index ae9821753..19aad73bd 100644 --- a/docs/small_nextflow/01_fundamentals.md +++ b/docs/small_nextflow/01_fundamentals.md @@ -129,7 +129,7 @@ workflow { process Resize { input: path(img) output: path("resized-*") - script: "magick ${img} -resize 400x resized-${img.baseName}.png" + script: "convert ${img} -resize 400x resized-${img.baseName}.png" } ``` @@ -168,7 +168,7 @@ For each iteration of our process (each task), the variable will be the path to For example, if the "thing" in the channel is the image `kitten.jpg`, then when Nextflow creates a new Resize task for this file, it will "render" our script block into bash, replacing the `${img}` variables with the path to produce this valid bash: ```bash -magick kitten.jpg -resize 400x resized-kitten.jpg +convert kitten.jpg -resize 400x resized-kitten.jpg ``` ### Run the workflow @@ -270,7 +270,7 @@ cat work/a0/*/command.sh You'll see the actual bash script that was executed with all Nextflow variables resolved: ```bash -magick 5n4MTAC6ld0bVeCe.jpg -resize 400x resized-5n4MTAC6ld0bVeCe.png +convert 5n4MTAC6ld0bVeCe.jpg -resize 400x resized-5n4MTAC6ld0bVeCe.png ``` ### Task isolation and idempotence @@ -301,7 +301,7 @@ It will infer the format from the extension of the final argument. For example, if we execute: ```bash -magick kitten.jpg -resize 400x resized-kitten.png +convert kitten.jpg -resize 400x resized-kitten.png ``` The `magick` utility will both resize the image and convert the jpg to png format. @@ -314,13 +314,13 @@ If you're a bash wizard, you might know that if you have a variable `$myFile` wi ```bash file=kitten.jpg -magick "$file" -resize 400x "${file%.*}.png" +convert "$file" -resize 400x "${file%.*}.png" ``` Or perhaps you use the `basename` utility: ```bash -magick "$file" -resize 400x "$(basename "$file" .${file##*.}).png" +convert "$file" -resize 400x "$(basename "$file" .${file##*.}).png" ``` I love bash, but it's easy to forget this syntax or mistype it. @@ -347,7 +347,7 @@ workflow { process Resize { input: path(img) output: path("resized-*") - script: "magick ${img} -resize 400x resized-${img.baseName}.png" + script: "convert ${img} -resize 400x resized-${img.baseName}.png" } ``` @@ -390,7 +390,7 @@ workflow { process Resize { input: path(img) output: path("resized-*") - script: "magick $img -resize ${params.width}x resized-${img.baseName}.png" + script: "convert $img -resize ${params.width}x resized-${img.baseName}.png" } ``` @@ -420,7 +420,7 @@ process Resize { path(img) val(width) output: path("resized-*") - script: "magick $img -resize ${width}x resized-${img.baseName}.png" + script: "convert $img -resize ${width}x resized-${img.baseName}.png" } ``` @@ -507,7 +507,7 @@ process Resize { tuple val(meta), path(img) val(width) output: path("resized-*") - script: "magick $img -resize ${width}x resized-${img.baseName}.png" + script: "convert $img -resize ${width}x resized-${img.baseName}.png" } ``` diff --git a/docs/small_nextflow/03_publishing_portability.md b/docs/small_nextflow/03_publishing_portability.md index da0712059..136c7fcf8 100644 --- a/docs/small_nextflow/03_publishing_portability.md +++ b/docs/small_nextflow/03_publishing_portability.md @@ -315,7 +315,7 @@ process Resize { tuple val(meta), path(img) val(width) output: tuple val(meta), path("resized-*") - script: "magick ${img} -resize ${width}x resized-${img.baseName}.png" + script: "convert ${img} -resize ${width}x resized-${img.baseName}.png" } process Collage { From 6887d91c8c82e1f787a701011496f79440c68bfa Mon Sep 17 00:00:00 2001 From: Rob Syme Date: Tue, 18 Nov 2025 15:36:33 +0800 Subject: [PATCH 17/22] Clarify closure description. --- docs/small_nextflow/01_fundamentals.md | 5 +++-- docs/small_nextflow/02_data_transformation.md | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/small_nextflow/01_fundamentals.md b/docs/small_nextflow/01_fundamentals.md index 19aad73bd..022c51658 100644 --- a/docs/small_nextflow/01_fundamentals.md +++ b/docs/small_nextflow/01_fundamentals.md @@ -451,7 +451,7 @@ It takes a collection of items in a channel and transforms them into a new colle The transformation is defined by a closure - a small piece of code that is evaluated "later" - during workflow execution. Each item in the new channel is the result of applying the closure to the corresponding item in the original channel. -A closure is written as `{ input -> output }` where you define how to transform the input into the output. +A closure is written as `{ input -> }` where to the left of the "stabby operator" `->`, you define the variable used to refer to the closure input, and then an expression or series of expressions. The last expression will be the return value of the closure. For map, the items in the resulting output channel are the collection of values returned by each invocation of the closure. Let's use `map` to extract the ID from each filename: @@ -506,7 +506,8 @@ process Resize { input: tuple val(meta), path(img) val(width) - output: path("resized-*") + output: + tuple val(meta), path("resized-*") script: "convert $img -resize ${width}x resized-${img.baseName}.png" } ``` diff --git a/docs/small_nextflow/02_data_transformation.md b/docs/small_nextflow/02_data_transformation.md index 2f43b4e67..861af4955 100644 --- a/docs/small_nextflow/02_data_transformation.md +++ b/docs/small_nextflow/02_data_transformation.md @@ -328,13 +328,13 @@ process Collage { output: tuple val(label), path("collage.png") script: """ - magick montage inputs/* \\ + montage inputs/* \\ -geometry +10+10 \\ -background black \\ +polaroid \\ -background '#ffbe76' \\ collage_nolabel.png - magick montage \\ + montage \\ -pointsize 48 \\ -label '$label' \\ -geometry +0+0 \\ @@ -402,7 +402,7 @@ process CombineImages { output: path "collage_all.png" script: """ - magick montage \\ + montage \\ -geometry +10+10 \\ -quality 05 \\ -background '#ffbe76' \\ From 9e7f8e2f2ecc0af4f086535c466deb4daec757f3 Mon Sep 17 00:00:00 2001 From: Rob Syme Date: Tue, 18 Nov 2025 15:37:37 +0800 Subject: [PATCH 18/22] Newline fix --- docs/small_nextflow/01_fundamentals.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/small_nextflow/01_fundamentals.md b/docs/small_nextflow/01_fundamentals.md index 022c51658..82a0af584 100644 --- a/docs/small_nextflow/01_fundamentals.md +++ b/docs/small_nextflow/01_fundamentals.md @@ -506,7 +506,7 @@ process Resize { input: tuple val(meta), path(img) val(width) - output: + output: tuple val(meta), path("resized-*") script: "convert $img -resize ${width}x resized-${img.baseName}.png" } From de1e5d3619310e78b3316ce556c797af153cfb96 Mon Sep 17 00:00:00 2001 From: Rob Syme Date: Tue, 18 Nov 2025 15:42:35 +0800 Subject: [PATCH 19/22] Remove notes to self --- docs/small_nextflow/04_advanced.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/small_nextflow/04_advanced.md b/docs/small_nextflow/04_advanced.md index 6deff7988..910cff7e9 100644 --- a/docs/small_nextflow/04_advanced.md +++ b/docs/small_nextflow/04_advanced.md @@ -318,12 +318,6 @@ Congratulations! You've completed the Small Nextflow workshop. ## Final notes -### Additional topics to explore - -TODO: explain difference between `path(img)` and `path("inputs/*.png")` - -TODO: Add in resources directive memory, cpus, etc. (note: this is partially covered in section 8) - ### What you've learned Congratulations on completing the Small Nextflow workshop! From f5b75c18b52f19e38dfb73047a4a83d16c35ddef Mon Sep 17 00:00:00 2001 From: Rob Syme Date: Tue, 18 Nov 2025 15:43:15 +0800 Subject: [PATCH 20/22] typo fix --- docs/small_nextflow/04_advanced.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/small_nextflow/04_advanced.md b/docs/small_nextflow/04_advanced.md index 910cff7e9..d0113ef2c 100644 --- a/docs/small_nextflow/04_advanced.md +++ b/docs/small_nextflow/04_advanced.md @@ -279,7 +279,7 @@ The parameter should take a comma-separated list of labels and use those labels ### Example usage ```bash -nextflow run main.nf --labels 'red cat','orange cat','black cat' +nextflow run main.nf --labels 'red cat,orange cat,black cat' ``` ### Hints From f3d61c75188b0ad65459db5f297b7f45804954ab Mon Sep 17 00:00:00 2001 From: Rob Syme Date: Tue, 18 Nov 2025 16:34:55 +0800 Subject: [PATCH 21/22] Include params.width definition where sensible --- docs/small_nextflow/02_data_transformation.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/small_nextflow/02_data_transformation.md b/docs/small_nextflow/02_data_transformation.md index 861af4955..201946a4f 100644 --- a/docs/small_nextflow/02_data_transformation.md +++ b/docs/small_nextflow/02_data_transformation.md @@ -146,7 +146,7 @@ Any non-channel object will automatically be converted into a value channel for ```groovy title="main.nf" hl_lines="8" linenums="1" #!/usr/bin/env nextflow - +params.width = 400 params.model = "${projectDir}/data/models/b32_400m.pt" workflow { @@ -248,7 +248,7 @@ Update your workflow to join the channels: ```groovy title="Workflow with join" hl_lines="12-14" linenums="1" #!/usr/bin/env nextflow - +params.width = 400 params.model = "${projectDir}/data/models/b32_400m.pt" workflow { @@ -282,7 +282,7 @@ To ask Nextflow to group on the item with index 1, we add a `by: 1` argument to ```groovy title="Workflow with grouping" hl_lines="13-15" linenums="1" #!/usr/bin/env nextflow - +params.width = 400 params.model = "${projectDir}/data/models/b32_400m.pt" workflow { @@ -350,7 +350,7 @@ We can then hook this into our channel chain: ```groovy title="Workflow with Collage" hl_lines="13-15" linenums="1" #!/usr/bin/env nextflow - +params.width = 400 params.model = "${projectDir}/data/models/b32_400m.pt" workflow { @@ -374,7 +374,7 @@ Because the `images` channel and the output channel from the `Resize` process bo ```groovy title="Optimized workflow" hl_lines="12" linenums="1" #!/usr/bin/env nextflow - +params.width = 400 params.model = "${projectDir}/data/models/b32_400m.pt" workflow { @@ -433,7 +433,7 @@ So we'll drop the labels and collect all images: ```groovy title="Final workflow" hl_lines="14-18" linenums="1" #!/usr/bin/env nextflow - +params.width = 400 params.model = "${projectDir}/data/models/b32_400m.pt" workflow { From a38e5198e3229aca00035526b840fb78e3da0ee0 Mon Sep 17 00:00:00 2001 From: Rob Syme Date: Wed, 19 Nov 2025 09:58:44 +0800 Subject: [PATCH 22/22] Fix type in output config --- docs/small_nextflow/03_publishing_portability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/small_nextflow/03_publishing_portability.md b/docs/small_nextflow/03_publishing_portability.md index 136c7fcf8..f51318f56 100644 --- a/docs/small_nextflow/03_publishing_portability.md +++ b/docs/small_nextflow/03_publishing_portability.md @@ -107,7 +107,7 @@ workflow { publish: collage = CombineImages.out - classification = classifiedMaps + classified = classifiedMaps } output {