From 9ac051888af2018b3ab2f7d3ea02560b913b472f Mon Sep 17 00:00:00 2001 From: Jmasseysykes Date: Tue, 14 Jul 2026 08:49:17 -0800 Subject: [PATCH 1/4] feat(com4FlowPy): optional Numba compute engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an @njit compute engine for the per-cell BFS, selected via a new `engine` config key (python | numba; default python). The BFS is expressed as a single @njit function over flat arrays; tiling, multiprocessing, forest handling, variable parameters, I/O and merging are unchanged. infra/back-calculation, previewMode and relId outputs fall back to the Python engine, as does a missing numba install (declared as an optional dependency). Faithful to the fluxDistOldVersion=False path: float persistence, g=9.81, all forest modules + forestInteraction, variable alpha/max_z/exponent, and the default flux distribution. The per-BFS queue is sized modestly and grows/retries on overflow, so a path is never silently truncated. Bit-identical to the Python engine on single-tile domains (Connaught 21 m; BFW 10 m, also matching an independent BFW run's stored zDelta/fpTravelAngleMax); ~30-45x faster at 8 cores with memory on par (PSS). On fine-resolution flat runout terrain a few cells differ — inherent numerical routing sensitivity (the Python result itself shifts ~175 cells under a 1 micron DEM perturbation), characterized in benchmark/REPORT.md. Co-Authored-By: Claude Opus 4.8 (1M context) --- avaframe/com4FlowPy/benchmark/.gitignore | 7 + avaframe/com4FlowPy/benchmark/README.md | 46 ++ avaframe/com4FlowPy/benchmark/REPORT.md | 232 +++++++ .../benchmark/fixtures/example.ini.template | 24 + avaframe/com4FlowPy/benchmark/flowpy_bench.py | 348 ++++++++++ avaframe/com4FlowPy/com4FlowPy.py | 3 + avaframe/com4FlowPy/com4FlowPyCfg.ini | 8 + avaframe/com4FlowPy/flowCore.py | 22 +- avaframe/com4FlowPy/flowCoreNumba.py | 654 ++++++++++++++++++ pyproject.toml | 6 + 10 files changed, 1349 insertions(+), 1 deletion(-) create mode 100644 avaframe/com4FlowPy/benchmark/.gitignore create mode 100644 avaframe/com4FlowPy/benchmark/README.md create mode 100644 avaframe/com4FlowPy/benchmark/REPORT.md create mode 100644 avaframe/com4FlowPy/benchmark/fixtures/example.ini.template create mode 100644 avaframe/com4FlowPy/benchmark/flowpy_bench.py create mode 100644 avaframe/com4FlowPy/flowCoreNumba.py diff --git a/avaframe/com4FlowPy/benchmark/.gitignore b/avaframe/com4FlowPy/benchmark/.gitignore new file mode 100644 index 000000000..02b1fddda --- /dev/null +++ b/avaframe/com4FlowPy/benchmark/.gitignore @@ -0,0 +1,7 @@ +# Local run artifacts and machine-specific fixtures — never committed. +# Concrete fixtures carry absolute data paths; only the *.template is tracked. +runs/ +fixtures/*.ini + +# debug repro (not for commit) +repro_tile.py diff --git a/avaframe/com4FlowPy/benchmark/README.md b/avaframe/com4FlowPy/benchmark/README.md new file mode 100644 index 000000000..bdd62d8a1 --- /dev/null +++ b/avaframe/com4FlowPy/benchmark/README.md @@ -0,0 +1,46 @@ +# com4FlowPy Numba-engine benchmark & validation harness + +`flowpy_bench.py` runs com4FlowPy in-process on a *fixture* (a `.ini` describing +inputs + `[GENERAL]` config), measures **wall-clock** and **peak RAM**, and +optionally compares every output raster against a **reference directory**. + +It exercises the normal `com4FlowPyMain` code path (as in the `useCustomPaths` +branch of `runCom4FlowPy.py`); the only added knob is `--engine`, which selects +the compute engine: + +* `--engine python` — the stock `Cell`-based BFS (default; always available). +* `--engine numba` — the ported `@njit` kernel (once merged). + +## Usage + +```bash +# Freeze the stock-engine oracle on the correctness fixture +python flowpy_bench.py fixtures/connaught_frequent.ini --engine python --tag oracle + +# A/B the numba engine against it (bit-equivalence check) +python flowpy_bench.py fixtures/connaught_frequent.ini --engine numba + +# Benchmark on BFW's own 10 m run and cross-check vs their stored outputs +python flowpy_bench.py fixtures/bfw_10m.ini --engine python +python flowpy_bench.py fixtures/bfw_10m.ini --engine numba --cpu 8 +``` + +Each run writes `runs/__/summary.json` with timing, +peak RAM, and per-output comparison metrics. + +## Fixtures + +| Fixture | Role | Config highlights | +|---|---|---| +| `connaught_frequent.ini` / `connaught_extreme.ini` | correctness oracle (fast A/B) | autoATES: `forestDetrainment`, uniform α (28 / 18) | +| `bfw_10m.ini` | speed benchmark + reference cross-check | BFW: `forestFriction`, `variableAlpha`, `variableUmax`, α-layer + Umax-layer | + +Fixture `.ini`s carry absolute paths to input rasters. The rasters themselves +are **not** committed (size/licensing); edit the paths for your machine, or copy +a fixture to `*.local.ini` (gitignored). `runs/` is also gitignored. + +## Comparison metrics (per output raster, over pixels valid in both) + +`n_valid`, `n_exact`, `n_diff` (|Δ| > `--tol`), `frac_diff_ppm`, +`max_abs_diff`, `mean_abs_diff`. Use `--tol 0` for bit-exact; a small tol to +ignore IEEE-754 ULP noise from operation-order differences. diff --git a/avaframe/com4FlowPy/benchmark/REPORT.md b/avaframe/com4FlowPy/benchmark/REPORT.md new file mode 100644 index 000000000..2949b2bf5 --- /dev/null +++ b/avaframe/com4FlowPy/benchmark/REPORT.md @@ -0,0 +1,232 @@ +# com4FlowPy Numba compute-engine — validation & benchmark report + +*Living document — updated at each phase. Audience: BFW / AvaFrame maintainers.* + +## Goal + +Add an optional Numba (`@njit`) compute engine to com4FlowPy that reproduces +the existing Python engine's results while cutting per-tile runtime, so large / +repeated runs (e.g. multi-site ATES workflows) become tractable. The engine is +selected by a config flag (`engine = python | numba`, default `python`); tiling, +multiprocessing, I/O, forest, and variable-parameter handling are unchanged. + +Motivation: a prototype on standalone FlowPy (fork of `avaframe/FlowPy`, +`foreste_detrainment`) reached ~100× by rewriting the per-cell BFS as a single +`@njit` function over flat arrays. com4FlowPy already parallelizes (tiling + +`Pool` over release chunks) but still runs the inner BFS through the Python +`Cell` class — the target of this port. + +## Method + +`benchmark/flowpy_bench.py` runs com4FlowPy in-process via `com4FlowPyMain` +(the `useCustomPaths` path), recording wall-clock, peak RAM, and per-output +raster comparison against a reference directory. Fixtures under +`benchmark/fixtures/` pin exact inputs + `[GENERAL]` config. Correctness is +established by A/B comparison of the two engines on identical inputs (the +Python engine is the oracle); a run is additionally cross-checked against BFW's +own stored outputs. + +Machine: 16 physical cores / 32 threads. + +All results below were re-verified on the current rebased code +(OpenNHM/AvaFrame master @6e02e45d + this branch), with peak memory measured via +PSS. Current-code headline (8 cores): BFW 10 m 302 s → 6.7 s (**45×**); 5 m +Connaught extreme 5714 s → 193 s (**30×**). numba (float64) is bit-identical to +the Python engine on both (zDelta / fpTravelAngleMax; 5 m differs only at +flat-terrain routing knife-edges, see below). PSS memory is on par with the +Python engine (BFW: 8.6 vs 7.1 GiB; 5 m: 23.1 vs 19.9 GiB). + +## Phase 0 — baselines & oracle (stock Python engine) — DONE + +| Fixture | Domain | Config | Wall | Peak RAM | Cores | +|---|---|---|---|---|---| +| Connaught frequent | 222×222 @ 21.3 m | forestDetrainment, α=28 | 17.8 s | 6.0 GiB | 16 | +| Connaught extreme | 222×222 @ 21.3 m | forestDetrainment, α=18 | 109 s | 7.6 GiB | 16 | +| **BFW 10 m** | 482×513 @ 10 m | forestFriction, variableAlpha, variableUmax, α-layer + Umax-layer | **299 s** | 17.4 GiB | 8 | + +**Key validation:** on the BFW 10 m fixture — reproducing BFW's own run config +exactly — the stock Python engine reproduces BFW's stored `zDelta` and +`fpTravelAngleMax` **bit-for-bit** (0 differing pixels / 81,795 valid; max |Δ| = 0). +This confirms the harness, config translation, and variable-parameter wiring are +faithful, and gives a bit-exact target for the Numba engine. + +Benchmark target to beat: **299 s on 8 cores (BFW 10 m)**. + +## Phase 1 — Numba kernel — DONE (integration) + +New module `flowCoreNumba.py`: the per-release-pixel BFS as a single `@njit` +function over flat arrays, plus `calculationNumba(args)` — a drop-in for +`flowCore.calculation()` (same args, same 13-tuple). `run()` dispatches to it +when `engine=numba` (default `python`); tiling / multiprocessing / merge / I-O +unchanged. infra/back-calculation and previewMode fall back to the Python engine. + +Faithful to the `fluxDistOldVersion=False` path: float persistence (no int16 +truncation), g=9.81, forest friction/detrainment/frictionLayer with the +not-start / FSI>0 / skipForestDist guards, forestInteraction counting, variable +alpha/max_z/exponent per release pixel, default flux distribution +(count = dist≥threshold, sub-threshold redistribution, conservation correction, +deposition). + +## Phase 2 — numerical equivalence — IN PROGRESS + +A/B (numba vs Python engine), correctness fixtures — **bit-identical**: + +| Fixture | Outputs checked | Result | +|---|---|---| +| Connaught frequent | zDelta, routFluxSum, fpTravelAngleMax, cellCounts | 0 diff / 21,335 px, max\|Δ\|=0 | +| Connaught extreme | same | 0 diff / 25,528 px, max\|Δ\|=0 | + +BFW 10 m (forestFriction + variableAlpha + variableUmax), numba vs Python baseline +and vs BFW's own stored outputs: + +| Output | numba vs baseline | vs BFW stored | +|---|---|---| +| zDelta | 0 diff / 81,795 | **bit-identical** | +| fpTravelAngleMax | 0 diff / 81,795 | **bit-identical** | +| flux | 6 px (0.07 ppm), max\|Δ\|=2.2e-4 | n/a (not stored) | +| cellCounts | 20 px (0.24 ppm), max\|Δ\|=1 | n/a (not stored) | + +The scientifically primary outputs (zDelta, travel angle) are bit-exact. The +sub-ppm flux / cellCounts diffs are IEEE-754 operation-order artifacts (scalar +kernel vs numpy vectorized ops): a child flux landing within ~1 ULP of the flux +threshold is included by one engine and not the other, nudging cellCounts by 1 +and flux by a rounding-level amount at a handful of boundary pixels. This is the +same class/scale of artifact documented for the standalone-FlowPy prototype +(sub-110 ppm). It does not affect zDelta because those boundary cells take their +zDelta from other paths. + +## Phase 3 — speedup & determinism — headline result + +BFW 10 m, matched core counts: + +| Domain (8 cores) | Python | Numba | Speedup | +|---|---|---|---| +| BFW 10 m | 302 s | 6.7 s | **45×** | +| Connaught 5 m extreme | 5714 s (95 min) | 193 s | **30×** | + +On the 10 m domain numba runtime is **flat across cpu=4/8/16 (~6.5 s)** — small +enough that it is no longer compute-bound (fixed costs: cold JIT compile + tiling +I/O + merge dominate). At 5 m it is genuinely compute-bound (193 s). The realised +speedup (~30–45× here) therefore depends on domain size and how compute-bound the +run is. Because adding workers past ~8 gives little wall-clock gain (numba +saturates the tile-parallelism early) but more memory, **~8 workers is the +practical sweet spot for high-resolution runs**. + +**Determinism:** numba output is bit-identical across cpu=4/8/16 on all four +outputs (zDelta, fpTravelAngleMax, flux, cellCounts — 0 differing pixels). The +engine is fully deterministic w.r.t. worker count; the sub-ppm flux/cellCounts +differences vs the Python engine are therefore a stable scalar-vs-numpy artifact, +not a parallelism effect. + +Peak RAM (PSS) is on par with the Python engine and is dominated by com4FlowPy's +multiprocessing, not the numba workspace (the per-BFS queue was nonetheless +right-sized from `2·H·W` to a 128k grow-and-retry buffer — a correctness/hygiene +fix, not a peak-RAM lever). A warm JIT cache removes the one-time compile cost. + +## Engine comparison for BFW + +com4FlowPy exposes two engines via `engine =`: `python` (the Cell-based +reference) and `numba` (JIT, double precision). Default is `python`. + +BFW 10 m, 8 cores, vs the python reference: + +| Engine | Wall | Peak RAM | zDelta | fpTravelAngleMax | flux | cellCounts | +|---|---|---|---|---|---|---| +| python | 302 s | 7.1 GiB | ref | ref | ref | ref | +| numba | 6.7 s | 8.6 GiB | **0 diff** | **0 diff** | 6 px / 2e-4 | 20 px / 1 | + +(Wall/RAM on current master, 8 cores, PSS memory. diff = differing pixels / max\|Δ\| +over 81,795 valid px; the sub-ppm flux/cellCounts differences are the documented +scalar-vs-numpy artifact, and are bit-identical to BFW's own stored zDelta and +fpTravelAngleMax.) + +**A single-precision (float32) variant was evaluated and rejected.** It was +prototyped on the theory it would cut memory on high-resolution runs; measurement +showed otherwise — no speedup (numba32 193 s vs numba 192 s at 5 m), no memory +saving (24.9 vs 23.1 GiB PSS — if anything slightly higher, since peak RAM is +dominated by com4FlowPy's multiprocessing, not the kernel workspace), and a +precision cost that is *worst exactly at high resolution* (5 m: ~68 % of cells +differ from the Python engine, zDelta up to 62 m). It offered no benefit where it +was meant to help, so it is not part of this contribution. + +## 5 m (high-resolution, compute-bound) — Connaught extreme + +964×1025 ≈ 988k cells; the domain that ran ~1.5 h in autoATESv3.0. 8 cores, PSS memory: + +| Engine | Wall | Peak RAM | +|---|---|---| +| python | 5714 s (95 min) | 19.9 GiB | +| numba | 193 s | 23.1 GiB | + +→ **numba ~30× faster**, memory on par with the Python engine. + +* **Wall time is flat across cpu=4/8/16** — at 5 m the domain splits into only a + few tiles, so parallelism saturates early and the remainder is serial + (tiling / merge / JIT). More workers add memory, not speed — so physical cores + are the ceiling worth using (hyperthreads never help), and fewer is fine. +* Peak RAM is dominated by com4FlowPy's multiprocessing (per-worker tile copies), + not the numba kernel workspace (a modest grow-and-retry queue). On + memory-constrained machines, fewer workers and/or a smaller `tileSize` lower + the peak. Memory is a non-issue at 10–25 m (small tiles). + +**Memory (measured with PSS — proportional set size — which correctly accounts +for shared copy-on-write pages across the worker pool):** at BFW 10 m, python@8 = +8.9 GiB, numba@8 = 8.6 GiB — numba is on par with (slightly below) the Python +engine. At 5 m, numba@8 = 25 GiB. Earlier RSS-summed figures (e.g. 76 GiB at 5 m) +over-counted shared memory ~2–3× and were misleading. The per-BFS queue workspace +was separately reduced from `2·H·W` (~300× over the observed ~3.8k peak depth) to +a modest 128k with grow-and-retry on overflow — this is a correctness/robustness +fix (a path can never be silently truncated) rather than a peak-RAM lever, since +the workspace was not the dominant allocation. + +### 5 m divergence — flat-terrain routing sensitivity (a model property, not an engine bug) + +The numba engine is deterministic (identical output run-to-run and across worker +counts) and bit-for-bit identical to the Python engine on every domain tested at +10–25 m (Connaught 21 m, BFW 10 m). On the finer 5 m run the two engines differ +in a small set of cells: zDelta in 124 of 444k valid cells (0.03 %, worst case +52 m at isolated points); routFluxSum and cellCounts in more cells (~20 % and +~5 %) but by small per-cell amounts (routFluxSum ≤ 1.3, cellCounts ≤ 21). + +**Plain-English cause.** FlowPy routes flow downhill, splitting it at each cell +among the downhill neighbours according to how steep the drop is. On a real slope +there is a clear downhill and the path is well-defined. On a flat valley floor the +neighbours sit within ~1 m of each other — there is no real "downhill," so which +neighbour the flow follows comes down to the last rounding bit of the arithmetic. +And because the model must keep routing until the energy line reaches the alpha +angle (18° here), the flow keeps spreading across the flat with no genuine +directional preference. The two engines evaluate the same formulas by a slightly +different route (per-element scalar math vs numpy array math), so at these +near-ties they occasionally send flow to a different — but equally valid — +neighbour, after which the branches rejoin. + +**Neither engine is "more correct," and this is not a tiling artifact.** We +reproduced the exact divergence inside a single isolated tile +(`benchmark/repro_tile.py`), so it is not a tile-seam or merge effect — it is the +fine 5 m grid resolving flat valley floors into many near-tie cells. To show the +result is genuinely ill-conditioned *in the model itself* (independent of which +engine computes it): nudging the DEM by a physically meaningless **±1 µm** — far +below any real elevation accuracy — shifts the Python engine's *own* reached set +by ~175 cells (±1 mm → 237; ±1 cm → 189). The numba engine is equally +deterministic and equally valid; it simply lands on a different one of these +near-tied branches. Crucially, the numba↔Python difference (~50 cells along a +path) is *smaller* than the change the Python result shows under a sub-micron +nudge. So at a flat-terrain fork the exact reached set is not a numerically +determinate quantity for *any* correct implementation — both engines are valid +IEEE-754 solutions of the same equations. + +**Impact, stated plainly.** The energy-line (zDelta) footprint is essentially +identical (0.03 % of cells differ); the larger routFluxSum/cellCounts pixel counts +are small in magnitude and concentrated at the margins of flow paths in flat +runout zones. Any effect on downstream ATES classes is therefore confined to +flat-terrain margins and expected to be minor — but we flag it rather than claim +bit-identical output everywhere. This ambiguity is an inherent property of +FlowPy's alpha-angle routing in flat terrain: the port neither introduces nor +removes it, and the Python engine exhibits it too. A slope-aware stopping/ +friction criterion for low-gradient terrain would address the root cause and is +noted as separate future work. + +## Phase 4 — upstream packaging — PENDING + +numba as an optional dependency with graceful fallback; config docs; commit the +Connaught/BFW fixtures as a replicable regression test; PR to avaframe/AvaFrame. diff --git a/avaframe/com4FlowPy/benchmark/fixtures/example.ini.template b/avaframe/com4FlowPy/benchmark/fixtures/example.ini.template new file mode 100644 index 000000000..0b25cf35b --- /dev/null +++ b/avaframe/com4FlowPy/benchmark/fixtures/example.ini.template @@ -0,0 +1,24 @@ +# Example fixture for flowpy_bench.py — copy to .ini and edit paths. +# Concrete *.ini files are gitignored (they carry machine-specific data paths). +[inputs] +demPath = /path/to/DEM.tif +releasePath = /path/to/release_or_pra_binary.tif +forestPath = /path/to/forest.tif ; optional (required if forest=True) +varAlphaPath = /path/to/alpha.tif ; optional (required if variableAlpha=True) +varUmaxPath = /path/to/umax.tif ; optional (required if variableUmaxLim=True) +referenceDir = /path/to/reference_outputs ; optional; compare outputs against these tifs + +[GENERAL] +# any com4FlowPy [GENERAL] key overrides the module default +alpha = 25 +exp = 8 +flux_threshold = 3.0e-4 +max_z = 270 +forest = True +forestModule = forestFriction ; forestFriction | forestDetrainment | forestFrictionLayer +forestInteraction = True +variableAlpha = False +variableUmaxLim = False +tileSize = 15000 +tileOverlap = 5000 +outputFiles = zDelta|fpTravelAngleMax|cellCounts|flux diff --git a/avaframe/com4FlowPy/benchmark/flowpy_bench.py b/avaframe/com4FlowPy/benchmark/flowpy_bench.py new file mode 100644 index 000000000..3c34baafe --- /dev/null +++ b/avaframe/com4FlowPy/benchmark/flowpy_bench.py @@ -0,0 +1,348 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +flowpy_bench.py — reproducible runner/benchmark for com4FlowPy. + +Runs com4FlowPy in-process on a fixture, measures wall-clock and peak RAM, and +(optionally) compares every output raster against a reference directory. It is +used to: + + * freeze a correctness oracle (stock Python engine output), + * A/B the Numba engine against the Python engine (bit-equivalence), + * benchmark the speedup on a realistic domain, + * cross-check a run against BFW's own stored com4FlowPy outputs. + +The runner mirrors the ``useCustomPaths`` branch of avaframe/runCom4FlowPy.py +(getModuleConfig + com4FlowPyMain), so it exercises the exact code path a normal +com4FlowPy run uses — the only knob it adds is ``engine`` (see --engine). + +Usage +----- + python flowpy_bench.py FIXTURE.ini [--engine python|numba] + [--out DIR] [--tag NAME] [--cpu N] + +Fixture .ini format +------------------- + [inputs] + demPath = /abs/path/DEM.tif + releasePath = /abs/path/release.tif + forestPath = /abs/path/forest.tif ; optional (required if forest=True) + varAlphaPath = /abs/path/alpha.tif ; optional (required if variableAlpha=True) + varUmaxPath = /abs/path/umax.tif ; optional (required if variableUmaxLim=True) + referenceDir = /abs/path/ref_outputs ; optional; compare outputs against these tifs + + [GENERAL] + ; any com4FlowPy [GENERAL] key overrides the module default, e.g. + alpha = 26 + forest = True + forestModule = forestFriction + variableAlpha = True + ... + +Comparison metrics per output raster (valid = non-nodata in both): + n_valid, n_exact, n_diff (|Δ| > --tol), frac_diff (ppm), + max_abs_diff, mean_abs_diff. +""" +import argparse +import configparser +import json +import shutil +import sys +import threading +import time +from datetime import datetime +from pathlib import Path + +import numpy as np + +from avaframe.com4FlowPy import com4FlowPy +from avaframe.in3Utils import cfgUtils + +try: + from avaframe.runCom4FlowPy import checkOutputFilesFormat +except Exception: # pragma: no cover - older/newer AvaFrame may differ + def checkOutputFilesFormat(s): + return s + +try: + import psutil +except Exception: + psutil = None + +try: + import rasterio +except Exception: + rasterio = None + + +# --------------------------------------------------------------------------- +# peak-RAM sampler (whole process tree, since com4FlowPy forks a Pool) +# --------------------------------------------------------------------------- +class PeakRAM: + def __init__(self, interval=0.1): + self.interval = interval + self.peak_bytes = 0 + self._stop = threading.Event() + self._thread = None + + def _tree_rss(self): + # Prefer PSS (proportional set size): shared copy-on-write pages across the + # forked worker pool are divided among sharers, so summing over the process + # tree gives the true physical footprint instead of over-counting shared libs + # and pre-fork data. Falls back to RSS where PSS is unavailable (non-Linux). + if psutil is None: + return 0 + try: + proc = psutil.Process() + procs = [proc] + proc.children(recursive=True) + total = 0 + for p in procs: + try: + mi = p.memory_full_info() + total += getattr(mi, "pss", None) or mi.rss + except (psutil.NoSuchProcess, psutil.AccessDenied): + pass + return total + except Exception: + return 0 + + def _run(self): + while not self._stop.is_set(): + self.peak_bytes = max(self.peak_bytes, self._tree_rss()) + self._stop.wait(self.interval) + + def __enter__(self): + if psutil is not None: + self._thread = threading.Thread(target=self._run, daemon=True) + self._thread.start() + return self + + def __exit__(self, *exc): + self._stop.set() + if self._thread is not None: + self._thread.join(timeout=1.0) + + @property + def peak_gib(self): + return self.peak_bytes / (1024 ** 3) + + +# --------------------------------------------------------------------------- +# config assembly +# --------------------------------------------------------------------------- +def build_cfg(fixture: configparser.ConfigParser, work_dir: Path, engine: str, + cpu_override: int | None): + """Return (cfgSetup, cfgPath, uid) built from module defaults + fixture.""" + cfg = cfgUtils.getModuleConfig(com4FlowPy, onlyDefault=True, toPrint=False) + gen = cfg["GENERAL"] + + # overlay every [GENERAL] key from the fixture onto the module defaults + if fixture.has_section("GENERAL"): + for key, val in fixture.items("GENERAL"): + gen[key] = val + + # engine selector: harmless extra key for the stock code; read by the Numba port. + gen["engine"] = engine + + if cpu_override is not None: + gen["cpuCount"] = str(cpu_override) + elif not gen.get("cpuCount"): + n = psutil.cpu_count(logical=False) if psutil else 1 + gen["cpuCount"] = str(n or 1) + + # [inputs] read case-insensitively (decoupled from the case-preserving GENERAL overlay) + inp = {k.lower(): v for k, v in fixture.items("inputs")} + dem_path = Path(inp["dempath"]).expanduser() + release_path = Path(inp["releasepath"]).expanduser() + forest_path = Path(inp.get("forestpath", "") or "").expanduser() + + uid = cfgUtils.cfgHash(cfg) + res_dir = work_dir / f"res_{uid}" + if res_dir.exists(): + shutil.rmtree(res_dir, ignore_errors=True) + temp_dir = res_dir / "temp" + temp_dir.mkdir(parents=True, exist_ok=True) + + paths = cfg["PATHS"] + cfgPath = { + "workDir": work_dir, + "outDir": res_dir, + "resDir": res_dir, + "tempDir": temp_dir, + "demPath": dem_path, + "releasePath": release_path, + "relIdPath": Path(inp.get("relidpath", "") or ""), + "infraPath": Path(inp.get("infrapath", "") or ""), + "forestPath": forest_path, + "varUmaxPath": Path(inp.get("varumaxpath", "") or ""), + "varAlphaPath": Path(inp.get("varalphapath", "") or ""), + "varExponentPath": Path(inp.get("varexponentpath", "") or ""), + "deleteTemp": "False", + "outputFileFormat": paths.get("outputFileFormat", ".tif"), + "outputFiles": checkOutputFilesFormat(gen.get("outputFiles") + or paths.get("outputFiles")), + "outputNoDataValue": float(paths.get("outputNoDataValue", "-9999")), + "useCompression": paths.getboolean("useCompression", fallback=True), + "customDirs": "True", + "uid": uid, + "timeString": datetime.now().strftime("%Y%m%d_%H%M%S"), + } + return gen, cfgPath, uid + + +# --------------------------------------------------------------------------- +# raster comparison +# --------------------------------------------------------------------------- +def _read(path): + with rasterio.open(path) as ds: + arr = ds.read(1).astype(np.float64) + nod = ds.nodata + return arr, nod + + +def compare_raster(produced: Path, reference: Path, tol: float): + a, na = _read(produced) + b, nb = _read(reference) + if a.shape != b.shape: + return {"status": "shape_mismatch", + "produced_shape": list(a.shape), "reference_shape": list(b.shape)} + valid = np.ones(a.shape, dtype=bool) + for arr, nod in ((a, na), (b, nb)): + if nod is not None: + valid &= arr != nod + valid &= ~np.isnan(arr) + diff = np.abs(a - b) + over = valid & (diff > tol) + n_valid = int(valid.sum()) + return { + "status": "ok", + "n_valid": n_valid, + "n_exact": int((valid & (diff == 0)).sum()), + "n_diff": int(over.sum()), + "frac_diff_ppm": round(1e6 * over.sum() / n_valid, 3) if n_valid else None, + "max_abs_diff": float(diff[valid].max()) if n_valid else None, + "mean_abs_diff": float(diff[valid].mean()) if n_valid else None, + } + + +def find_reference(ref_dir: Path, output_name: str): + token = output_name.lower().replace("_", "") + cands = [p for p in ref_dir.glob("*.tif") + if p.stem.lower().replace("_", "").endswith(token)] + return sorted(cands)[-1] if cands else None + + +# --------------------------------------------------------------------------- +# main +# --------------------------------------------------------------------------- +def main(argv=None): + ap = argparse.ArgumentParser(description="com4FlowPy fixture runner / benchmark") + ap.add_argument("fixture", help="path to a fixture .ini") + ap.add_argument("--engine", default="python", choices=["python", "numba"], + help="compute engine: python (Cell-based) or numba " + "(JIT, float64, bit-exact). Default python.") + ap.add_argument("--out", default=None, help="output root dir (default: ./runs)") + ap.add_argument("--tag", default=None, help="label for this run") + ap.add_argument("--cpu", type=int, default=None, help="override cpuCount") + ap.add_argument("--tol", type=float, default=0.0, + help="abs tolerance for the diff count (default 0 = bit-exact)") + args = ap.parse_args(argv) + + if rasterio is None: + print("WARNING: rasterio not importable — comparison disabled", file=sys.stderr) + + fixture_path = Path(args.fixture).expanduser().resolve() + fixture = configparser.ConfigParser() + fixture.optionxform = str # preserve key case so camelCase GENERAL keys match AvaFrame's + fixture.read(fixture_path) + + tag = args.tag or fixture_path.stem + out_root = Path(args.out).expanduser() if args.out else (fixture_path.parent / "runs") + stamp = datetime.now().strftime("%Y%m%d_%H%M%S") + work_dir = out_root / f"{tag}_{args.engine}_{stamp}" + work_dir.mkdir(parents=True, exist_ok=True) + + gen, cfgPath, uid = build_cfg(fixture, work_dir, args.engine, args.cpu) + + print(f"[flowpy_bench] fixture={fixture_path.name} engine={args.engine} " + f"cpu={gen.get('cpuCount')} uid={uid}") + print(f"[flowpy_bench] alpha={gen.get('alpha')} forestModule={gen.get('forestModule')} " + f"variableAlpha={gen.get('variableAlpha')} variableUmaxLim={gen.get('variableUmaxLim')} " + f"max_z={gen.get('max_z')}") + print(f"[flowpy_bench] outputs={cfgPath['outputFiles']}") + print(f"[flowpy_bench] work_dir={work_dir}") + + t0 = time.perf_counter() + status = "success" + err = None + try: + with PeakRAM() as ram: + com4FlowPy.com4FlowPyMain(cfgPath, gen) + peak_gib = ram.peak_gib + except Exception as exc: # noqa: BLE001 + status = "failed" + err = repr(exc) + peak_gib = None + import traceback + traceback.print_exc() + wall_s = time.perf_counter() - t0 + + res_dir = cfgPath["resDir"] + produced = {} + for p in sorted(res_dir.glob("com4_*.tif")): + for name in cfgPath["outputFiles"].split("|"): + tok = name.lower().replace("_", "") + if p.stem.lower().replace("_", "").endswith(tok): + produced[name] = p + + comparisons = {} + inp_lower = {k.lower(): v for k, v in fixture.items("inputs")} if fixture.has_section("inputs") else {} + ref_dir_str = inp_lower.get("referencedir", "") + if ref_dir_str and rasterio is not None and status == "success": + ref_dir = Path(ref_dir_str).expanduser() + for name, ppath in produced.items(): + rpath = find_reference(ref_dir, name) + if rpath is not None: + comparisons[name] = {"reference": rpath.name, + **compare_raster(ppath, rpath, args.tol)} + else: + comparisons[name] = {"status": "no_reference"} + + summary = { + "fixture": str(fixture_path), + "tag": tag, + "engine": args.engine, + "uid": uid, + "status": status, + "error": err, + "wall_seconds": round(wall_s, 2), + "peak_ram_gib": round(peak_gib, 3) if peak_gib is not None else None, + "cpu_count": gen.get("cpuCount"), + "general": {k: gen.get(k) for k in ( + "alpha", "exp", "flux_threshold", "max_z", "forest", "forestModule", + "forestInteraction", "variableAlpha", "variableUmaxLim", + "maxAddedFrictionFor", "minAddedFrictionFor", "velThForFriction", + "maxDetrainmentFor", "minDetrainmentFor", "tileSize", "tileOverlap")}, + "outputs": {k: str(v) for k, v in produced.items()}, + "comparisons": comparisons, + "timestamp": stamp, + } + summary_path = work_dir / "summary.json" + summary_path.write_text(json.dumps(summary, indent=2)) + + print(f"\n[flowpy_bench] status={status} wall={wall_s:.2f}s " + f"peak_ram={summary['peak_ram_gib']} GiB") + if comparisons: + print("[flowpy_bench] comparison vs reference:") + for name, c in comparisons.items(): + if c.get("status") == "ok": + print(f" {name:20s} n_diff={c['n_diff']:>8} / {c['n_valid']:<8} " + f"({c['frac_diff_ppm']} ppm) max|Δ|={c['max_abs_diff']:.3g}") + else: + print(f" {name:20s} {c.get('status')}") + print(f"[flowpy_bench] summary → {summary_path}") + return 0 if status == "success" else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/avaframe/com4FlowPy/com4FlowPy.py b/avaframe/com4FlowPy/com4FlowPy.py index 972f10da2..af4e15891 100755 --- a/avaframe/com4FlowPy/com4FlowPy.py +++ b/avaframe/com4FlowPy/com4FlowPy.py @@ -78,6 +78,9 @@ def com4FlowPyMain(cfgPath, cfgSetup): # Flag for use of old flux distribution version modelParameters["fluxDistOldVersionBool"] = cfgSetup.getboolean("fluxDistOldVersion") + # compute engine: "python" (default, Cell-based) or "numba" (JIT kernel) + modelParameters["engine"] = cfgSetup.get("engine", "python").strip().lower() + # Tiling Parameters used for calculation of large model-domains tilingParameters = {} tilingParameters["tileSize"] = cfgSetup.getfloat("tileSize") # float(cfgSetup["tileSize"]) diff --git a/avaframe/com4FlowPy/com4FlowPyCfg.ini b/avaframe/com4FlowPy/com4FlowPyCfg.ini index 763164e3b..ca6da7c07 100644 --- a/avaframe/com4FlowPy/com4FlowPyCfg.ini +++ b/avaframe/com4FlowPy/com4FlowPyCfg.ini @@ -171,6 +171,14 @@ skipForestDist = 0 fluxDistOldVersion = False +# compute engine for the per-cell BFS. The 'numba' engine requires the optional +# 'numba' dependency (JIT-compiled kernel); it reproduces the 'python' engine's +# results and is much faster. infra/back-calculation, previewMode and relId +# outputs automatically fall back to the python engine. +# python : default, Cell-based reference implementation +# numba : JIT-compiled kernel (double precision), bit-for-bit vs 'python' +engine = python + #++++++++++++ Parameters for Tiling # tileSize: size of tiles in x and y direction in meters (if total size of) x # or y of input DEM is larger than tileSize, then the input raster diff --git a/avaframe/com4FlowPy/flowCore.py b/avaframe/com4FlowPy/flowCore.py index 434190c2c..77222cf0b 100644 --- a/avaframe/com4FlowPy/flowCore.py +++ b/avaframe/com4FlowPy/flowCore.py @@ -234,6 +234,26 @@ def run(optTuple): ) release_list = split_release(release, nChunks) + + # select compute engine: "numba" JIT kernel, else the default Python (Cell) path. + # numba does not (yet) implement infra/back-calculation, previewMode or the + # release-id (relIdPolygon/relIdCount) outputs, so fall back to the Python + # engine when any of those are active. + engine = optTuple[2].get("engine", "python") + calcFunc = calculation + if engine == "numba": + if infraBool or previewMode or relIdBool: + log.warning("engine=numba does not support infra/previewMode/relId outputs — " + "falling back to the Python engine for this run") + else: + try: + from avaframe.com4FlowPy.flowCoreNumba import calculationNumba + calcFunc = calculationNumba + log.info("Using numba compute engine") + except ImportError: + log.warning("engine=numba requested but 'numba' is not installed — " + "falling back to the Python engine") + log.info( "Multiprocessing starts, used Cores/Processes/Chunks: %i/%i/%i" % (MPOptions["nCPU"], nProcesses, nChunks) @@ -241,7 +261,7 @@ def run(optTuple): with Pool(processes=nProcesses) as pool: results = pool.map( - calculation, + calcFunc, [ [ # TODO: write in dicts: dem, diff --git a/avaframe/com4FlowPy/flowCoreNumba.py b/avaframe/com4FlowPy/flowCoreNumba.py new file mode 100644 index 000000000..6316827cf --- /dev/null +++ b/avaframe/com4FlowPy/flowCoreNumba.py @@ -0,0 +1,654 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Numba (@njit) compute engine for com4FlowPy. + +`calculationNumba(args)` is a drop-in replacement for `flowCore.calculation()`: +same argument list, same 14-element return tuple. It is selected per run via the +`engine = numba` config flag; `run()` dispatches to it instead of `calculation()` +while keeping tiling, multiprocessing, I/O and merging unchanged. + +The entire per-release-pixel BFS is expressed as a single @njit function over +flat numpy arrays (no Cell objects, no Python lists/dicts), which is where the +speedup comes from. The math is a faithful transcription of flowClass.Cell + +flowCore.calculation() for the fluxDistOldVersion=False path: + + * persistence is float (com4FlowPy tiles the DEM as float64 -> no int16 truncation), + * g = 9.81, forest friction/detrainment guards (not-start, FSI>0, skipForestDist), + * forestModule: forestFriction / forestDetrainment / forestFrictionLayer, + * forestInteraction counting, + * variable alpha / max_z / exponent resolved once per release pixel, + * default flux distribution: count = (dist >= threshold), sub-threshold mass + redistributed over >=threshold children, flux-conservation correction, and + deposition (fluxDep) when count == 0. + +Paths NOT handled here fall back to the Python engine (see run()): infra / +back-calculation and previewMode. +""" +import logging +import math + +import numpy as np +from numba import njit + +from avaframe.com4FlowPy.flowCore import get_start_idx + +log = logging.getLogger(__name__) + +_SQRT2 = math.sqrt(2.0) +_HALF_PI = math.pi / 2.0 +_DEG_PER_RAD = 180.0 / math.pi +_G = 9.81 + +# 9-neighbour layout, row-major: [0]=TL [1]=T [2]=TR / [3]=L [4]=C [5]=R / [6]=BL [7]=B [8]=BR +_DS = np.array([_SQRT2, 1.0, _SQRT2, 1.0, 0.0, 1.0, _SQRT2, 1.0, _SQRT2]) # z_alpha (center 0) +_DS_TANBETA = np.array([_SQRT2, 1.0, _SQRT2, 1.0, 1.0, 1.0, _SQRT2, 1.0, _SQRT2]) # distance (center 1) + +# forestModule codes +_FM_NONE = 0 +_FM_FRICTION = 1 +_FM_DETRAINMENT = 2 +_FM_FRICTIONLAYER = 3 + + +@njit(cache=True) +def _bfs_single(dem, forest, H, W, nodata, row_start, col_start, + cellsize, alpha, exp, flux_threshold, max_z_delta, + ds_cellsize, distance, + # forest scalars + forestBool, forestModuleCode, forestInteraction, + maxAddedFriction, minAddedFriction, noFrictionEffectZDelta, + maxDetrainment, minDetrainment, noDetrainmentEffectZDelta, + forestDetrainmentBool, frictionLayerRelative, skipForestDist, + fluxDistOldVersion, + # output arrays + zDeltaArray, fluxArray, countArray, zDeltaSumArray, zDeltaPathArray, + routFluxSumArray, depFluxSumArray, + fpMaxArray, fpMinArray, slArray, + travelMaxArray, travelMinArray, forestIntArray, + # workspace + pending_qidx, visited, + q_r, q_c, q_zdelta, q_flux, q_is_start, q_first_parent_start, + q_mindist, q_mindist3d, q_fic, q_isforest, + q_n_parents, q_pdir, q_pzd, q_pmd, q_pmd3d, + modified_r, modified_c): + """Run the BFS for one release pixel, accumulating into the output arrays.""" + if row_start < 1 or row_start >= H - 1 or col_start < 1 or col_start >= W - 1: + return 0 + for di in range(-1, 2): + for dj in range(-1, 2): + if dem[row_start + di, col_start + dj] == nodata: + return 0 + + tan_alpha_base = math.tan(math.radians(alpha)) + altitude_start = dem[row_start, col_start] + + q_head = 0 + q_tail = 0 + n_modified = 0 + + start_isforest = 1 if (forestInteraction and forest[row_start, col_start] > 0) else 0 + + q_r[0] = row_start + q_c[0] = col_start + q_zdelta[0] = 0.0 + q_flux[0] = 1.0 + q_is_start[0] = True + q_first_parent_start[0] = False + q_mindist[0] = 0.0 + q_mindist3d[0] = 0.0 + q_fic[0] = start_isforest + q_isforest[0] = start_isforest + q_n_parents[0] = 0 + pending_qidx[row_start, col_start] = 0 + modified_r[0] = row_start + modified_c[0] = col_start + n_modified = 1 + q_tail = 1 + + while q_head < q_tail: + r = q_r[q_head] + c = q_c[q_head] + q_pos = q_head + q_head += 1 + pending_qidx[r, c] = -1 + + # first-visit count (once per distinct cell reached in this BFS) + if visited[r, c] == 0: + countArray[r, c] += 1 + visited[r, c] = 1 + + z_delta = q_zdelta[q_pos] + flux = q_flux[q_pos] + is_start = q_is_start[q_pos] + first_parent_is_start = q_first_parent_start[q_pos] + n_parents = q_n_parents[q_pos] + fic = q_fic[q_pos] + isForest = q_isforest[q_pos] + + # --- 3x3 neighbourhood --- + dem_ng = np.empty(9, dtype=np.float64) + idx = 0 + for di in range(-1, 2): + for dj in range(-1, 2): + dem_ng[idx] = dem[r + di, c + dj] + idx += 1 + altitude = dem_ng[4] + FSI = forest[r, c] if forestBool else 0.0 + + # --- calcDistMin: projected (min_distance) and 3D (minDistXYZ) --- + min_distance = 0.0 + minDistXYZ = 0.0 + if not is_start: + best = 1.0e30 + best3d = 1.0e30 + for p in range(n_parents): + pdir = q_pdir[q_pos, p] + dxp = (pdir % 3) - 1 + dyp = (pdir // 3) - 1 + dd = math.sqrt((dxp * cellsize) ** 2 + (dyp * cellsize) ** 2) + q_pmd[q_pos, p] + if dd < best: + best = dd + if forestBool: + palt = dem[r + dyp, c + dxp] + dz = abs(palt - altitude) + _dy = abs(dyp) * cellsize + # NOTE: replicates flowClass.calcDistMin (calc3D) which uses dy twice + # (upstream quirk); only affects the skipForestDist gate, =0 in tuned configs. + dd3 = math.sqrt(_dy * _dy + _dy * _dy + dz * dz) + q_pmd3d[q_pos, p] + if dd3 < best3d: + best3d = dd3 + min_distance = best + if forestBool: + minDistXYZ = best3d + + # --- calc_z_delta (with forest friction) --- + applyFor = forestBool and (not is_start) and (skipForestDist < minDistXYZ) + if applyFor and (forestModuleCode == _FM_FRICTION or forestModuleCode == _FM_DETRAINMENT) and FSI > 0.0: + if z_delta < noFrictionEffectZDelta: + rest = maxAddedFriction * FSI + slope = (rest - minAddedFriction) / (0.0 - noFrictionEffectZDelta) + friction = max(minAddedFriction, slope * z_delta + rest) + alpha_calc = alpha + max(0.0, friction) + else: + alpha_calc = alpha + minAddedFriction + tan_alpha = math.tan(math.radians(alpha_calc)) + elif applyFor and forestModuleCode == _FM_FRICTIONLAYER: + alpha_for = (alpha + FSI) if frictionLayerRelative else FSI + if alpha_for < alpha: + alpha_for = alpha + tan_alpha = math.tan(math.radians(alpha_for)) + else: + tan_alpha = tan_alpha_base + + z_delta_neighbour = np.empty(9, dtype=np.float64) + for i in range(9): + val = z_delta + (altitude - dem_ng[i]) - ds_cellsize[i] * tan_alpha + if val < 0.0: + val = 0.0 + elif val > max_z_delta: + val = max_z_delta + z_delta_neighbour[i] = val + + # --- calc_persistence (float, no truncation) --- + persistence = np.zeros(9, dtype=np.float64) + no_flow = np.ones(9, dtype=np.float64) + if is_start or first_parent_is_start: + for i in range(9): + persistence[i] = 1.0 + else: + for p in range(n_parents): + pdir = q_pdir[q_pos, p] + mw = q_pzd[q_pos, p] + no_flow[pdir] = 0.0 + dx = (pdir % 3) - 1 + dy = (pdir // 3) - 1 + if dx == -1: + if dy == -1: + persistence[8] += mw + persistence[7] += 0.707 * mw + persistence[5] += 0.707 * mw + if dy == 0: + persistence[5] += mw + persistence[8] += 0.707 * mw + persistence[2] += 0.707 * mw + if dy == 1: + persistence[2] += mw + persistence[1] += 0.707 * mw + persistence[5] += 0.707 * mw + if dx == 0: + if dy == -1: + persistence[7] += mw + persistence[6] += 0.707 * mw + persistence[8] += 0.707 * mw + if dy == 1: + persistence[1] += mw + persistence[0] += 0.707 * mw + persistence[2] += 0.707 * mw + if dx == 1: + if dy == -1: + persistence[6] += mw + persistence[3] += 0.707 * mw + persistence[7] += 0.707 * mw + if dy == 0: + persistence[3] += mw + persistence[0] += 0.707 * mw + persistence[6] += 0.707 * mw + if dy == 1: + persistence[0] += mw + persistence[1] += 0.707 * mw + persistence[3] += 0.707 * mw + for i in range(9): + persistence[i] *= no_flow[i] + + # --- calc_tanbeta / r_t --- + r_t = np.zeros(9, dtype=np.float64) + tan_beta = np.zeros(9, dtype=np.float64) + for i in range(9): + if i == 4 or z_delta_neighbour[i] <= 0.0 or persistence[i] <= 0.0: + tan_beta[i] = 0.0 + else: + beta = math.atan((altitude - dem_ng[i]) / distance[i]) + _HALF_PI + tan_beta[i] = math.tan(beta / 2.0) + tb_sum = 0.0 + for i in range(9): + if tan_beta[i] > 0.0: + tb_sum += tan_beta[i] ** exp + if tb_sum > 0.0: + for i in range(9): + if tan_beta[i] > 0.0: + r_t[i] = tan_beta[i] ** exp / tb_sum + + # --- fp / sl travel angle (non-start) --- + max_gamma = 0.0 + sl_gamma = 0.0 + fluxDep = 0.0 + if not is_start: + dh = altitude_start - altitude + if min_distance > 0.0: + max_gamma = math.atan(dh / min_distance) * _DEG_PER_RAD + sl_dx = abs(col_start - c) + sl_dy = abs(row_start - r) + sl_ds = math.sqrt(sl_dx * sl_dx + sl_dy * sl_dy) * cellsize + if sl_ds > 0.0: + sl_gamma = math.atan(dh / sl_ds) * _DEG_PER_RAD + # forest detrainment reduces flux + if forestBool and forestDetrainmentBool: + rest_d = maxDetrainment * FSI + slope_d = (rest_d - minDetrainment) / (0.0 - noDetrainmentEffectZDelta) + detr = max(minDetrainment, slope_d * z_delta + rest_d) + flux = max(0.0003, flux - detr) + + # --- calc_distribution (fluxDistOldVersion=False default) --- + dist = np.zeros(9, dtype=np.float64) + rt_sum = 0.0 + for i in range(9): + rt_sum += r_t[i] + if rt_sum > 0.0: + pr_sum = 0.0 + for i in range(9): + pr_sum += persistence[i] * r_t[i] + if pr_sum > 0.0: + for i in range(9): + dist[i] = persistence[i] * r_t[i] / pr_sum * flux + + if fluxDistOldVersion: + count = 0 + for i in range(9): + if 0.0 < dist[i] < flux_threshold: + count += 1 + else: + count = 0 + for i in range(9): + if dist[i] >= flux_threshold: + count += 1 + mass_below = 0.0 + for i in range(9): + if dist[i] < flux_threshold: + mass_below += dist[i] + if mass_below > 0.0 and count > 0: + add = mass_below / count + for i in range(9): + if dist[i] >= flux_threshold: + dist[i] += add + elif dist[i] < flux_threshold: + dist[i] = 0.0 + dist_sum = 0.0 + for i in range(9): + dist_sum += dist[i] + if dist_sum != flux and count > 0: + corr = (flux - dist_sum) / count + for i in range(9): + if dist[i] >= flux_threshold: + dist[i] += corr + if count == 0: + fluxDep = flux + + # --- collect children (dist >= threshold), sort ascending (z_delta,flux,row,col) --- + ch_r = np.empty(9, dtype=np.int64) + ch_c = np.empty(9, dtype=np.int64) + ch_flux = np.empty(9, dtype=np.float64) + ch_zd = np.empty(9, dtype=np.float64) + nch = 0 + for i in range(9): + if dist[i] >= flux_threshold: + ch_r[nch] = r - 1 + i // 3 + ch_c[nch] = c - 1 + i % 3 + ch_flux[nch] = dist[i] + ch_zd[nch] = z_delta_neighbour[i] + nch += 1 + for i in range(nch - 1): + m = i + for j in range(i + 1, nch): + sw = False + if ch_zd[j] < ch_zd[m]: + sw = True + elif ch_zd[j] == ch_zd[m]: + if ch_flux[j] < ch_flux[m]: + sw = True + elif ch_flux[j] == ch_flux[m]: + if ch_r[j] < ch_r[m]: + sw = True + elif ch_r[j] == ch_r[m] and ch_c[j] < ch_c[m]: + sw = True + if sw: + m = j + if m != i: + ch_zd[i], ch_zd[m] = ch_zd[m], ch_zd[i] + ch_flux[i], ch_flux[m] = ch_flux[m], ch_flux[i] + ch_r[i], ch_r[m] = ch_r[m], ch_r[i] + ch_c[i], ch_c[m] = ch_c[m], ch_c[i] + + # --- dedup vs pending / append new children --- + for k in range(nch): + cr = ch_r[k] + cc = ch_c[k] + pq = pending_qidx[cr, cc] + dxp = c - cc + dyp = r - cr + pdir_idx = (dyp + 1) * 3 + (dxp + 1) + if pq >= 0: + q_flux[pq] += ch_flux[k] + if ch_zd[k] > q_zdelta[pq]: + q_zdelta[pq] = ch_zd[k] + np_i = q_n_parents[pq] + if np_i < q_pdir.shape[1]: + q_pdir[pq, np_i] = pdir_idx + q_pzd[pq, np_i] = z_delta + q_pmd[pq, np_i] = min_distance + q_pmd3d[pq, np_i] = minDistXYZ + q_n_parents[pq] = np_i + 1 + if forestInteraction: + child_isforest = q_isforest[pq] + if fic < (q_fic[pq] - child_isforest): + q_fic[pq] = fic + child_isforest + else: + if cr < 1 or cr >= H - 1 or cc < 1 or cc >= W - 1: + continue + nd = False + for di in range(-1, 2): + for dj in range(-1, 2): + if dem[cr + di, cc + dj] == nodata: + nd = True + if nd: + continue + if q_tail >= q_r.shape[0]: + return -1 # queue overflow -> caller grows the workspace and retries + new_pos = q_tail + q_r[new_pos] = cr + q_c[new_pos] = cc + q_zdelta[new_pos] = ch_zd[k] + q_flux[new_pos] = ch_flux[k] + q_is_start[new_pos] = False + q_first_parent_start[new_pos] = is_start + q_mindist[new_pos] = 0.0 + q_mindist3d[new_pos] = 0.0 + cif = 1 if (forestInteraction and forest[cr, cc] > 0) else 0 + q_isforest[new_pos] = cif + q_fic[new_pos] = cif + fic + q_n_parents[new_pos] = 1 + q_pdir[new_pos, 0] = pdir_idx + q_pzd[new_pos, 0] = z_delta + q_pmd[new_pos, 0] = min_distance + q_pmd3d[new_pos, 0] = minDistXYZ + pending_qidx[cr, cc] = new_pos + modified_r[n_modified] = cr + modified_c[n_modified] = cc + n_modified += 1 + q_tail += 1 + + # --- accumulate outputs for the processed cell --- + if z_delta > zDeltaArray[r, c]: + zDeltaArray[r, c] = z_delta + if flux > fluxArray[r, c]: + fluxArray[r, c] = flux + routFluxSumArray[r, c] += flux + depFluxSumArray[r, c] += fluxDep + if z_delta > zDeltaPathArray[r, c]: + zDeltaPathArray[r, c] = z_delta + if max_gamma > fpMaxArray[r, c]: + fpMaxArray[r, c] = max_gamma + if fpMinArray[r, c] >= 0.0 and max_gamma >= 0.0: + if max_gamma < fpMinArray[r, c]: + fpMinArray[r, c] = max_gamma + else: + if max_gamma > fpMinArray[r, c]: + fpMinArray[r, c] = max_gamma + if sl_gamma > slArray[r, c]: + slArray[r, c] = sl_gamma + if min_distance > travelMaxArray[r, c]: + travelMaxArray[r, c] = min_distance + if travelMinArray[r, c] >= 0.0 and min_distance >= 0.0: + if min_distance < travelMinArray[r, c]: + travelMinArray[r, c] = min_distance + else: + if min_distance > travelMinArray[r, c]: + travelMinArray[r, c] = min_distance + if forestInteraction: + if forestIntArray[r, c] >= 0.0 and fic >= 0.0: + if fic < forestIntArray[r, c]: + forestIntArray[r, c] = fic + else: + if fic > forestIntArray[r, c]: + forestIntArray[r, c] = fic + + # finalize once per distinct cell: fold this path's max zDelta into zDeltaSum, + # then reset per-BFS workspace (zDeltaPath, visited, pending) via the modified list + for i in range(n_modified): + rr = modified_r[i] + cc = modified_c[i] + if visited[rr, cc] == 1: + zDeltaSumArray[rr, cc] += zDeltaPathArray[rr, cc] + zDeltaPathArray[rr, cc] = 0.0 + visited[rr, cc] = 0 + pending_qidx[rr, cc] = -1 + + return q_tail + + +def _forest_scalars(forestBool, forestParams): + """Translate the forestParams dict into scalars/flags for the njit kernel.""" + if not forestBool or forestParams is None: + return (_FM_NONE, False, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, False, False, 0.0) + + module = forestParams["forestModule"] + if module == "forestFriction": + code = _FM_FRICTION + elif module == "forestDetrainment": + code = _FM_DETRAINMENT + elif module == "forestFrictionLayer": + code = _FM_FRICTIONLAYER + else: + code = _FM_NONE + + forestInteraction = bool(forestParams.get("forestInteraction", False)) + maxFr = float(forestParams.get("maxAddedFriction", 0.0)) + minFr = float(forestParams.get("minAddedFriction", 0.0)) + vThFr = float(forestParams.get("velThForFriction", 0.0)) + maxDe = float(forestParams.get("maxDetrainment", 0.0)) + minDe = float(forestParams.get("minDetrainment", 0.0)) + vThDe = float(forestParams.get("velThForDetrain", 0.0)) + skipForestDist = float(forestParams.get("skipForestDist", 0.0)) + fFrLayerType = forestParams.get("fFrLayerType", "absolute") + + noFrZ = (vThFr * vThFr) / (_SQRT2 * _G) if vThFr != 0.0 else 0.0 + noDeZ = (vThDe * vThDe) / (_SQRT2 * _G) if vThDe != 0.0 else 0.0 + + # matches flowClass: detrainment only for the forestDetrainment module with non-zero params + if module in ("forestFriction", "forestFrictionLayer"): + detrainBool = False + elif maxDe == 0.0 and minDe == 0.0 and vThDe == 0.0: + detrainBool = False + else: + detrainBool = True + + layerRel = (fFrLayerType == "relative") + return (code, forestInteraction, maxFr, minFr, noFrZ, maxDe, minDe, noDeZ, + detrainBool, layerRel, skipForestDist) + + +def calculationNumba(args): + """Numba drop-in for flowCore.calculation(): same args, same 14-element return. + + Processes one release-chunk (one Pool task). The per-release-pixel BFS runs in + the compiled kernel in double precision (reproducing the Python engine); + variable alpha / max_z / exponent are resolved per release pixel (constant + along each path), matching flowCore.calculation(). Output rasters are float32. + """ + dem_in = args[0] + release = args[2] + alpha0 = float(args[3]) + exp0 = float(args[4]) + flux_threshold = float(args[5]) + max_z0 = float(args[6]) + nodata = float(args[7]) + cellsize = float(args[8]) + forestBool = args[10] + varParams = args[11] + fluxDistOldVersionBool = bool(args[12]) + + varUmaxBool = varParams["varUmaxBool"] + varUmaxArray = varParams["varUmaxArray"] + varAlphaBool = varParams["varAlphaBool"] + varAlphaArray = varParams["varAlphaArray"] + varExponentBool = varParams["varExponentBool"] + varExponentArray = varParams["varExponentArray"] + + forestArray = args[14] if forestBool else None + forestParams = args[15] if forestBool else None + (fmCode, forestInteraction, maxFr, minFr, noFrZ, maxDe, minDe, noDeZ, + detrainBool, layerRel, skipForestDist) = _forest_scalars(forestBool, forestParams) + + dem = np.ascontiguousarray(dem_in, dtype=np.float64) + if forestArray is not None: + forest = np.ascontiguousarray(forestArray, dtype=np.float64) + else: + forest = np.zeros_like(dem) + H, W = dem.shape + + # release start pixels, in the same order as flowCore.calculation() + rel = release.copy() + rel[rel < 0] = 0 + rel[rel == nodata] = 0 + rel[rel > 0] = 1 + row_list, col_list = get_start_idx(dem, rel) + + ds_cellsize = (_DS * cellsize).astype(np.float64) + distance = (_DS_TANBETA * cellsize).astype(np.float64) + + # The per-BFS queue workspace starts modest and only grows if a single release + # pixel's flow genuinely needs more room; on overflow the whole chunk is re-run + # with a 4x-larger queue (grow-and-retry). This keeps memory small for typical + # runs while guaranteeing a path is never silently truncated. (Peak queue depth + # observed on a 5 m long-runout tile was ~4k, well under the 131072 start.) + MAXP = 8 + MAXQ = 1 << 17 # 131072 + while True: + # output arrays — dtypes/init identical to flowCore.calculation() + zDeltaArray = np.zeros((H, W), dtype=np.float32) + zDeltaSumArray = np.zeros((H, W), dtype=np.float32) + routFluxSumArray = np.zeros((H, W), dtype=np.float32) + depFluxSumArray = np.zeros((H, W), dtype=np.float32) + fluxArray = np.ones((H, W), dtype=np.float32) * -9999 + countArray = np.zeros((H, W), dtype=np.int32) + fpMaxArray = np.ones((H, W), dtype=np.float32) * -9999 + fpMinArray = np.ones((H, W), dtype=np.float32) * -9999 + slArray = np.ones((H, W), dtype=np.float32) * -9999 + travelMaxArray = np.ones((H, W), dtype=np.float32) * -9999 + travelMinArray = np.ones((H, W), dtype=np.float32) * -9999 + forestIntArray = np.ones((H, W), dtype=np.float32) * -9999 + zDeltaPathArray = np.zeros((H, W), dtype=np.float32) + pending_qidx = np.full((H, W), -1, dtype=np.int64) + visited = np.zeros((H, W), dtype=np.int8) + + q_r = np.empty(MAXQ, dtype=np.int64) + q_c = np.empty(MAXQ, dtype=np.int64) + q_zdelta = np.empty(MAXQ, dtype=np.float64) + q_flux = np.empty(MAXQ, dtype=np.float64) + q_is_start = np.empty(MAXQ, dtype=np.bool_) + q_first_parent_start = np.empty(MAXQ, dtype=np.bool_) + q_mindist = np.empty(MAXQ, dtype=np.float64) + q_mindist3d = np.empty(MAXQ, dtype=np.float64) + q_fic = np.empty(MAXQ, dtype=np.float64) + q_isforest = np.empty(MAXQ, dtype=np.float64) + q_n_parents = np.empty(MAXQ, dtype=np.int64) + q_pdir = np.empty((MAXQ, MAXP), dtype=np.int64) + q_pzd = np.empty((MAXQ, MAXP), dtype=np.float64) + q_pmd = np.empty((MAXQ, MAXP), dtype=np.float64) + q_pmd3d = np.empty((MAXQ, MAXP), dtype=np.float64) + modified_r = np.empty(MAXQ, dtype=np.int64) + modified_c = np.empty(MAXQ, dtype=np.int64) + + overflow = False + for k in range(len(row_list)): + rIdx = int(row_list[k]) + cIdx = int(col_list[k]) + alpha = alpha0 + max_z = max_z0 + exp = exp0 + if varUmaxBool and varUmaxArray is not None: + v = varUmaxArray[rIdx, cIdx] + if 0 < v <= 8848: + max_z = float(v) + if varAlphaBool and varAlphaArray is not None: + v = varAlphaArray[rIdx, cIdx] + if 0 < v <= 90: + alpha = float(v) + if varExponentBool and varExponentArray is not None: + v = varExponentArray[rIdx, cIdx] + if v > 0: + exp = float(v) + + qt = _bfs_single(dem, forest, H, W, nodata, rIdx, cIdx, + cellsize, alpha, exp, flux_threshold, max_z, + ds_cellsize, distance, + forestBool, fmCode, forestInteraction, + maxFr, minFr, noFrZ, maxDe, minDe, noDeZ, + detrainBool, layerRel, skipForestDist, + fluxDistOldVersionBool, + zDeltaArray, fluxArray, countArray, zDeltaSumArray, zDeltaPathArray, + routFluxSumArray, depFluxSumArray, + fpMaxArray, fpMinArray, slArray, + travelMaxArray, travelMinArray, forestIntArray, + pending_qidx, visited, + q_r, q_c, q_zdelta, q_flux, q_is_start, q_first_parent_start, + q_mindist, q_mindist3d, q_fic, q_isforest, + q_n_parents, q_pdir, q_pzd, q_pmd, q_pmd3d, + modified_r, modified_c) + if qt < 0: # queue overflow -> grow workspace and re-run the whole chunk + overflow = True + break + + if not overflow: + break + MAXQ *= 4 + + backcalc = None + # 14-element tuple matching flowCore.calculation(): res[12]=relId startcell dict + # (not produced by the numba engine — relId outputs fall back to the Python + # engine in run()), res[13]=forestInteraction array (None if not requested). + startCellIdDict = None + forestIntOut = forestIntArray if forestInteraction else None + return (zDeltaArray, fluxArray, countArray, zDeltaSumArray, backcalc, + fpMaxArray, slArray, travelMaxArray, travelMinArray, fpMinArray, + routFluxSumArray, depFluxSumArray, startCellIdDict, forestIntOut) diff --git a/pyproject.toml b/pyproject.toml index 458d80a3f..245292b7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,12 @@ dependencies = [ "fiona", ] +[project.optional-dependencies] +# Optional JIT compute engine for com4FlowPy (config: engine = numba | numba32). +# If numba is not installed, com4FlowPy transparently falls back to the pure-Python +# (Cell-based) engine, so this dependency is not required for normal operation. +numba = ["numba"] + # Setuptools [tool.setuptools] include-package-data = true From 3c2a516c46d67c6915645509b8b0ffa52f61f5b1 Mon Sep 17 00:00:00 2001 From: Jmasseysykes Date: Tue, 14 Jul 2026 08:49:17 -0800 Subject: [PATCH 2/4] test(com4FlowPy): numba-engine equivalence pytest Runs synthetic cases (no forest, forestFriction, forestDetrainment, variableAlpha+Umax) through both engines and asserts bit-identical output. Skips automatically if numba is not installed. Co-Authored-By: Claude Opus 4.8 (1M context) --- avaframe/tests/test_com4FlowPyNumba.py | 125 +++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 avaframe/tests/test_com4FlowPyNumba.py diff --git a/avaframe/tests/test_com4FlowPyNumba.py b/avaframe/tests/test_com4FlowPyNumba.py new file mode 100644 index 000000000..dd38e30c7 --- /dev/null +++ b/avaframe/tests/test_com4FlowPyNumba.py @@ -0,0 +1,125 @@ +""" +Pytest for the com4FlowPy numba compute engine (flowCoreNumba). + +Verifies that the numba engine (engine = numba) reproduces the reference Python +(Cell-based) engine, flowCore.calculation(), bit-for-bit on consistently-sloped +synthetic terrain (no flat-terrain routing ties, which are numerically +knife-edge in both engines). +""" +import numpy as np +import pytest + +import avaframe.com4FlowPy.flowCore as flowCore + +# the whole module is skipped if the optional 'numba' dependency is absent +pytest.importorskip("numba") +from avaframe.com4FlowPy import flowCoreNumba # noqa: E402 + + +def _make_dem(ny=30, nx=30): + """A consistently down-sloping DEM (downhill in +y) with gentle cross-valley + curvature and strictly varying values, so flow routing has no flat-terrain + ties (which are numerically knife-edge in both engines).""" + yy, xx = np.mgrid[0:ny, 0:nx].astype(np.float64) + return 500.0 - 7.0 * yy + 0.15 * (xx - nx / 2.0) ** 2 + 0.013 * xx + + +def _default_var(): + return { + "varUmaxBool": False, "varUmaxArray": None, + "varAlphaBool": False, "varAlphaArray": None, + "varExponentBool": False, "varExponentArray": None, + } + + +def _args(dem, pra, *, alpha=25.0, forestBool=False, forestParams=None, + forestArray=None, varParams=None, outputs=None): + if varParams is None: + varParams = _default_var() + if outputs is None: + outputs = ["zDelta", "flux", "cellCounts", "zDeltaSum", "fpTravelAngleMax", + "fpTravelAngleMin", "slTravelAngle", "travelLengthMax", + "travelLengthMin", "routFluxSum", "depFluxSum"] + relOutputParams = {"relIdBool": False, "relIdArray": None, + "relVolBool": False, "relVolArray": None} + return [dem, None, pra, alpha, 8, 3e-4, 270, -9999, 10.0, False, forestBool, + varParams, False, False, forestArray, forestParams, outputs, relOutputParams] + + +# comparable result indices (4=backcalc and 12=relId are None on this path; +# 13=forestInteraction is compared separately when active) +_IDX = {0: "zDelta", 1: "flux", 2: "cellCounts", 3: "zDeltaSum", + 5: "fpTravelAngleMax", 6: "slTravelAngle", 7: "travelLengthMax", + 8: "travelLengthMin", 9: "fpTravelAngleMin", 10: "routFluxSum", + 11: "depFluxSum"} + + +def _compare(py, nb): + assert len(nb) == len(py) == 14 + for i, name in _IDX.items(): + a = np.asarray(py[i], dtype=np.float64) + b = np.asarray(nb[i], dtype=np.float64) + assert np.array_equal(a, b), \ + f"{name}: {int((a != b).sum())} cells differ, max|Δ|={np.abs(a - b).max()}" + + +def _pra(dem): + pra = np.zeros_like(dem, dtype=np.int32) + pra[1, dem.shape[1] // 2] = 1 # single release near the top of the slope + return pra + + +def test_numba_matches_python_noforest(): + dem = _make_dem() + args = _args(dem, _pra(dem)) + _compare(flowCore.calculation(args), flowCoreNumba.calculationNumba(args)) + + +def test_numba_matches_python_forestFriction(): + dem = _make_dem() + forest = np.zeros_like(dem) + forest[10:20, :] = 0.6 + fp = {"forestModule": "forestFriction", "maxAddedFriction": 20.0, + "minAddedFriction": 2.0, "velThForFriction": 30.0, "maxDetrainment": 0.0, + "minDetrainment": 0.0, "velThForDetrain": 0.0, "fFrLayerType": "absolute", + "skipForestDist": 0.0, "forestInteraction": True} + args = _args(dem, _pra(dem), forestBool=True, forestParams=fp, forestArray=forest) + py = flowCore.calculation(args) + nb = flowCoreNumba.calculationNumba(args) + _compare(py, nb) + assert np.array_equal(np.asarray(py[13], float), np.asarray(nb[13], float)), \ + "forestInteraction array mismatch" + + +def test_numba_matches_python_forestDetrainment(): + dem = _make_dem() + forest = np.zeros_like(dem) + forest[8:22, :] = 0.5 + fp = {"forestModule": "forestDetrainment", "maxAddedFriction": 52.0, + "minAddedFriction": 5.0, "velThForFriction": 270.0, "maxDetrainment": 0.003, + "minDetrainment": 0.00001, "velThForDetrain": 270.0, "fFrLayerType": "absolute", + "skipForestDist": 0.0, "forestInteraction": True} + args = _args(dem, _pra(dem), forestBool=True, forestParams=fp, forestArray=forest) + py = flowCore.calculation(args) + nb = flowCoreNumba.calculationNumba(args) + _compare(py, nb) + assert np.array_equal(np.asarray(py[13], float), np.asarray(nb[13], float)) + + +def test_numba_matches_python_variableAlphaUmax(): + dem = _make_dem() + var = _default_var() + var["varAlphaBool"] = True + var["varAlphaArray"] = np.full_like(dem, 22.0) # per-cell alpha (deg) + var["varUmaxBool"] = True + var["varUmaxArray"] = np.full_like(dem, 150.0) # per-cell zDeltaLim (m) + args = _args(dem, _pra(dem), varParams=var) + _compare(flowCore.calculation(args), flowCoreNumba.calculationNumba(args)) + + +if __name__ == "__main__": + test_numba_matches_python_noforest() + test_numba_matches_python_forestFriction() + test_numba_matches_python_forestDetrainment() + test_numba_matches_python_variableAlphaUmax() + print("all numba-engine equivalence tests passed") From a72493776da669e4bb49bbe59cdfd02a00067425 Mon Sep 17 00:00:00 2001 From: ahuber-bfw Date: Tue, 18 Aug 2026 17:17:16 +0200 Subject: [PATCH 3/4] minor maintainer edits to PR loggin python engine analog to numba, small changes to runStandardTestsCom4 for numba added 3 benchmark tests for numba engine, will be performed along with existing com4 standardTests minor refactor of code in runStandardTestsCom4 --- avaframe/com4FlowPy/com4FlowPy.py | 14 +- avaframe/com4FlowPy/flowCore.py | 2 + avaframe/runStandardTestsCom4FlowPy.py | 44 ++- .../ArzlerAlmForestNumba_com4FlowPyCfg.ini | 308 +++++++++++++++++ .../com4_avaArzlerAlmForest_desDict.json | 10 + ..._b44fd62ee8_20260812_164438_cellCounts.tif | Bin 0 -> 47776 bytes ...62ee8_20260812_164438_fpTravelAngleMax.tif | Bin 0 -> 56004 bytes ...d62ee8_20260812_164438_travelLengthMax.tif | Bin 0 -> 54147 bytes ...com4_b44fd62ee8_20260812_164438_zdelta.tif | Bin 0 -> 57815 bytes .../ArzlerAlmNullNumba_com4FlowPyCfg.ini | 318 ++++++++++++++++++ ..._03f0497040_20260812_165012_cellCounts.tif | Bin 0 -> 48363 bytes .../com4_03f0497040_20260812_165012_flux.tif | Bin 0 -> 59542 bytes ...97040_20260812_165012_fpTravelAngleMax.tif | Bin 0 -> 57069 bytes ...497040_20260812_165012_travelLengthMax.tif | Bin 0 -> 55068 bytes ...com4_03f0497040_20260812_165012_zdelta.tif | Bin 0 -> 59112 bytes .../com4_avaArzlerAlmNullNumba_desDict.json | 10 + ...olaChannelPaperNullNumba_com4FlowPyCfg.ini | 318 ++++++++++++++++++ ..._avaParabolaChannelPaperNumba_desDict.json | 10 + ..._ff6612bd19_20260103_135958_cellCounts.tif | Bin 0 -> 34591 bytes .../com4_ff6612bd19_20260103_135958_flux.tif | Bin 0 -> 49768 bytes ...2bd19_20260103_135958_fpTravelAngleMax.tif | Bin 0 -> 47383 bytes ...12bd19_20260103_135958_travelLengthMax.tif | Bin 0 -> 44030 bytes ...com4_ff6612bd19_20260103_135958_zdelta.tif | Bin 0 -> 49484 bytes pyproject.toml | 6 + 24 files changed, 1030 insertions(+), 10 deletions(-) create mode 100644 benchmarks/com4_avaArzlerAlmForestNumba/ArzlerAlmForestNumba_com4FlowPyCfg.ini create mode 100644 benchmarks/com4_avaArzlerAlmForestNumba/com4_avaArzlerAlmForest_desDict.json create mode 100644 benchmarks/com4_avaArzlerAlmForestNumba/com4_b44fd62ee8_20260812_164438_cellCounts.tif create mode 100644 benchmarks/com4_avaArzlerAlmForestNumba/com4_b44fd62ee8_20260812_164438_fpTravelAngleMax.tif create mode 100644 benchmarks/com4_avaArzlerAlmForestNumba/com4_b44fd62ee8_20260812_164438_travelLengthMax.tif create mode 100644 benchmarks/com4_avaArzlerAlmForestNumba/com4_b44fd62ee8_20260812_164438_zdelta.tif create mode 100644 benchmarks/com4_avaArzlerAlmNullNumba/ArzlerAlmNullNumba_com4FlowPyCfg.ini create mode 100644 benchmarks/com4_avaArzlerAlmNullNumba/com4_03f0497040_20260812_165012_cellCounts.tif create mode 100644 benchmarks/com4_avaArzlerAlmNullNumba/com4_03f0497040_20260812_165012_flux.tif create mode 100644 benchmarks/com4_avaArzlerAlmNullNumba/com4_03f0497040_20260812_165012_fpTravelAngleMax.tif create mode 100644 benchmarks/com4_avaArzlerAlmNullNumba/com4_03f0497040_20260812_165012_travelLengthMax.tif create mode 100644 benchmarks/com4_avaArzlerAlmNullNumba/com4_03f0497040_20260812_165012_zdelta.tif create mode 100644 benchmarks/com4_avaArzlerAlmNullNumba/com4_avaArzlerAlmNullNumba_desDict.json create mode 100644 benchmarks/com4_avaParabolaChannelPaperNullNumba/avaParabolaChannelPaperNullNumba_com4FlowPyCfg.ini create mode 100644 benchmarks/com4_avaParabolaChannelPaperNullNumba/com4_avaParabolaChannelPaperNumba_desDict.json create mode 100644 benchmarks/com4_avaParabolaChannelPaperNullNumba/com4_ff6612bd19_20260103_135958_cellCounts.tif create mode 100644 benchmarks/com4_avaParabolaChannelPaperNullNumba/com4_ff6612bd19_20260103_135958_flux.tif create mode 100644 benchmarks/com4_avaParabolaChannelPaperNullNumba/com4_ff6612bd19_20260103_135958_fpTravelAngleMax.tif create mode 100644 benchmarks/com4_avaParabolaChannelPaperNullNumba/com4_ff6612bd19_20260103_135958_travelLengthMax.tif create mode 100644 benchmarks/com4_avaParabolaChannelPaperNullNumba/com4_ff6612bd19_20260103_135958_zdelta.tif diff --git a/avaframe/com4FlowPy/com4FlowPy.py b/avaframe/com4FlowPy/com4FlowPy.py index af4e15891..42b088331 100755 --- a/avaframe/com4FlowPy/com4FlowPy.py +++ b/avaframe/com4FlowPy/com4FlowPy.py @@ -392,7 +392,9 @@ def checkInputLayerDimensions(modelParameters, modelPaths): def checkInputParameterValues(modelParameters, modelPaths): - """check if the input parameters alpha, uMaxLimit/ zDeltaMaxLimit, exponent + """check if the input parameters + are valid and within physically sensible limits + alpha, uMaxLimit/ zDeltaMaxLimit, exponent are within a physically sensible range Parameters @@ -402,6 +404,16 @@ def checkInputParameterValues(modelParameters, modelPaths): modelPaths: dict contains paths to input files """ + + engine = modelParameters["engine"] + validEngines = {"python","numba"} + + if engine not in validEngines: + raise ValueError( + f"Invalid engine '{engine}'. " + f"Valid engine options are {sorted(validEngines)}" + ) + alpha = modelParameters["alpha"] if alpha < 0 or alpha > 90: log.error("Error: Alpha value is not within a physically sensible range ([0,90]).") diff --git a/avaframe/com4FlowPy/flowCore.py b/avaframe/com4FlowPy/flowCore.py index 77222cf0b..99d0c9ae8 100644 --- a/avaframe/com4FlowPy/flowCore.py +++ b/avaframe/com4FlowPy/flowCore.py @@ -253,6 +253,8 @@ def run(optTuple): except ImportError: log.warning("engine=numba requested but 'numba' is not installed — " "falling back to the Python engine") + else: + log.info("Using python compute engine") log.info( "Multiprocessing starts, used Cores/Processes/Chunks: %i/%i/%i" diff --git a/avaframe/runStandardTestsCom4FlowPy.py b/avaframe/runStandardTestsCom4FlowPy.py index 6edebc504..93f842c96 100644 --- a/avaframe/runStandardTestsCom4FlowPy.py +++ b/avaframe/runStandardTestsCom4FlowPy.py @@ -22,6 +22,13 @@ from avaframe.in3Utils import logUtils import avaframe.in2Trans.rasterUtils as rasterUtils +def _checkNumbaInstalled() -> bool: + try: + import numba + return True + except ImportError: + return False + def compareRasters(path, pathRef): """ @@ -80,9 +87,18 @@ def main(): # filter benchmarks for tag standardTest filterType = 'TAGS' + valuesList = ['standardTest', 'com4FlowPy'] # looking for 'com4FlowPy' and 'standardTest' in TAGS list - testList = tU.filterBenchmarks(testDictList, filterType, valuesList, condition='and') + testListAll = tU.filterBenchmarks(testDictList, filterType, valuesList, condition='and') + + valuesList = ['standardTest', 'com4FlowPy', 'numba'] + testListNumba = tU.filterBenchmarks(testDictList, filterType, valuesList, condition='and') + + if _checkNumbaInstalled(): + testList = testListAll + else: + testList = [item for item in testListAll if item not in testListNumba] # Set directory for full standard test report outDir = _avaframeDir / 'tests' / 'reportsCom4FlowPy' @@ -93,13 +109,16 @@ def main(): _startDate = datetime.now() with open(reportFile, 'w') as pfile: - # Write header pfile.write('# Standard Tests Report \n\n') pfile.write('Comparing __com4FlowPy__ simulations to selected benchmark results \n\n') - pfile.write(f'__tests started__ : {_startDate}\n\n') + pfile.write('* * * \n') + if _checkNumbaInstalled(): + pfile.write('`numba` __found__: running all tests for python and numba engines ✓') + else: + pfile.write('`numba` __NOT found__: skipping tests for numba engine ✗') + pfile.write('\n* * * \n') - log = logUtils.initiateLogger(outDir, logName) log.info('The following benchmark tests will be fetched ') @@ -108,9 +127,10 @@ def main(): for test in testList: pfile.write(f"- {test['NAME']}\n") log.info('%s' % test['NAME']) - pfile.write('\n* * * \n') + pfile.write('\n') + pfile.write(f'__tests started__ : {_startDate}\n\n') + pfile.write('* * * \n') - # create a temporary directory, where the outputs of all standard Tests are stored # clean-up is automatic - this way we don't pollute the avaframe/data/ directory with tempfile.TemporaryDirectory(prefix="avaframe_stdTests_") as tempDir: @@ -145,14 +165,16 @@ def main(): pfile.write("|Model Output|Result of comparison|status\n") pfile.write("|----:|:-----:|:---:|\n") - avaDir = test['AVADIR'] + # define avaDir relative to _avaframeDir to allow execution of this script from different locations + # not just AvaFrame/avaframe directory + avaDir = str( _avaframeDir / pathlib.Path(test['AVADIR']) ) cfgMain['MAIN']['avalancheDir'] = avaDir # Fetch benchmark test info refDir = pathlib.Path(_avaframeDir, '..', 'benchmarks', test['NAME']) # Clean input directory(ies) of old work and output files - initProj.cleanSingleAvaDir(avaDir, deleteOutput=False) + initProj.cleanSingleAvaDir(_avaframeDir / avaDir, deleteOutput=False) # Load input parameters from configuration file for standard tests benchmarkCfg = refDir / ('%s' % test['INI']) @@ -165,7 +187,11 @@ def main(): avalancheDir = cfgMain["MAIN"]["avalancheDir"] cfgPath = readFlowPyinputs(avalancheDir, cfg, log) - compDir = tmpTestsDir / pathlib.Path(avalancheDir) + # for the temporary output folder we cannot use the full path, but just the relative part of the path + # following the _avaframeDir path + avaDirTempOutPut = test['AVADIR'] + # compDir = output location of outputs generated for each test within the temporary directory + compDir = tmpTestsDir / pathlib.Path(avaDirTempOutPut) cfgPath["customDirs"] = False cfgPath["resDir"] = compDir diff --git a/benchmarks/com4_avaArzlerAlmForestNumba/ArzlerAlmForestNumba_com4FlowPyCfg.ini b/benchmarks/com4_avaArzlerAlmForestNumba/ArzlerAlmForestNumba_com4FlowPyCfg.ini new file mode 100644 index 000000000..4ef7a7186 --- /dev/null +++ b/benchmarks/com4_avaArzlerAlmForestNumba/ArzlerAlmForestNumba_com4FlowPyCfg.ini @@ -0,0 +1,308 @@ +### Config File - This file contains the main settings for the com4FlowPy run +## Set your parameters +# This file will be overridden by local_com4FlowPyCfg.ini if it exists +# So copy this file to local_com4FlowPyCfg.ini, adjust your variables there + +# Optional settings------------------------------- +[GENERAL] +#++++++++++++ Flow-Py Model Parameters +# alpha: +# Angle-of-reach Alpha [°] - defines max. longitudinal runout limit +# equivalent to a Coulomb-friction of tan(alpha) in a sliding block model +#--------------------- +# exp: +# Spreading Coefficient (influences lateral spreading) +#--------------------- +# flux_threshold: +# Flux threshold (influences lateral spreading) +#--------------------- +# max_z: +# Energy-Line-Height Limit (can be interpreted as velocity limit) +# max_v = sqrt(max_z*19.62) +# typical values: +# - Avalanche: ~270 m ... ~72 m/s +# - Rockfall: ~130 m ... ~50 m/s +# - Debris-flow: ~12 m ... ~15 m/s +#--------------------- + +alpha = 25 +exp = 8 +flux_threshold = 3.0e-4 +max_z = 8848 + +#++++++++++++ Use Infrastructure + +infra = False + +#++++++++++++ preview Mode +# if previewMode = True, not every release cell is processend independently +# if a releaseCell is already "hit"/"affected" by a prior calculated path +# then the processing of this release cell is skipped +# can be used for a faster preview of model results (e.g. for checking of input parameters) or +# to save calculation time (e.g. when calculating with infrastructure) +# NOTE: results will deviate from "normal" model run and all outputLayers relying on the +# summed/combined output of different paths will not provide sensible values!!! + +previewMode = False + + +#++++++++++++ Use a dynamic u_max Limit +# Requires an additional tif-file containing the uMax (in m/s) +# or zDeltaMax (m) Limits +# in every cell where a release cell is. +# the paths for each release cells are calculated with these uMax values +# (computed to z_delta, similar to the max_z) or zDelta values. +# In varUmaxParameter the parameter (uMax in m/s or zDeltaMax in m) +# provided is given. +variableUmaxLim = False +varUmaxParameter = uMax + +#++++++++++++ Use a dynamic alpha angle +# Requires an additional tif-file containing the alpha angles +# in every cell where a release cell is. +# The paths for each release cells are calculated with these alpha angles +variableAlpha = False + +#++++++++++++ Use a dynamic exponent +# Requires an additional tif-file containing the exponents +# in every cell where a release cell is. +# The paths for each release cells are calculated with these exponent +variableExponent = False + +#++++++++++++ Use Forest Information +# NOTE AH 20240408: Dummy settings for Forest Interaction +# The forest implementation should mimick/reproduce the +# one from the "foreste_detrainment" branch in the avaframe/FlowPy repo +# which is partly described in D'Amboise et al. (2021) +# +# Forest-Interaction is only used if 'forest' is set to 'True' +#++++++++++++ +forest = True + +#++++++++++++ +# Type of Forest-Interaction Model to use (only works in conjunction with forest = True!) +# valid choices: +# +# * 'forestFriction' (D'Amboise et al., 2021) - use added friction on forest Pixels +# forest-Raster (set in 'forestPath') is assumed to have values between +# 0 (no Forest) and 1 (full protective function). +# model behavior is governed by 'maxAddedFrictionFor', 'minAddedFrictionFor' +# and 'velThForFriction' +# +# * 'forestDetrainment' (D'Amboise et al., 2022?) - added friction and detrainment on forested Pixels +# forest-Raster (set in 'forestPath') is assumed to have values between +# 0 (no Forest) and 1 (full protective function). +# model behavior is governed by 'maxAddedFrictionFor', 'minAddedFrictionFor' +# and 'velThForFriction', 'maxDetrainmentFor', 'minDetrainmentFor', 'velThForDetrain' +# +# * 'forestFrictionLayer' - provide Layer with local alpha / local delta-Alpha on forested pixels / for all pixels in +# the domain +# forest-Raster (set in 'forestPath') is assumed to have values between 0° (no friction) and 90° (inf. friciton) +# forest-Raster is either interpreted as 'absolute' or 'difference' +# 'absolute' - absolute 'alpha' values are used on forested pixels (all pixels with values > 0) +# 'relative' - 'alpha' values are interpreted as additional friction on forested pixels (all pixels with +# values > 0) +#++++++++++++ + +# ['forestFriction', 'forestDetrainment', 'forestFrictionLayer'] +forestModule = forestFriction +# ForestInteraction gives an additional output layer for the amount of forested cells +# a path ran through +# (now implemented, only when forest = True) -> TODO: we dont require a forestModule to compute +# for ForestInteraction Layer, but we need to read in the Forest - Layer (requires forest = True) +forestInteraction = False + +#++++++++++++ Forest added Friction +# These are the parameters for the "add friction on forested cells"-approach +# described in D'Amboise et al. (2021) +# +# maxAddedFrictionFor [°]: maximum increase of Alpha angle/basal friction on forested cells +# minAddedFrictionFor [°]: minimum increase of Alpha angle/basal friction on forested cells +# velThForFriction [m/s]: velocity threshold for forest-friction effect +#++++++++++++ + +maxAddedFrictionFor = 10 +minAddedFrictionFor = 2 +velThForFriction = 30 + +#++++++++++++ Forest Detrainment +# These are the parameters for the "forestDetrainment"-approach - D'Amboise et al. (2022??) +# The idea is to remove 'virtual mass' (aka 'flux' in Flow-Py) from the modeled flow/process +# on forested pixels in dependency of FSI and local z_delta +# +# maxDetrainmentFor [°]: default set to '0' (i.e. no detrainment) - foreste_detrainment val: +# minDetrainmentFor [°]: default set to '0' (i.e. no detrainment) - foreste_detrainment val: +# velThForDetrain [m/s]: velocity threshold for forest-detraiment effect # default set to '0' (i.e. no detrainment) - foreste_detrainment val: +#++++++++++++ + +maxDetrainmentFor = 0 +minDetrainmentFor = 0 +velThForDetrain = 0 + +#++++++++++++ ForestFrictionLayer +# The forest layer defines the alpha angle in forested areas. +# If forestFrictionLayerType = relative, forest layer values are added to the global alpha angle. +# e.g. if alpha = 24° and value on forest cell is 5°, then effective local alpha of 24° + 5° = 29° will be used. +# ------------------- +# If forestFrictionLayerType = absolute, forest layer values are used as alpha angle, +# if they are larger than the global alpha angle. +# e.g. if global alpha = 24° and value provided in the forestFrictionLayer on a cell is 35°, +# then local alpha of 35° will be used. +#++++++++++++ + +# ['absolute', 'relative'] +forestFrictionLayerType = absolute + +# skip Forest Effect (added forest friction) for first x meters (calculated in 3D - XYZ) +# should allow an initial acceleration phase of processes starting in or directly above +# dense forests (these would in many cases stop otherwise) +# if e.g. skipForestDist = 40, no added forestFriction will be assumed until 40 m 3D-distance +# along the path from the startCell. +skipForestDist = 0 + +#++++++++++++ Method to calculate flux distribution +# We fixed a bug in flowClass.py, which affects the distribution of the remaining flux, +# if a cell receives flux smaller than the provided flux_threshold. +# +# The default now (post Jan. 2025) is a calculation with the fixed bug! +# +# For backward compatibility the old version (prior to Jan. 2025 - with minor bug) can +# be switched on by setting "fluxDistOldVersion = True". + +fluxDistOldVersion = False + +# compute engine for the per-cell BFS. The 'numba' engine requires the optional +# 'numba' dependency (JIT-compiled kernel); it reproduces the 'python' engine's +# results and is much faster. infra/back-calculation, previewMode and relId +# outputs automatically fall back to the python engine. +# python : default, Cell-based reference implementation +# numba : JIT-compiled kernel (double precision), bit-for-bit vs 'python' +engine = numba + +#++++++++++++ Parameters for Tiling +# tileSize: size of tiles in x and y direction in meters (if total size of) x +# or y of input DEM is larger than tileSize, then the input raster +# layers are tiles +# tileOverlap: overlap between single tiles in m (5 km is rather conservative) +#--------------------------------------- + +tileSize = 15000 +tileOverlap = 5000 + +#++++++++++++ Parameters for CPU usage/multiprocessing +# Recommended to leave at default values unless performance has to be +# tweaked/optimized (e.g. for application on dedicated machines, maxing out +# computational resources) +# +# procPerCPUCore: number of processer per core for multiprocessing +# default value = 1 (it is recommended not to change this value +# unless you want to max. out CPUs, which might come at the cost +# of RAM comsumption!!!) +# chunkSize: tbd. +# maxChunks: Maximum number of single tasks for multiprocessing, if set too high +# this can max out RAM and lead to unexpected behavior (in this case +# probably an infinite loop --> you see it in the .log if this happens) +# On machines with a lot of CPUs and RAM this value might be set higher ... +#++++++++++++ +procPerCPUCore = 1 +chunkSize = 50 +maxChunks = 500 + +# Optional Custom Paths +[PATHS] + +# define format of output raster files (default = .tif) +# available options: [.tif, .asc] +# if you plan to utilize avaFrame tools to analyse result rasters +# then you should choose '.asc' here! +outputFileFormat = .tif + +# use LZW compression when writing tif raster files +useCompression = True + +# define noData value that is assigned in output rasters in cells that are not affected by the process (default: -9999) +# when changing this value BE AWARE that noData values can have same values as a affected cells +# (e.g., when using outputNoDataValue = 0) +outputNoDataValue = -9999 + +# define the different output files that are written to disk +# default = 'zDelta|cellCounts|travelLengthMax|fpTravelAngleMax' +# additional options: +# slTravelAngle +# flux +# zDeltaSum +# routFluxSum +# depFluxSum +# travelLengthMin +# fpTravelAngleMin +# relIdPolygon +# relIdCount +# if forestInteraction: forestInteraction is automatically added to outputs +# if infra: backCalculation is automatically added to output +# if relVolMin or relVolMax is in outputFiles, the Volume of the PRA should be provided in the raster file in the REL folder +# if relIdCount or relIdPolygon is in outputFiles, the ids of the PRAs should be provided in the raster file in the RELID folder +outputFiles = zDelta|cellCounts|travelLengthMax|fpTravelAngleMax + +# whether simulation results with the same simHash of the running simulation already exists +# AND the existing resultsFolder already contains valid com4FlowPy outputs. +# 1) overwriteResults = default ... does not re-run a simulation if results folder (res_) and .json +# -- if there are remnants from a previously attempted but not succesfully finished simulation (e.g. existing res_ and/or .json) +# --> delete existing results folder (res_) and .json and run Simulation +# 2) overwriteResults = reRunAndOverwrite ... deletes existing results folder (res_) and .json and runs Simulation +# 3) overwriteResults = reRunAndBackup ... moves existing result folder (res_) and .json to a dedicated backup folder (e.g. in /BACKUP) + +overwriteResults = reRunAndOverwrite + +#++++++++++++ Custom paths True/False +# default: False +# if set to 'False': +# the default AvaFrame Path/Directory structure is used +# i.e. the Work- and Result-Folders and inputFiles are taken from cfgMain and +# Input-Files are expected in AvaFrame directory structure +# if set to 'True': +# workDir, demPath, releasePath and [infraPath <-> if 'infra'==True] +# can be set here +# +#++++++++++++ +useCustomPaths = False +# for now only works with 'useCustomPaths=True'; if 'True' the temp folder is +# deleted after the output-files have been successfully written to disk +deleteTempFolder = False + +# if useCustomPathDEM set to True: only DEM path can set here, the other input data are used from +# avalancheDir (AvaFrame folder structure) (works only if useCustomPaths = False) +useCustomPathDEM = False + +# if 'useCustomPaths = True', then these input and output paths are used +# the output Folder is automatically placed inside the 'workDir' +workDir = +demPath = +releasePath = +relIdPath = +infraPath = +forestPath = +varUmaxPath = +varAlphaPath = +varExponentPath = + +# plot save results flags------------------------ +# NOTE-TODO: These Flags still don't do anything, +# would be cool to have full integration for +# com4FlowPy with avaframe post-processing utils ... +[FLAGS] +# Plot the avalanche path and DEM figure +plotPath = False + +# Plot the avalanche profile with alpha beta points +plotProfile = False + +# Save the profile figure +saveProfile = True + +# Write results to txt file +writeRes = True + +# keep intermediate results +fullOut = False + +#---------------------------------------------------------- diff --git a/benchmarks/com4_avaArzlerAlmForestNumba/com4_avaArzlerAlmForest_desDict.json b/benchmarks/com4_avaArzlerAlmForestNumba/com4_avaArzlerAlmForest_desDict.json new file mode 100644 index 000000000..6262c68b3 --- /dev/null +++ b/benchmarks/com4_avaArzlerAlmForestNumba/com4_avaArzlerAlmForest_desDict.json @@ -0,0 +1,10 @@ +{ + "INI": "ArzlerAlmForestNumba_com4FlowPyCfg.ini", + "TAGS": ["forest", "infra", "standardTest", "real", "com4FlowPy", "numba"], + "DESCRIPTION": "Arzler Alm Test Case\ncom4FlowPy run with forest friction extension\ntest compares numba engine results against python engine results", + "REFERENCE": "https://github.com/OpenNHM/AvaFrameData/tree/main/avaArzl", + "FILES": ["zdelta", "cellCounts", "fpTravelAngleMax", "travelLengthMax"], + "TOPOTYPE": "real", + "AVADIR": "data/avaArzlerAlm", + "BENCHMARKED_AVAFRAME_VERSION": "2.1" +} diff --git a/benchmarks/com4_avaArzlerAlmForestNumba/com4_b44fd62ee8_20260812_164438_cellCounts.tif b/benchmarks/com4_avaArzlerAlmForestNumba/com4_b44fd62ee8_20260812_164438_cellCounts.tif new file mode 100644 index 0000000000000000000000000000000000000000..7b691d33103c352b9b590eab6e6a9b397e2d226b GIT binary patch literal 47776 zcmeFacT`i`);_!;LBP-xnsfmX2_$ssU215d7ZCwb0qG(FDhf70KtL>DIf5Le6N>cS z6G90fT?Iv@cclIn-t*pjzxVxp;~V20+j6@4z5)Fy{v^T5yNq*WK5E zdEp&uNEsa81M|oX>R;!dJh)NtNCP+dzm<>RS{?AdE&xdek_F@`kcdCzCKywIm;u29 zQ3ApMgc%4M5Dp-$K)8S$1tJE73CO|E_rYt^fpHOxn?Mdm2t0=YIXGV!$T1)&Ac8;+ zjvrh@4MsSSZ7}}{w698 zzzqnxbralY6a;;|4MEd)!S9cOAmcc2fAJ91ng~I)Ne~p10zoHJ!Tmo5zyB%FnKTIc zJspB{Ux4dgLQwc`V4X4`s5ldX%(Eb9_IGf8o+*bk6$s(|>#b7N< zAn1G<(1UVtf4?3gn*Y!7AJ6{Z{>R=}`E~xc7mQl}bq>`pi+}_#3T(o`zduH5c;Owe zTtImbj_+TmqAR>33~@lzzn=fjb?DdlK$xDc;0_h|CL(@~G!Wyjkruq!e_TiX#|Zm1 zQbF)vqqr~~!@+e_5dE(af)4*0VG!ci2!~L=MxexhJRZM0c>LG;RN=kzH zcJpiLyJ;6yc%u!DCcaO;Z7dw$KG}P|>YbiMva!g+p^Ppc@uywKR>Udmp&F07&!Lne zIGpv^#5_9}Kn zKN`vGn!8XjF0~=izy9<3`;V8nRIYK1-KqMBeH=56V7%Arh8g0$jgg3N`9e^NeeR`_ z)_h*(N!6n}#;?iWSBAZ7vh}hlUUE;z-jIFX4$muWOAc&=)_z#L%&T^t)46qY@nl-u z1R}0|I#6+p{|+Xb{B^BT?ZFGL%F!=Zl+$Zc?{rQs{M;VDRGZyAvl^!Qd?JfH_kC+m zeIGNpvAJJ^MSP;FUX&Pv)neiFa1B2cBfP%bZI`Zr#Yuv;zMxo^LEDd_!lY#Z7iVm+ zU@&HFTvAm-YA;#ou{X228*;QHrum>+3Y&bn^(|&Y1zKbnT!osK(ayrHGVpsM`T?jo zk@jBqh~ph>PT6AZMa2ZPQ2?b<%FsxL`tP;*du{&y-u(T&`P-)b?Qj0}H-EW-|X@BEse`B40;+c0}QHg`oO?Ycl%{>1WC)v;RJJAKoeVe0A_ z-_)_0t?=D_%=GluZf!RM6;&;Z@B*xcM14q52np#pih(T~Ci zuPH>VQ8y*%>h3oc!u64Q{#F(odP_kxsHUYfSDsVmJ6?Qxg`*ev+gM{Gcv>H#Zt)uU z6R!&!KGcmr+TQDREs~n$KHFiVHr_(oPCxH#+Ro+GqDU$v7ZvuEtTrtZ&Tr+8qBhGF zDp_1o_zTevS>h`-GIHvkdN*>HM$|k2$Eea>tVgfcy_^C&WntyLrr(QS6I>S*{1fX8 zhG(?2;m6=d-}OhOJjPwhf%2`h*_UVn4)GGK=L8Xw*WS9&IW%%gWL69;3Q)vD-bE;E zT&p3!Q|vf*c>i`?Z{bPRL!9gnTF%OFgG4;yBK_= z>NG(t_k-8EeYH*Ng}jluVjkz`3;s1@=Qp0EFCebBeEj}XRh^bIrR!^eGVitP$XwFQ zYNgug){v6%`H&q41NPr)q)K8X-2`0T44_NySMMutKAo{CAKdBp9TnS(tUXb*I9GvT zV5F{R%3%9aZ|==aTj3W)Ppgg15u;aSagtHX| z!eK;2V(~B%BN#BUQ<8UL6C&snOoHY0R-DEZx>$*m{J53mIc3uT)Q}1ppR=THKp5TE zA`?h34fA679ldTV9KB9A#e>zr!pcWnzt<{GQtzduGF#!h`ga2Yxn~deYSJO>A}n%U zzc`fKF}CwAxozy&ou6~!#~?a;;bG0DZMJhjWbSHkobiWMl7!)qRkNb!(4%Pi^TUtQ zb3KOFgR{LIAN3R5hi&HSeMSb6A~$aQc`Ed{?k1ehgwD|FU7Qz7FZ-oKdih#(k637K zzPpjvugUQ=w+MMDGT!@Ej9_n@R1tknN|^PJZFlxF=$kQuXRA%$m=&@gJ}vVC*D@u& znezZIX4Lt4*)FXmQ2r6wBk=tklc(N`p0BGPtkMOm%!D!Rc z=0xsvs=OmOygPFkdHl79$!{0ij+qcldKAz}u{3ICF;>S*v~8w@eY;Z+zpEQ4lk^X? zO(BIjJEuC21)t{ByAU{@iu>X`9#S$BK9SnIdwE=OY(HfDhr^#)6E!@e4Qv&i*1P_@ zYmYA|lvAHU%`6(T+d)=^FVTh6(Xlry3Javz-DygRro|6^L{zw-zz7}}UEA(A2*z_< z6Q@FYa#!tLey50so`_Hsd(=TLQo4W63-g@VTdY)lC`9i~?}g>3v3uD{*}zsgI6vs~ z%p18B!c%R!;aJnNu<68qj1W^Y?zcHwqYryhHXRu1ETy57HSlA;LhZqIul(`Z<#4rH z+K}e2%b{D(AM9tB&uoV6opy=uo1TB6d6%n^o%i_2y zj>&$SlP`6D_?r+Wdy80?Lp(xk$f>8^W<_`WM&y=`nKdWC7;X)(wy=h~FSYo(U*Ku! z>HM*ypBU@U*V@y!AlSy56C=GEYS=51Dr6)R_2QV(at1-T(=s_5ZQMJW zC1sMtOTu&(6*tP0@Lmm(df6{yi|@^tutn9)V2g^$fxTl?ITUKPg8YJ;BzmFW9Qu@U z$iqZl%&V$XH8&97l#KBR?3reK4Az;`1R2z4PD$az$&lD}o2`gz&dv%}CDyL^{*rVL z3!Ak#FDKjJetVbxwJ(v5wk4T&TvOmUQ=$-BCnTX0NSFUcrpa5)#3Lp+Tz9QuHZ ziFQzkNWAcw>ze3=vsW&Q$9(?K#eMG6m3f}H&nH{7?oWkssCm5nGyNxCtdC1bFp@qJ z^<(i7)!8!G8IJ5FnXw0S&IA@o_9&XG1)35LMvu-=8TDFJAf=)lUr~QkuV9k8<@NIZ zH^Ig+7^nV?y(H@90SKn@1}`F0NV$lr8dHCkMxK6mm688`${W)8iw+T|vy5|ly<{sH zPqjK1^?PqPrhlyssTrR?$*a!E>DxLIaJi~FOYwTkm%#U`4?H#A_I?iD8snd^%^F?U zs8nYbhXa&dcNC> z`x=E+LEEkI#o=3R%o))<`b7-)1ls(GF(R#O-XS6e%W^5ljRKO>MLT5NUq~1SPzq0U z_GaWu8y68tSd$gratsO2FermdLiZ?Sw z`o$|rw09G(++r>=fr%Vxk$7oR0grikvRTi<{d@%tg?KK=Yu! z9kO`OHV=1{{6g6 z@-ClIO2*r7CCYK?&&09^FP2pBovZcC9QM9kDOn1rK4Xg^PHJRM|JD!w<*ISDm~_%N z-}g>=@vwWu@4-KJpJxRY4$N<=>}m*jWlzm*R;$NrJgc7BxLRE!;8{EUWh?wzEv5`m zeQJHr!jb5&)IajkUPZn6TIabU5B+wwcld;qpDJ&jgb4JsG(Uy_uRJt!-mfnA)yUyW z>UkX$Dz1Lu@A>cm!_`q6o^QhI?HzGJ`~bzR5+Q*}?ct=D*$t1Z9^+BIVH zPCedMl$I&hvFr}4W&kBo!ni1-R>~w`HC@&OiEfr91)2<@Nd%DzY!@=;y9xGYlCd)xpSIyLIKb$2+D5+l-Wj{#xumm$Hr8>T z_N5;2uJ-FGuPzK-*30&BQ20{lGyLcWEnhYTN2_UGtSW=fEk1waxa4p0qI_1y=j~Zf zpEoh7#Iow3x+Z1{1WUUID&KPV8}QjjyBvn z_w*8`9+%?c+)jSgPb&VM?$3)J|4Bkiue%>bX8_%B$0r!Xw4W>0kA6X=BvX zRxG&l)9RJcy%6Cil$o^(r*X1Q)-W)j={2oM^^=Qh;ip~9Li!gr!o$aFH3&T$+t-5F zKd+S%3(<#X_HO6i{|<@eW>D|K=n(HdUZI^iY5x9D%@teHz1)|rsbmB*=92_M$5p88b912VTnj|54S`&1GJCpM1PK?qrUl3SPOfR zjBGjsSI%qjQawS)k5T2wD?q5g2?eqJyo4rHRG*BD@eLfiko4I~PLmaDH5n_mF&$}& zwWJ}QXpIxYTU*WP+mM3eG;BZ|>1AgVT;l8ioVthYz&c}?lN|?Nw9DWly$GidacAjO zH1TO8Cmmu=K2hzX-=d*I&lHOu)~3i|33*AwpEOVEyvHY#2yC*K zaZlVPF3azROgxgJx<2uqpYi4=E)HabtKB8;J40743*WUbtoSn{%6~B+v(8Xcg-6oo z+2*hh?;LA)E7Yaqurv5N1iQjK3nAi;!BJ&((VPiT@8-!G7T;TyyNgQURZcUhD!E-h zV{!&g1ixJy-xRDt z#!XvOduA8IuDJ+gWsUvV*gEju58&v031wpOOFtBXTF_kPP?=QcZ&Q;mwT^C9=XouE zt&{R*`^t}rJiEyt&2V+I<(2eNVvE)S;cHZ(1f)wOWIvNmf%n!UiM z-%}MSP+P>{0~nC#d;EqVtUP95Y4!TJAscU&m{D&|rlirs@IuMXUhifJV!%|bRHrOM zyF9TE-LKHVV%Z4QBcJvt%l7c%c?GrPj@2B>p5iLdF^18V7tEw-mfvJWS?Cv1qCWn3 zX|grV*B0k`I(W8E7ZY;8s9&vdo7De-V|v`y*v zbg`9>3pr!64wUc1I-~#DA^NTN5@cj`Ce^S6e1`VaT@+3|KQa!gFHo4(@b5_u04Xpk zj#5}?D?b$6v~iP0FNgJMU6+)1bC~c#z!>bp>qFbi@m3X^s72lG9&N-i+ zc6;3oFqqxv3o(r!=QgVL%xtqr^Q)oP)F)C26KhL*5!dtYjC|d$NjQMk7A4vWCGXF& zwbuJ-uNQ0KOz7UfG;!o*$JD>u;nVS2F=~Y~>hLxd5>K)4$Db99h-V-A=A)T z*z;RMg+_ei248IdbE6K_QCj+DE4SWkB^!sF*Q(!sZQ<qd@u~Dk9JK9 z=qO%sU$E|C_p!Fctwng)9V9QxHtf|f`$2qjgpF-V^DXCrQudg$wxu-jE`tj4N#}+* zf-?d5=W)l&{<5X9y4m`VK}{`u#<6Z8Y*DMO)65#Gu(AKhPN$wfQa(V??>A)F>9XHy z$P+PqOA$?-d~~gPZM=t@C`4mJ?tTT*D{uSt>JpGuzY)9mc{5M?&g98?rTEYGhxihv zp77)DzWnx|2nhe#l@Q5-n4&YFZg!6vnk~)7J;*;$O3fHu5oe&3zAQr3vyeaj=cTO3zKWaHAc*W#o%Z_ZV^W2v5Si{@QUfb3FnFo3GS^XD- zDtMj;v`aQKcSz$aKf@oq(78_hu^u}1U;lU$0GJE(#WStiSwD;yX>qmit zPe*^51g37S?8TF{TE4F-TaEXe*biZ67IkZ7yPxiGVd{QcVya#ob>?Vc4)S0;C7p3@ zS1+-Om9y1sG=jIKm%*Rkz+%)(sBKv;Owf?c>E5xn!krhQMl$Yij~la9m53Y4Or=Y8 z^sZ)$6D^71`*r53jB?F^B5V+hRrM%2zdMi_QLqS7ZF1648dY?mE>0__AQOQi5#VEb zJyzbe`oy4MVFR-u4{=Fyaf$>Ek17!}>kF#tH0=W@j|s(EPs`F;ZcWFUvKDM_XKfwf zW1khMJt%K$a3f#0HRl!+($rg3$hBdB=7PPA%nx`MI+q=$jGd)P^q`dF`uhx(_z=!H z+RPRDI;HZF97k>S8iPkUeQHB~|IeyZrXA>G`$>&-D;k=8bRL(NZ~3C`Ic)nRq~#qI zm(L=dIS)Tp3&Bz=GB16K&i^zsz-xVrHMSxedo+ixRU)bGrYxsZX&`+}>$jjIAivK=3c^;Wi2|&Zkmt)o{MlNhm?-bE-F#V{O%Kq^K zg+(WgE@}AceBP)rT!xdRUIj{#wPRG22@U2sJck7RZg$aOCDNEybp1?>X>|N_EtvEy zRGn#>3!@CVb$c>wxtgWZ0*|!%R|RwEFEvH-8~Cr@69Qf~1z0w@bk5eIs2HF`?m6#4 zH`SZ1S~{LNjB(M^E;g)CHjE0rC@jp9mstGv7-_k3G*W6NXGX5UuXs7CVPz5~kDmSQ z2NzaOgqE6=6#GKa;$a+*v;`$j$grc>S=6Yoy4~$k(!mpNyn^~@j>YH1Y0AL!Q zg0sh4mpIGY+MGv~NZBH+8n#T&z}xnTvkdc5w-`FtZ2PU_QtYGyU&9#4=DoD)!+~Sj z7X)p7qtVgfkoWQ!Qt)W>AL5j(^>^xLBnLQ$;4T1kI^xUGKN<7;pM6sakEl!{=^+-x z`W4)}Ay}CM7folxZDVRS=QBE9-Oz;y78mWu61fj*Ei0{;xV&+`oTpnpE!iuL(tp$_ zhG+&y$2~F0QFrrS^YN$3V7pucxJc*nozD(VuJfop8q`W0^}75lVqk=?>cgU9*xoVY zm~xP^R!MH~q_=$n7AnuoIJ^7nx>9;gmVU|D?CRFo0_XMCPcuIwPQUO>8u_*neoa7q z`o+xG9S5JwF>z(<`|`%ri#7SN9ec!EvBEO-H4Bbk4plEk@h@tdy*>e9(Vd2jm|Pt* zsDWH8&e~)lu|%sYtt!l>XP$;bX&1)YbDi>casdSolM6h}Jwy+FeT$|Lfm4OEcZAz4 zP2xliWSr8Eb+GQ-6Ew2ah41At?-W%V({I6hWU(iDV7V+N95pu)Ga5%bWZkom;g-Wc zS2i-r^f2bKEO*0ok!ib%=4_cT@$QGkBU%)wPe1zo(RPWhH9oeec zZgR<&6f65+wRD#cHjIY9YG$X0ZJGg?vVQTmKOKeppAC1(bT?cCwKX}yOpXm*EE~z? zctdr1gr3LwqniqaPCNpl#@HzDrqa7M+7_~h<6_wFw7D(Y&Aw5Tya>@@VZ2Zy)%BM1 z-cQj|Nt~}K7lM@|Y|8BO+r9*i@YdvCAhgc~k3^JC`xlJ^#hGWCac%mv_84y$rSd}3}JvL1guAM-UWN1tnm1b(f?id2? zfm{E^bS)sm5-%KQxBeu>q)C zf(D7B+58<#TOE-`EZ)!SsM%uQN|-!s>XbAJ;3bP2%eoIr6Z>L&6uOXU6Y{3`pcQ2@ zzIa2`jKZ+3MNXEa(l%d}>(lG_#!_;uMkZL)pf^4vxsGUslRss;I<;@6XYQy_2TLYt z=~}H?eaH0&y2mqxvKajcw!?NFn-bxOyN?U5cQMP8~e=h-7du^B{@lJoH~wc~zQM)UNMPa3~0Dmy)Y zp_1M6#FxLab;s`KkIIBYba*zOxMm= zZbO<0NXpDhli?4LOo7vpQW_H?s#LZ>j(T3xuNc9gxi}@Y(1=}CWYH-UQD16WV!*M2 zzDsM4MvGWIZax1DSMFBUoIsw|gXYWTWd>tGg8&r%Xgia{hwb*5jHlIxy|E#}dc7vu z!kr+@fkP~@yu>1NJ1u&Rhs8c8OjhKw2hk+is0qogz}W8!q=zD3RLue@3mT>r zh8Zn#($1K6x0N2frbUn%w}5(Y(kC&1rG1wn7YEC$_63oYREuVb{80oJr(;o+)d*|T zZpyBjZGTBL(z?Gt&d0t#rP<5b#_;HWFctf^UfdTD9nCWQSig6f!(73$l$K9Hm38x_ zM%B-&&((bNeXP8@)qK794EAx36oN-3Qk8c7Bh)ZW&-LZw*&JX#(jm{K52RyaxVseB zH9Mbk@n!&^4}M>LjVw^sJ6o#4_+}=oYSbqr%!dq%Y5L^*lV_e;Hooy&K!xhKqS)Kc zA1f83d#NG$ee)sP;{sy7q~X=e5zg_Rx5|9G#uscKb9TP zmq;VaF?5R4ND?QH_egf%WTVsY-QL-&!vYSJW&sLARo6YG!=lqYYRITlsH@N1oG9mr zYO+APB3q=bJlKId@#Jh}9SsINo2qCYgT$P;BZkbdkCp0}#yQcvo2wGStq*zM3Uu_E z6tW9y=^`;j2|-7@wGDVX(H%wT5&_&phJLw@74II&F4>?Nr7nECTDKgIRj)gl9iiVtiG5<2&*p>fpk+%JJ!QV?!)}I;50k)35((lMnMhQ-b>Hfo zv&H~Q-p#%=s5K`G4!qK7Rm}3p)C2aN)YX_+=00*i`Bj zwTITZEZU?oA!)B27>M^Q+TZE9+&;STCL@G>Ao`;6v=4ZdXIGdP-&f8*`k7Vz!`Gpj z|Cx0z#S6PN_ekPG0l=M9;_{M0Dn{qZcBGiLW6D1IhVq))GRKi7SIWb8r)`r4zFt;N zzc%ezIkIpioL~J#YUcE}t*iVs{p({_Ds}`zd>G3>k+> zMKkBENOdviHOT~qAnJY9c#0UJHlbK%N5?ONim8!#N1jzDQACZkp%9HnHkR11D4wnlbwy=bHtx_dG996TVO@jje#s=-;lOQN`fy>* zk@r~Hg}Wf_9?8A7;`{qJAlrdf{p;nEzU5BOJ&||nCYM6T_^%6Rb4rxY2QfSq7!_E$CJXkv}Rb2jzFdVx5i z%7YKM4Qqi@i96APZHdG?!m0~NKYrHqOvQ5=vc**T*M!BCz$|#YIu|mK1JjIY}N>xJ=OO0AWQA=_pVHX8HO52Tj#5i91 zaItWN_gJk*>F`XMqsb|!fHT%oAqTibl2I}j4;^0c`>iNvUz+iNa{;OE&qOP@ow$H%GQQv{3J`H?CdjL zl$=Q)wkt2e=ukI-QRX{B)f`R>Q|(5>n05Silo)leC}S4gC0-m;vxTlb>nT1WElvY}@39qi@ z3Zxox?mxA9f};7fdse+)iM_Op!-(OE^(4%CQyA4O%onF#(RPwhABZM?cs1GLToxr$ zN^zW|BZZwW*r7Lg%*SakIVIZ3=s!|d{U?m}-y0LPs5w}K1gWX6eW9-y`xx@R3-2n!QNyw>064otgMwk(7^+g+c$)ADiYeIf}zbgLu2l3_X*43JLm+ZnhK$dd8o^_ihyvjN6dd$R3%Z7UX_9Rr*4V!~$nx!$N zyu_&(bjkg0aI#06VT!lB1ifujelsqJmZsj^gl=6Ii`HP(W#wfB#7+@wWOHJ;BZwtT zd{FwVvA%rz(m7$Eq#6~$Z}5;ePOv2amB?+FI2tb`6~D4Ytc|bA6l<3uJ`-rSoGlSE z^d-n2CM;Ho9@bisFz=VW5v7LdOp1!ZcFBxRV@!LSR+LRwlGm_iDEO|r*-9=A2q@&) z)q7BUNDx89accLX(&^d+W41RWI%G1HPUPJrPMo47r%2#?Q*fB$5X)?DdBbsDneqnj zZGAiKJ|}q#O24&$i^Tw=v9ax%bM)yC1AG?lnSZT@{JSe@@Z}6E=_Z60kNDY`@YB)* zSK{+H_VkIKIFOCnRHfgyrM|*Ee}n22^&u=-vzWV3Aqo=gL&y!Qyv)XBlI{|Q94`+d z5<4yhj8vD(UMQPbD03VQVUBG105?p9`8u(FsdlV@)p#{i9nTX9Ywt9*tVjm zTxpX&-KUY--lHcfbR>~D#=NB8kj!1I2n9A1q-v))xmUMOgP6=tC2vmPgXvkIoN09| z9 z5C-QD;?Vun>gn&H)#?x8%gU(VPuD1FQ~BzJZfsSLO}K2VFKL){4pC`z3x`KiGa{`z zi?#4(w8autyp_@2w>I~y5_mB%orF;}NXG(>U~ZJ=)nm|<7QrJM3*D_*K#G+Ll}>G$*-MM#$V z5J_rIW(lSSm?y7gosnP10 z6L#tKd+zckZ%R>oSb@Tjq;WlzIJF}{8}uaQvEGu{Vl6#0DMC=ciml~Zgp%Drva_qB zO>nb^bM;?pG?Jl%BBH`}T-T?7yX*@b&wSR~uhqCL+VP4KXm2+ROLJA(C=5O*bX&IP z&3FV~6d3(z``$DFjv_-e!>qTHa8EhTYjYE$vn6h{xUY?<-7fPg8kt$#_8FfHt7uwy zugVyY3~e0t{izy9#>8|@uKcW4x5Yo{pAFu2mLdn1^(^{Tj%zUUG=KRPx_jE!Gi#)v z!ZUBfD=%bbVr{$D*;j0Ob5&zur4rKgLzYGsON7Z0Wwl_Y<;6LDr%2iokIe@($ukqw zjlNAPj2eDZI?THMa2Ru4qKFw=v$UKZJK*4Yu%Ge|I^f_E9r+rir!0Wc*oo!^n9}LmWa&(ulQ*aJ(}x zXi@@4$as6wlo0+!wJVS^r(n7=2Gby;cz1Qk#U?-Xx+&J6`GhT}Vq4wPZCblmu326S zFZV@J8{dEUx?vuXGEm;&7u6O?Y~#h4QrP2I4e)E}0-$JJ5~yz*boQ@-E&kX-ObZ5} zO*r*#3o@wE!KbWJba^e}AW0>HB=!73k_w{F=3hQ!HnW2!wDL>z&|MB+`80?@jye~@ zOF!uN6wEnmqmREc=p}zO_d%an(eSLow$HV)kc#oyO`f>D?OWxO{wH@%bBcv_d<)!i zQh%ZHc5vo$XmyQ#;1fVS+;OTE(=Qxf{&XqCF=cXO_Q#g$SUz(Wc@1KxIWcS@?CiJv_e;s$&6xG-|SBfauCCs zR+rq-@U=D>d6o?#3cwa*xu_+>uGO4feN91lC3UCOEPz3JOGJ&$QS#!(lzvfZJf(!s z!uI%I>l-woGwcQ|2~<#RVANPXOE2U6kbCJzKQ0wh1R|>T(~@19$7>$1R`&pI6%Na> z)JC^i79r6sxD*+Z8&S$SLRIwnbAuoRdDX`v`O4$;j;TNZk00Th+uuX?Y(1 zv`_WqVyNnAePkpNu%DgwvdnTjf2=F-N)3jTPfT7>syPTDdJaMeW>3%NXT8(;wq~H# zJHNUecKtM4?+m1gAl?hjyvlcOT|z1@53|4 zTG@Exk91h-W}h%P*g?+VK`gcx#>j9qvcWG|7Jh@p0V%Kla5f8AgQzh{(}1Z7g|6i) zU=3uOzN?!abPsh=L}|2pP-+LshX<#cMc2-YW3o-~R2@sXXnFO%AVyhJE7Y3ur_RM& zW_W_Uq=0!hyN|R*|C)!Jg^iVuE7MrazoHWVG6Qd5?1@EyYxmfoQVGW8 z`BB@41rmSJ>f!e!wlvEzr+;bGX+=>d&+_ip7u;H+*I1Z3zOTa)R!G^8C3A`gQs2T?8O|hEV61Y;QnpzL*OgR_(C;hu(J{vx{FO1`zefR4 zWAlTKqNTeLbq>CpXhs6!e50$hUD`QeKCjjfMck^7Z2BM7PX;^*|9TF7qDuSJ5iTw_ z!^EynE1~J92)6Ht-ts4_FLgQ>4ts5=#15=@RSqp&QQ6~|xKr_YuH1=9Q8BgsYv5+O zx;{SQAPR80Hlgx*;@g$0V`R|xq zxK~cca(v|mn9FP_WP`^6Jz26ZKR<1UNs{u7GMUC0EOW%q}TxVMWPU z{VTzys{gU>q-ENxMUH+T`pW{*--xtAtc@@qaQ8AGWsDI|?L6mWpQElX{jA&5KS3z^ zGcvQsb4`hOI@>X)^-GZQ?(96J4%|Wv3pjQNyi`T7~0e$VvTA} z)OBXjTa0z(Zn4PG;1S2%+}fzdxL;XsEu`3TwJf9cju`Z)CJMK*Sv?bK4+sqJ)xtWk z!Bv+*92`$z(Gi{^FrbmB!4&BW$x_honB~EI^ zGiw4=t84f-V(LHpDLP%U*-mZy>Q`QM?YeFH;E&775jCkHN#ox(!1rBDhcqy|751p| zdBy(CjWfGt`SF9_zpeee&d;pjwYgj6J44wATL3`2VEt)fS`sJVobq)rhjKw&EH_QP z8Igujb8(cOL8rS)=}==K+7Q{qDu+ku`a6O6j%CM<^^`@<1)fIf;y_+q>Cp&U7V4SV zI`w6d*iC~X-FWWSC95a=9c)3t$D1FfW%3&>duO9%%)^E0^Kemw6MD zWBQNn@XEUUkeEFr%)epi-Fh~AUeEoavmoKm%mPifQ?lGu10e!0TF?7esGdv3Jny+c zP-Z;#9FA(dafpZYa+6Zzz(vBdboCY6w}7**9G+!~hNP@~~OaXQq&mXUcp@93xwK$%h1 zBE}EplB9^NtIgOpf=R73;W*NlQK$2k7wB@d>Qn}l;T09)irgF^uB^dS&GA$FYTYP# zTJ`Q^A8!5bRY{~_v!zqki9WWfoV`A|FTez(Adq@i$(f?MfS_)zZz=D=Y17Mol~yqi zc^|3NXgOwUM=8NM18*4}ZEr{Sm$`;M@K**vKf{R622XGUGPuHNe&ct(&l{I(CIoF~ znP9Xx%UuiQ!>sQQo+p4%z$86OBC+FKz)(($uUdMyyPVzbF%?`K#bfnqgiR`5C6ZQJ zlj<_?9dOr@t2=uOjyYYv@0I8FR&)TDNwMmIi$r|ar@$+q5NY!+C-*UzWxm`i}yQtR|B54J@OM96A$ zsj@x-8vbp0PgxLsKzSN{OSp~ABBfob++0}`uS-B7B!n<47^qezry}Yx^~nCQ z*5#mf{)3)-`PM%7C&zKU;iQw22{Y%|6-bXwhMHE=cI9!!3=SA#;D(G70hkaE0-y6+!vGN z=d;`eL`40k++QjZ`CoLmSfah`NnkpZUX*$kD>FQ>RRnamfGtVuVrWrd=H}xYP~~~- zu(#aS*f}%XjEPH|fO?C({`S-2$*Mf{l_w2fmX%fKQx%i?zXexBT+8~G>4^I}{c{)a zB;5w**Egyh!cB$guQY4c?B^5da_W>9-uZ(Y7Q%q0{sRR&6~s#4Hmlc>{>l0vK-C+;S!v>OUd@!Ez&u`Nd3f&Fy7Xs zsA#ZlOjR6@A?s*_Xd9awb)?Rs@Zz>c{>UN8(paAF62=d+w;%)yJYLk0fQ}JqlM(5c zCds;|%af2{zg5Zi-XCGfEj%*9)`gEHois^OgK6RjYc$&3fkFFv<|}hZ9rNU1R>NMa zTuuXv_=0u{Mv;QeDIcMEbzb+m)X& zb~%i1M&gUyrH6Rmm`TPF9b^@LX-VOENw5P=JnkXsdocH$jm>pxxGO|Gm(b`@SPt>S zc2CEI<#x`IMQS5kd&C@a2eGAPt(9AoyP|7-Bi2&OV)0@#6OyGDaEwQ+j( zDljAmjE0T%8Uc;efu!gL7ys*N?uYj%g0VbIdzuM6b?Y?=Dw8x?7~%dyRoyyswA$wG zVs!O>88T62h@|tQr6i&XgEmVz2C3a+BG1-P=w!yxlo+lE0!=Ho1D=c5L46@mpoIF>nBjj)faWQA0sk{w{KUS#a#KFeYxG|pfs5Vwv_Si9G2zw{_3mtwA*1- z;TJiw>|#Lqu0pnKZut$G4*+RAe`45K{;JyLkkHBxUTeHe0*J7j&!F6pXC@q9JL#_k zcAYZL?U@Z!4j;dBu+zs{xY`8gEfOF#s5zUt1P+0w;IQlJd$fEp4G~N4+hgfib_!$B zR8$4Cu;DFL-zb)qx`iAbc#U}wJH0kGiet6Gk8t)1?Oe>c52d6SzYk^R6x<;lXIbYE{|f78^qv}s#?`>mZ~cnW+;XY zP4htXh&Fju?k6ZUSks^Ck&B}-unh8$)Fa14Rf0ax;cqAK0bf*i1+^!a(WaCLj5VI! z!&%=3mBMP&UlJp=uaK}ct@KJr6z^zrnNi%;`ZCQOw+|&rX@4aS0g_J%K*gV;!?jEY zmpcn7K5HJ|xE48q*~np{1_1PDP(Gm&X9ixH8N;5HIv7 zk_MY_ke5geLhz^6Qn$>~M%>pca4jpiit)LPElDymq8S9Gy!^DxNq|UQo_5JrAh&-O zU>12~d^rd{LieOz!~^{Zs&*Fe^lhK1nJKVyd30oMZ6`cp{Q8}c&GlNc3ACuOFiHc{ zC=g+W>M>#WBjKbNhl(3i__GWO37|cwuE3p^R*Pjvg|W$;0+Lf{6ASjHL@P%Q-KB63 zu9iezFF=Y1L1|;+l<}`B6sOoi|xATEuaq<>E~4mNpi_ z4-MTJ$0~@}3|Yzc-kfqYz#HG9aeeN6Cvo_+5@pg#)ps>OiyKij#S{0{%>!5G0Pd{P zC~S!GPSH1vm8a3O49bNWcgc~caOTOG(#$2YaFGwCEH7DQ`+{*Q#`t7;u-Oo0*CzAj zpS$tO|7^ne?|;0;>|=B;u*AD%Y-;(<{`+>rz__8UVA=OhDB!Hs) zhtPSx;<0U>pM;EO8LTrS8RQ|V%lU@s00(-?_}opcsOMa%3a)D+^!-QQO-G*zq*uk{ z!uyeNjPR58ugm45{4V&c#+kJ+KB;N_>h8tB9p_66ciO?OQ@f~V)zeh-)jOC8De_?F z_ql7=#%pb#ZS83IuB>iXEuvtM)N$nLO1YT=s9&pI$Ep80)eZu#k-#epYgnAwXVx*7 z`#}46=-AtVyUu%Q<#K5|WVmq6jS~T27qKIh1Xb zvNP71F_^Jst0Ck(5Iw z$S&HK6&COs1_jCsXm@#W@#D&p$~7HG)(kaAW?-o?-lcjRMbCrYdrgR>>UA?> z))OwQlk281uEj-mY)^MKY|+}8o%Ye5h|1G)V2X#=CpZ?CcxqRWxW#tjGAaZeT;1qI ze6L}Uhdc3~`+od2yGZ?esQ;x;v7}eE+G;@1JoPVELaTgeo7eoI&%a43W>Nja zD|3Sbgt!Hc`R=BR|7-b$%}?v)Qs2+T$e*R6iw7s4$v){E4|(<~czIZUh9|o2)%-c{ zU`55NZSUqQYqV2{kAUz9uqE+PdBD69eb4-HO!K?hXoFjMHeWZ-FMo_3ecI3Hp8Onz z!NxzHnq8|4p-yvIAdf(I7zX`3+oJ>Fap!Ar;`zFTEs;~*QD5?CRk%JQS4-Hcgo1%J z&2c&tVU9KuO&Qf*paTs%hOlwYb>0O&9e(mHAX}r2DZa$FY!@yDJuVcI9U^<6locam zD?0H|%|So*mbya#MU*dD*ju5Rmwwn()xIpd*ub%j%tX?~iUtii!sEV!Hz@Cn8Hnmd zLk1}g2#>%%Z`4J~owm@>IkCLQnV2qQ21*BBd*_8|O}nn_NAn2^TpKb_Jl(sAFG<+P z65kU+ceiqnNCs7d1Zm|Teginut1@m&bAJFAz(eEI-oAyXy5ZSFcY`JQ!yA3Sm{eUh z8oU17KXic8n@nr-pI`ZWQ`6KQc<7fcAiu2hNd5F0H|+$md`vqXUM1JZ!~pHFJSP2K zJgH(VBxa2@p)in(>VB`Bk}2qA_>_BGEqPllYph$%t6_3uE!kle25Fd1H#F zsND5q6&^G`a`~SY5=#m*#)T_O)g?RwY)r^!AubEXh9ECyFMMxbG9kdF2dAZ?2(8!k z*59%UH8bRD%7kLzO`_t`AUk2^4xF$kPfnNyFWn$ogp~pMlD)i*UJ{^z>BAC~Up(OX zP*b~C{Msr?gt`oFfxvi$7MHY#%78srplJ;!9qB`5{PJSkE2jNC8>hFY3lBV^6_zWU*yF&GOzq&O^Cm40p!hBjso!SjcSV8a$LgL zjEN?F^dpIKBy^>SK$X7U>K|;1zIo}0R=8LR z;@xKm5Ad$OBvK6?0o|reSY4>NsZdi13d=*$Wnq90W#%qwqv!8|_}-r`EwYrQWKwM9 zn@jT}6@V`#>5bB*s)@DQ8)i95wsWzG3idsLCjoU(u5d5<3S3XXFe6rWN5=x}k)~rf zB~BlRQ49lzEMzl^j_@8cB`}NL7!h0pXG|Hc`OE0iT!<IO3ayshP*t>J*G1n#& zTVKtbd->7~G`;>@uCJ*Rwu6)xvVrxaSbef@3tMck#bu#0-py_CaUT?B{shN{@DeHT z%{nJ2Vh70AYJjZFun^3ugk=K*?>`=H1D|jeX?70ulx&=f@RPFY&JB`j6_q0ZRiZjl zp*RTte$`xG2)YJHE+j;Yi+40=GbP=TanOHsK7k7!FeT71Ps`cY+LPO)-oaF91qh9Z zZUcf#_Mk4I9NBM5XXd}#&v^h07zPu|CJdG^i@CuWv1Sp20)YYK8`EgfcX|A@S$F8e8GA?>=pXN)VgejiJ>V zg18ct-p(dnvduTwJA%^fh?i(`4zz_+G9&yXDWXN->J_qg$)ao7S__ERzoe!X zRVR_No7M4M?(#MctVLBG_nsa9Ujd;1eI)9?IrA|2-@tY6TWX9vWGsEXaw1q7ec=$W z8;zHF26kF$+-|0EmRA)^p;{65!0EC9U;1-@XhMfKi800^qis%p|4MKO7<@(ga;8DK5IPVmBX1QnJyH zO_c2r6OYp9a7lYwqGXXTE2!giei7)8lcQP1(##2fb$aw%!qsXa=BGL>m$1=?!aB!n$h58ue>e5a~KYJVWwcqlhQF6 zwk8*R410(9esMqM=&fTleX~)q7{1(|R4GLPY zRSIjhpf$8?u2cSD)iT2ynA4q-)NVYmbUi+(V*xeXnV4awwWBd35P`ty$vF!(>mgOe zz}MeH%BD2UUD{T(I#SX$j2x}l+5-zvZoYy1Y0rUhM_^38VV0+=6|%wi)gjPtW3!`6 z2wo$_&9YGgf<&@zYgyn!UHb)b+H^(NkgrIwGoWtLvaA`P$DF@_CIDbHx|1}qXaU~+ zEptW$hTqyLn;>l4MNXHucP2lP#utQVKPPl#Pn7Rtx=mXrB#3QFSaS3zJD12HEt_UL zL|~0oD(iJpr(1!ZTW?NWw9g@lO&_0LyC0ww9LiVumbRCSp}(3tUwd3-4uETaS6V zoHh7*VXf9xb`&kSwDxK4emp=uY?SH>!LJAr?pCf~LeV3<<~p&vmKsX@5dye?eb8PV z3*c8UETNu+CBRX8R~wyGZ256|W_WR2NvtTsX3pOiFw7nOBw9t?B4t~{io9fPyK@ti zTj!C&j()BwGWS=f)_!*L8zhu zWzugIO4CC*&Rb9E+m?fUK_pR{LDYraYe6^Y6$GGqUVdOq23F`EP!CwTpbFHjsq>%9 zbzEIWOtx4ei#D3fh#FB>2D#bY&1sR~sr0~UZEg?H% zq^={RH7#MP{bT0OBx$-rq2b8{etihnDIily1U5(mgR;lHo|)yLQ4xf5HG~GpD0Q2A4K4n~YoiAD8FOU)XoIv>r+nhQ{e$b+i%9@Hb_PEz*U3?)k_UchKJktD#K>HP)? zyodan-Qrq;XlGK8tW~!gK^l~U1r%?x98gzDFWnd#pZ(2-bx*&8}jdhB=Immtw;f!eq#Zg;2F7Xe)?gMQk!y zpt}SHUPy9m?SR*p9VdTaZaD0_uHDk(7_ZO zCJezx3#YXU;$3l|3*{vDhkB=&|2q(iss3o(8D4HTXq4c0M%*anVmzAYnl^(wSoQ?y zmF09vTO!b;?7GQ0(zZRUWKjoK@h6yrEb(_X1Xgw)9$)mkQn4PvPQ0?tJy@LX*(gz@ z=+&D=h$Oi6PCL8y=WNCuAIR?DwXhIX;W9ofPR}E<&dT3+8hn=gXX1}>!N9*?FsXl> zS~Z6pEuKX$_N*t?kItCR32B=5WxjTlRm;{b6fDhsn*+X8ni4el_s_>*M z-1oTO_bD<<3Vs(3kqBy(!e~={Yx+N&lf^_K;u_!1teRajZ%ynRC#}Rj4I$p?dB@(L zcdhvJlUGyhSf4t@l=jJm^RWuGThD7|zHG$Y$Lf^7VYB1%;Ye-#xr$P)%4b_eBI8Qb zdStBlmSrGWg#YOSp0z~`BWr3$9sJRZP*Vs^Zzrycy+0`_z0zsuSyw6#+)9V#B|>qA z8A{u9`v*xi&&dSCT52g>zgycLm*&S8|j21UFiU*nN(F(a`vm z=h6r;LPIP|t$81>&jXbCEZdrh4&cEr?_OdVr5VvF?Pj4%cR1EQsLE4#IMV z_GD4tm6hanl8k?9^(ISVfc*U{%RACT_+%Xj^q+}5zj^byUjfTv{y5~fnV9n}L1*u# z-y0(~zX_{A2NR92(k5qDHo_7k0lIq1?aQ z_(2$2%b1AB?ll8+pjE)JV|_GXl#lRYyRvzOaa|F@($)YqD{Ir^maJjT6i+w67o6Dx zsRFof1i|%JUaT%Hou$lexqvbC=!s0%@^o5!qUhyPY9&)+97YHM&_!P3BZu8peLY|N z5Syc*b6QvKyU1=I}%*EHz9_H8|;bKu!B0XF|8wC4m$#G#NUrmBK7^kDzY=m z5}<}7#cw0(<6|HJWYFZgYtQ-cdVze6Rc-H=Nc2}l{+Q;`Fr(^g{Oai)Z^GAn6do&r zi5F}%r@wiBZ1P-mt@-I&#iMi4V9bE}{lTV7zR#e6W-YuImyf(Y_xX_3T2h(L(AO`R z8S1OW8eM~4ZcFgP<+Gfz-d$Poy-f3AXX9;?Y!$b)i-OtXF z&6$7!0zCCWVlAbV5JBLL03Y5PP!IW92=`h|hDesmdTr^vLxPaVME;gTz<{1wn{!~E zGBuo4E{!jT*2z8ZD)fapy1;q?z+Gj~kjhMA8h`uib~0&m7+O~LI-0T2%VSPpNeJN_ zQSuU2RfXfbAcAS$SxEcWJad+M8M2}TSN$ShqH_d6n!_g^awYbvg8rtV4VSw+pb~pH z_5SMe14tb=hYC8yAZl>!*WB)(zIfK*LX0E0Vt9DOfzZpv9I4}fle;gN-_PQpU}7%l=sfQRIFPF6QoDAZ1^KUeB^aM8yHS^M zsjpq1*1<=wpVjAuT86p+EZmz^@nv%&&o~!mCRYQ7EA}@KvI=EkMXXAyZNw;goFQD! z)*j-NjER#7a7P4yy+8|+w*}5AAb0yGN&!Cb)jiGgjy@o%jmnX&ExHQXQ+I;$=VrZT zYXIoocTc}d)v*t?2P8xmc!*Xu6{>SzC6a}^==a`S4)fYRpW zkb!8uRG@vL`e7`2K8w`qHmU=4z}Mp-YDCs)a2F=WZZIM_&w;fo=%KTx(a9&L2l`%s zEpri~1j3Q>m;c#8q2>uZP${YL#M z-8)4GNYeK@{5vX3ApQ1fwI{Tn<@r!Oqxu7b(df&4W6z%lM^+J9W`bT0gqX;DRinn& zj?SI~>3+%Nwl{MxYP7Fo@ASS~sKOjfi7E#84`A(WO?^K8C2I31k^k}F)N*W`B4GQ{ zm*>C6)e&1~K5v=}{TCFb=MN!MmDPBl)CSnFOlQOAjon@4x_2#wN>4#bdIW@(teGV< z6Od)f^(>BZ3IkV%9?}N*w^3doniuOS(;ODTDM>zuDB3y$-)<}Diz&BeZshE?6LY+y z3hI(fjkfusbj`Lh+TDG4k)NRwdX3pydJS1fO#@4&wXz}3)qihNc^7Jj3Mv6*rHOY* zdSTYS@VUSY9|2voBr->Mw7Vi~gm6T*yR5l08>0*o+j-Is&JmG%c0J@AP900vBDgF9 zu71m2_=#2>W`v#xFz%g1^^tr6jEd9d5_l?hC)z2H6Ra(XtV61%Cs;<99Kv9Zp)J@c zLC$9e`wD)W`Vs0_aK}uWC%~@2Z!4eT?3bBYvzy{7hUs#q}ZF!J&zdWX{YRdmSu$_oQlS$wR)NDJW#v3oy-|PDVc;`5aX(pCWm> zer%@F8~AzCnU(pp};~Mt zJ-HEGcXXzBa%H_P~F41iizC&=3BtHjHD#IHrb6jVJLjyRgl!-?m@%pgeVV>xcGCUIrQlh zhmGlPVg~w0=HibP{zH(vf2GBnIg&l+9h(-E2IY*h#HS^A9nMNhHs`k~^IBgeE2I-7 zFJ0BoZWUQD4Y6r?uRmBguqC72@_Kf~N41p@%JNz~H2p8g-SaqYs$a>gfTeRmokKB| zqoHT3cPh+)3n%!6T+NnxT;n@Zbl%Q*#e)7%Vf!)nq5=wsrlR&=P*hK(kFld_e8x^+ zXMia*aW&Uj4P=nK zy%_W=*jcCm_119zWOWB!2&7x*gq$c3HFZdhUivFat|f)Gf!u_^s~>CFhbLtj(t832 z_YoEX3(=kB`R`0R5tL;tv79yun1>a-I1nrXTr+V)N!xBWPVe@+XGc}mv4b&=RH6n9 z3p=~*4Xmx9x$>|TN*`KxZW7#T3=bU2PCjW3W__3v`cVZjUi~@E$;X*zet@0}XJ7pE za#VX+leH#bp@chb?YL7&@Djv#);bocl9Qg0Sq8^xJ3{tT1Y1Q|u zj~O4>uky5JJPW&~LSCj{YjWpEdh3`AQzyv)F|t1Lev%V2dNvHK9C1{RmV4p!p1bWF zs6_CjM@{#qbEcyTz|J@LWu-=bOQ*PZXerwKX#C|phF1hmCf@OVdiXq5zep6)t_R-( zRV-_;GU}`?4wPA~=^`-&0S-c4z;c$XDbNJ3MlCmWO<;Fe!-ow-ZGemqD0hEZIVhP$ zXh5Qo3jtXInk&%t03^Ou^mDp$bJ>m)30yGpFBCpl2??U#rH_C*!2J^x0rLC#p0*oq zRl1Jh03&W!ZryA^(`Pe{Xj%SLVCReI2c0_FJI;{N8EhvDHV%>{y!=)N$rxVi?rRRv zt$Lg+QogLgzXI1yDlo*jio0VDFtcrpv95-~co)}2d4V6LM)|((y-Ta{x9gKAVBTa@ zQsbN12V7@&Z-%vb>mS-EsM3nA_n%*`zUHBq2WW+!n+hcSuZ*9-RH~h)J)ZV`K>f0L zv(WGHK;Y6kp{3vR6GY1#%3Uam$S=Ep05!urwDAm z{vZs{MUxb6aQci8X%K~}3tM%IYwiU1fiuEN)Z1OGd5W8FMK|!UyNr2Q5)llSov;^g z4Tr`k+s^r?O0{)IB&xLbxTUDs-H5oZ+Adb~hi3aczy~;l57%hZ#Il>!XgAWRdkJE! zCWAkKb!Y{8{i2N=jakIhb|jI(-*)q!kd#m$|!G7e|5Zm8+Cd*!>A1y>b>C{4qEsY+AOJ$tKo+<_z9WqFM7g~uzXBAUY0trM#)&)oXb6=m1IO?Uh9SW~jP~Olyvo)uWs6%xMM>ZddKKbYz~dsYq;FX;ouhM436oMd|YB*}P0Aw{Q<@qV!i zbM1aW)$qG>`}1mVrBKIbR@X;^>k6Bv*fD1J#vU`KKdyZ>-_q#^WMex!FL_h+$X#DoM**-VhJ)2YFrg6J`Y;(KzZl48{#Ud)MB(3N5Wm01*HH)Bpg& z0TAL&A%OqnZ$l?w|Ku^ydH6qh0*FWclmBxM6^H=rA!-i;0J{Hnp8lUa0=mZV&ob)} z&-&lWDEv3xv&<`>XtwKMx8WInYD?-+~%?CsOElJt5qIkPhJ~gu4);AcR181%V!dD1>RK zd<29n2#Np1Lx`q8D2MPG!aWGk;{sj~ydj7|FoR$W;m^5f2rUq1A-soB1mVxUp%9!P zI6!cOUEggir#Z4FVDL9BUBWhX7Rt;0&P*!Z8FGbnOvDWg)CX^-}l$qi+7c z>hvm9|I<*z1wkT4O#%QFqyV5z4*f0#R0k-i4I#06O9KGrbO5kN57ieV09;}QfG!s3 zJ=p*tfCH*$E&%w-3ji+s0Psr?05U|Nx|RTdC(@9t$pJv?RR9=Mg5*&R02Vc%deMg7 zQx^c@44`tx08n5G)vpBrELcHpU<&{=4$!@hPA(cn1Kkh5^9lT>x;u2LRns0B|uD0Pf!hfT@Sj_a*{Baxzpt6{_cS0C3HO z+A|w!$EN_GRsaBB3Zd^U27sfN(EF7Fz>f;3-K(KCssVr(bpT-709|VYfV8&&Akqu~ zz3%|PqZI%UZ2*w}9suM!0AR8cs)r8%fa-?Y@Dl)N_5#3a9{}J6pnfz2J@YUCn2rL# z&KLm1Pe9K%^$(c`03OUgxc{L4asK~IMR5CnmH(Va4%OyA4)CuL0cs$qPyOMcF8mjT zA*y4c=7I$6&*$HFL89tdZh#(u{q^}JBl0hr$W2UC6$?T=k@PQ00FeJh2?2oYANRrj zN#TD{5J3Dz`MHV6{@e!w#D7r$Ao+{J0n)!H0-*hiLIV8HBlFdt<3HYWNAW+-|Dl7_ zCT+>ZW1BZ$`(ZeuH?_7+EdTTU^fa{4!f2&H{{X*hj%W=Xw1umGK&Xa_30mLL+ZC;V zh6;ol`d#yO^@cuZ__?~?xOL6hBQz8cl7S8Y2cR(s7+VZJq76M5$4XC09IJvhVvM>h zKpE3nC>ob@U5+(clPje$Ek=hsse5JEqxqGZK$;Fu>eTaoZ~iC!7xwu_4}%pm2i<68 z!szWZat1G^-Jc^#(0XCQH^v-&xzwO|tW@$r?)4_yBFjtZZ_{IXoGYg`$FIM8)#+Jl zBlvixsDJy~+xg2s1;!44-ECaF!6+9_KNs7)D3bYbo|OFl2WR0a))--dq^`9ZnfRxk zaygwIVp+|PV|5A#zU)tXwUnxrj(SQwnJXUfE}8x&bs{wWGq`nfCx}(yE`!~N*_|sn z2@9kN-5dVWbL_Fgr332+O$v#5o=vlBw`Fr%GGcpHzW+LzztLLSx%neR{^`Ptf$cBH z=U0CVPy9Uk-6BG|1isr5n1i>9Aczr)zO>|swn67JqW36U0m8PbT5&RE-%&XVB_jks z`FkTW9jf+Sc?BBfT~jr>PAV5I`mU55Z`!Wv4j(2Bqpe_$E-^Ak&dxnSJMIr+i2FPm zezXZZ-NPoypjjcWH=75Xc0b*}FPg|Yos%$WQ7 z^GG3F(f!5R?%}DT!`+3Aqu;HAq}$-tDuK`aj6nW8nuv%IZxMZuhxvZY1(6WqM&DjZ z^7iB{1psrCjOKMsWr?^^^gk`Et!!6Wa9y=Nw5rj=L};ds;ya8q+hjFXc|q%tcp z)O6zo3B-F}7<7vzs2OUSk!$go*6A6UeLV2WHZi9&v*S0Hbak*YKd8HAYY{l6{tb=( z7e*#j(@4khrx9@9OKi_&n~Csi<`!r-!oeq|{df9jCH72iTwxw8d!&kgpr+03JQgD!lb41E|xB@Mf)c3cC1CHbx=P$Uz(&xO_(ye4~Y;=gh%K zr5e5@bFbh6uNYUoYP(3QUKA`t%}RJ=-&;yxxXp#u0L+cWUs7QdAKv7l<8KoyJrTI- z*IXyqleF3d(@vdzC)!Kh(T&hG5gd`w*>@Qr=oR-`64x_HTaoVHFWZ&X8yP*jI!Kc< zp`<^uMyxRC-v?JS+&^Pc8yt1zRbd1(+1ZqNtPa`B?}6+bmwACFBpn3AJ``G=Xfk~2 zv*l%9_UM4?evb}<1#QNbhA5eHBdwRQbnCQ!5wVeyF#r*`IAscDRxH{o7fmf(=y*=V z%VSkg&3&B+e&3%OMfkCe-&k6R#^10(VJu)SO>IWQ_?F#V@H~Us7fpMM^8HY|pKISW z78w#waPt%6gb@x$>f}+24TlV|OOuJjVavC#vOIWZn1W_|O=!hQg!WJ>bj|@u_C7@! zt80{9jI>e(c#b&h>L|O8XwZ^%HnW)SzSmI~XPq}=%SglaOk&>~rk4ToCRe2;7`2#w(yKvkIPDdsavT|Zb&4Sk#uK}58w&G#?&%DCnd(=vOcMp>+1HB z^GtulM8P?U>EAE9N5_rRg9qNl5sef+Vi$5nyr$o)kk}Mm%F`Y9 z?k9Kl`}Ld0tlu(dNO#4#Cc=~k_I;n(EwTnv9sSc&7u~8XQk@KRAQ-oMFhpP^RwT4H-Wb{l% z`xp#IYCCldMgt|4%*-U!q>aoD6daAs0t2s^j|aLsTTe*V1=>smcDg!FJm!sdD&7D( zd@j{gvl@18$$SdxbxFw|#1R0~loipysQMrlz`5n@1!KLm*3ntqQOF$w?l>4~g+DPi zp$dTKKie}Vk|x4vB^M z6@qc$6|sPa?8ie2GpuX+xt)U>-8WZf*Whq>7t3G$o|)@eH-dBAgIfagJgg}~vORvC z$3FL9zVPVTm+2c>MPIC?ie7z{EiNb-;sienc6w+MNiK0QJ`9Ok9;@;tYezh0BF0A9E$kILFBE|GWQr#!;QPdsKW-cX zYkY|bqM~_q0UN78jqpg$d^F-q6o3gLd<9Rb*kLRnWe&#NO&E01E;5~a} zSD7#!%=SHUIru3@?#eHF;nc!^t=7pQf>%LW2P>#7;TyglsJ%+9uPsq`pgRh;>CRy7u5-vKJWmBtTT;+ zVA+yecPV&>2;DK@81vlzP8>C^g0&SBM|?O$-+?0*lbSI6Qh5vJ%QiW~$a#%3=jAAW z=&La4jQ;h3Hwq-~I(g%thnp0xez2&U|90!wQ;ODyB`ZII&+HZqY9`iyhK6$}Qcm<7 zZlC>j$lG{yeAtpSa|YrmJBWVb2L%ZT6n9h^2$d?+XoxU7X&j`=B4pyk?Wi(IN|oeO zCY16nEk9K!mAn~6r>Khs4c~JhQAIM2vl%841$GJ4-UHLf8Y2Pp&DSe{tn2ITO#5Y4 zgcZIg2VnH(d9rmj)Wi(EcqJk4A4Ijq7RTX^!h+Wm*CFi9c_MPL`86NG`(o?IBR?r>^ zB$qQZ4d_&37)@8xGagAdvtcl&J+rVdJxDOIF%Rr?u(Akz7i>Lo;2Pmzncf#^ZE3ET z;yPi@mf$o|^#-teu(aHPC5(Q4-H>TTMH67*I&A@2bUiS%PmCj8UlJ_>@$zD;pdp9Y zIuI15gDp|x!4-toU#B$IG8D1Tzj%9?5xA(u9FHi~ucjr;?D4^^)vnXQjb37I<)rcx z$yV3ne+H$loG_#5;A_lW#DX4pBvybf$!*H2C@X7&K04Q9BWphF$%a&T^wZBw;|~iq zezhbNU1Pp_zxdj(`l5WOmpv-^a;Gc%<@MwG+=@-v!#9;%NtQTE)~~Oth@A3(-5o(1b&LP-WJpVC0M%ACD*u;FZ3--MK{EW-TlDl>n@zZr_N}Ipr0h7?{`A$ zMG?bDHBqeMZpU0%nU2(!HmgesthlZmKV?|z)Uy7^{QK2HNwoJmP?UUgtyM?N^L4L| zuJT1!-wq*6TK;7(ox6U_BK+t}FneXz*E=U1bze@x8y*+s|10f16TaaHnCTG)jhQ4W zzJ{!UVI?hAFCBq+HW0P-9K0AbM($cxmMjWGB>6{NL47>wpbBql$)t%4`3e|P;A`}6 z4T8^A>?4xt;o2R%F!4|LG~%vx7XZx|zlY+2U6d{yNV5-K2U%HO8gi<I+|e=!n#b~(Y|#rW6ZCi(dy9jNC8$+Lz3R{1cq zcRTdmf^@^w_uY^dMM{T(?Ss=ht@e2vg{S*?%13+Wc;Ujs-+0L0J83i2B_>otNpKJ= zi@5NSDp#?JBi|?6{^@xgN~G;OQ5xhODP%en9YbDL3>`*(y?V(?)1Q3lBjpTH)LbJ^NV=q=spbIkYzjKvx#vh zqbFB>1b@!r{Wa?2LE_XbF-H;Z#aRAWV{2wzYC*>%!v;dIQYoQmt2dBIa2EiW#c`$v zbiFW9dg-Gs(yn)O@Wyl2x-uBLl!9J4Mw zO%>Nq=zCsy9iIz9~ zk6ZWrr#?A3D#f|X%U=M+2t;Fc@@~cG%V(+)Fa(S zARU)ZzlL=Jjq)U6zEm_!J0N(qX80oGHz96ZncT6vjl1@)UJ!0$;DovHQcC_(hF^is_noidBtDNmEPoFUqcgC$HMQ|G z_?;pp<%fyApQnG+;mYX=p0XBkj8F0+ImQg?QW3QJoDvc!RgqB=we2{c5G$ja1xWs+ z?h&f&bO@nTM0qr*R8jajlYZQ4fB%?=c*OQ2XIr@RDt ztH~*%VloYMG~P~W_ixZrAJg?T>#TEu>`l5bSGEsyWj;1zW(n79&Bo^Zp+34DX=fQk z;O#ImPM73r@t8Nm-Kt))&}Eul?csH6^SV5*smEh4JZI_Ws&CFp-N!TE(~H9d>26-c zTVx8NpV9T8fnJ`T3*h{%H?g~2tHgY;Re&77)vTPT_tHzi6i7ZCCL_qy98Vy7_6cB8 ze`M{>PPPQkRPv{^JwzAP;CS_%ka^>jKUDEs^dq(fC+ueO+5>Z&)hDc)6V-Z}4l zy&YmcP~$D9-raCRE@tD6cXs$p^NsAc>zI__vRaR{zK@JfZ=zq8+==9zeGO%kpS~7B zBC9MypV=(%tDfJ@5 z?R6X?%Wyn#|Dm9GTqyldf;iwl(U&xY>!^83f8%4Nw~ps=UH1zfXSD|3r8wCA*2>Vc z7s{c&zkilzFmrIWkhJvcdu#EplWz+R`^POWiT-u-!v9RH&Ccg|K}`%pF4C07nSG{? zu#V!K0>}uBUXQ>Hr8}gKsK8fc)f4!Frd}fa*$oTF3bCssh(#){IQk-Bi{@b?TT&M3 z%e9EK2uiRK3xYVE{);5)vx^b5LH$=4yULb;`ro~1WSo3k1h$7`IukJzzFuiuWX@Ls zs}{I|7km=PTiB8&8?%z!7u*i)?q)9TTuPg6@QXl!R6HH>y zBh`AtF-)+~@Sdj>SM+sjqQIx2H6S?1A&1C+(n||-3DkC zrBMvKlD{%9;}#w#jY#dzH*ENr8P7T>`rJ4>2M#>>O6E*+1t2K&H@2^%$=c$td%^i_ zOWkzK*`SM#%)zLu_CU>el#Z8D#VqBWiK*#mx6gMj=?{s&oAca!*>7h&v0Wfrg3CK=4!V!YQjQE=j)xYp0B-KZN z%}kl2zPa{uyp2W?9DGetg9JNxb>T8BA{6U&$H73tH8;wkCA8r9V~kuP#fX3AOKpc@ z9Fqv;b4e0k0%bGO>(&vVjE0a3D8C}41bpMED;l&MfAZ+lSgLh!Us3L~%yvQEjLdjg z{>Br6>cZ(`{hQA|KVeTP`Z8@EU3lHvEwALu+v0+9Pv(|#XofgnQ@uGu(NuLw1EBa) zt^r=>p6g682)toom?LWQzEL5NL+zn80nx4Z^1cXs`*juEKAY^Q4!0ig=x47T8 z>xc+27rs&!lgm_Y*<60J8f5R2wXroHoALShXEGR)d3~t{V z`IHf%WJ^nivADG2wY_>-6Ij%#dfkr{HxR^t5`MkNJNts@i@dM9ko+5h`9Pcm_5l4G ziIx=%8k7XU{id?3}rmx1X78})7C07Vg&ws2OF%- zP?enEy>A*fVO#QZVIrlfRY{6=ky<5^b}AkPDwSPbBf5^NSrz(@n*n+Z9lKFxENU^p z1)4Vnq5-SPAk*S%JaA{VEfcm#`z8tuzRN}8%#zx1ul-^D&oqSL#evjkAyOBU`mVTW z*SJ?>M(!bIq^>3lP8o|xI1$y3Bm>;`7c%vsRU$Mvn|i9Ktx0=2?cE$rEPKJjR5Z+K zG*0fp9=ONtPu)nh_oqC!1yr>^OadMZ90l zgoF3_A4Zmc1c1XI{T*t{AUkQD6FPAuLd8=M1CJ~=W;l%s&h!Lrx+^k8a@<-WL zZ#uJbr>&4ac^k6G;4F8mZ`TSurZ3-re(erLZo$TM_@mrL6_ z+u<8n1mBFhG+8Rhs{V!(oHR9Z;nY)0k36`fk4!5$c6B~{vu-e;4jvOaV4pJAMBi)I zry{eMOcWD$rqRCbgqT(eAcyq(G8T(2mp*Qo4T zI=5IAVUPK)276rzen`!6v%e~zYmUL7Z_+1Qbm7qmWW>f7?S+F2nO$7STv1(?E+vb zr8Y0E4-$|)0h=wyBT0gHl=FRA5U=h)<5EL$p;rr$Q?4MDdX+4Wx8hB%x=T<_E?9SLl;5}M9c;Hp5{wuj37s&U?AG&A9dI%^c^eL$T( zLm?%}N=nKmed^HMHN!Ql!ug5Yujt&|X{K-Px$aY4!Fg-Pm%R%azP6 zJz{L%sBIYCB`a#iAv48*riQ8x@Rhras?xLZ7{Zw}j(fvbJo|6l>92eh%$%1FspI+b zjidS4+`m-d`U$&1*7oglho?3^B>E)JOH#dXB>jaW82I*A`|hjN0k^~hEx!H>R z&@2bRAgueW#^lX6b=dm{2d3qtm34O+?lpz(-#)!+;9fbmu{ZI~fzoGWVgK~bT?gTX z!oNbcE8x}2UJz_U37jBWc5Lb3Z960Q$v9+V=xG7=As8;>lf$0O4uBI=zxNk3;@UUu8qoV7})lsq{XHCAuZ6DEsmx)>9%XE58X@?3_W4ke_ zyYNPIw8*TJ&c*q!xgqlh$-rx7=92HcStjUmgYCM;$vtf+4tb;PC(I>d9V`zCGF+w} zhduO|qR%bxoF4DXcD1gbD{;4}Pk-lS8#H{|myD+8r7S7MRB!WZ1c+Cu_&~J0oe8oQ z-r#Nn$jJ5GiMl0{V`zu<=*nIKx>S3O7l?JAmGpDS#(b%JAcRJ_%RGKwRzVAvR`Xnt z!tT^gJ#{A4WTMomx~J3R^2K^0melzrRb~Qu-T5uDAa&wFg)>^@aw4wNFy9Hhgy_Ir_W2PHnZ7!YBpIezpL}gMh;bbhXjqhxxsRy z@6ET9iiLWgkm6s>H!db)B=SZs;?Z?$+tVKzk;$t~hogNKrJE{)pcfEdGK4VI)rcWFiWJkJt|2k zQ(0A5{fpE@DgoElr8J?e2PTqBQ~^!oOg*VC?%(ob+-2d>3`QK&VJ5BLZIRpfbB?=f zE#DR0w_ERWRPG<0=WWdXcF6vXGpL728UKsDIREPcOkWZXl|2L*UX~FhSS`)TJ_bk8 z64xKG;B*2iM@x*_VN0K`*5OzI&fL^Ra`mNrUD!077HCY^{RXox1mS2_(%{>u2zgAu z3+cr$BH-iK!A1rgS*B}c5{9nhCk~`dND{fIXL$goa5lMe?(avJ4JWN|stzYL$88I!F(>D${6E_@9o6|5c zrV~;yGo@1#H60tHb2J;5bTzQHp!4>(nK0W9b{G#lh{T#53aJ``eBv>_u!MYes<%!l zFZj*1Fc=JT0uk&yHtw0{>H+zOlL3TO%;gSGGP{CV3@QL#No~ zo)aky1z?I!Bhrwf`+V>&lb-8K>2*snC89>6cS zmIqvv?Bz<1nk0U%qiqw}ciBCCLkdKhURf@Ax{*v6lIZcYV#p1^dZ2JdY`5*btK4n+ zect)7%%8RFF#jgV^384~Z1;UrNmsQaZ@~ESUmS4w-aTht~bSY}tNsyIQK_q1^uo#}C@FL0u!BgFqRTLr9{6YL+;oMGO(W#UVJS-{ue| zZ=mRKr%GgnQQ-S)uXV13zRiTa45Vh}_UG{>#_RBse57)r;%s|OB9Ecu3&El2V)t=q z!U7$yVOpTM7{jJT8RMJdC7Nk*xd7zSR13gj^f<_p@fRY8Kop%SP?hDt#Fs53wf|a8d844Bm^xDnd{jUHq~znz)^B-EI@Kmdui|me-5ntd_1}- z+r{L@)Zpm_?NxNNUlQlps`h=c#%X~CPV7KvGWOfZV=opy&hB;AVy%z+to05(`&O0W zpMJ1zsPz4~fBk<{jX-OVk}ts7^shoOJ5u_O=OZv*vU#b`+R<$R5sd8jdpg5;&}4Dn z^AX)wkFm(sZ74{tDf#fV=p20JHh}SRS-W%~K;icxyzjGT@YlmzYWYL`y5#jG@Jo2J z7D>p>QM6PO@zf+cLEVa;620adSwS(_Z@!ElSU=YUa5nP$nn>kXX}yvypF3w|zkS}# z?OY3$5@yKg-Wa|karRsK#oXrZ$;s6Li-DCLC^PHbz`)G$xuQ?u+MkT916a%VA8-Ud zS)PqRiSm@3NNLB5pY;7;FCV#5@~i?y2P#d4phL?FC0khpJh(3W0%UD!RN+r;t62pu zQHiW1kG4l(HYsTD>*4?r4t!eFbawoxC}l@APcBjSPFdE)b`X@}lX(s>%E}ajv#PRf zzYt$&pPiIbeWKAVlF;+>8sqJI6g4OHDS356M*nrrf}DYP{4a$8lOj;jP`r~+Zr}$U zor+O_9+SqH? zLyqnUVg%BZMQznoof#9aBd$rcec z;4+m#o~vhT8lB`eJHh+G*Y1`Ye~_JY^ujIMpF$fU_6^>M zlY8Gh*1l;3{>A6ESQ<;y7-kZ zOCIw~{<0&{!<{EYmD~V|XXr3&&2ieIe8bLK9n`heL}(e$u_ErnP3<$@MTcU?y|?cz zWva78E;E|bcq|76aojh)_&+z7{;w@kc<>kM2|I|iY>|rfm^ctC^d5cvx#??Dtbh09{}|@%nFYQqj8C z5m+!ffEIwdd=p_B;nW0SJVEyv83E28064HF0EU$>lpO_do3fu{s(5UVTX(%2!LFN+ zWDPkA_x+4hz4P&RsJuVgD@jEGwvtj2z#X;*(Rlk)t)a4~C!bPWUI=I#_ZBeg+^j9V z*z>x%O}OU~&pSa~vC%$J-5S*iNxhNy4>Ec+Ioq-WH1gjS48&c2DGm&0Vw71OH}wH{ zyDdA0B|4fuh?&ZA-M<}V`c9YwDt)0zO3tNJ7!;3IGBT*+1tn3i&);TaI^;aCK^!5+Oa5Z#3Ej~=+@2I$u3F1mD@tUBU~6SsQ5TvS=UQ)d800y`fnH+ z|LooKG@lJD*o84OUY@~v5xkaKgJA_27<}QN(?rV^BJ!q1@LTEoX~D4&g%AdpE*C#0 z*>kDR#7;MVrsif}+$lq{_mI?5k{aAlFhTY<3x{4Iz=$H4b0HSfpI+H}BQ$Rk*xKjr zH>BE@z?!9bU^<^Ql$X!14khIHJ6HrHL|RVL zb;jCHnoFkJSp^AYxlfHJJo2y(N{?HMf?;6zjHN9RiDZE#jB%RhBF;V~{-Qe7P*C}t z=RCdIibDq{sAO6X0;)XqMZh>`XH@2c>16QWXp$Ac2j3%&h_>;xw@mmFJbCn#_rVIi z_rk*!%VN1i>&!j+e`O-{e+In&&pSzZFypFF94JmMxKI}oeTS!FkQ?|#98x>BZ7yK< zY|5q0=_X?{g*auxN9R5B2pFTb(i==B&^GUc#8Xhz406GHaIY;{I?$` z_Ux^ZB@4T^GDg;SkN@cD(#@Td@VgwXcYn_Qepf|01-AH%oK_$ZRg8EP@?{yG z6|h3$5%A1mykatf=mftrOHPid>P0MPB6d`#SqdKdUM48V;kjdseCwI(2|#srDcq{l zC@U;kcao8CM{r&hwfc~Ij@AcR1lu9g!rX1l z>!R80%zi{)vx6dT5jGR)XCY3OQbN(r6HiT$3p(oxo_sf-bxsfXewX0K^+|M*R9ylygW$+ zU}?;!58b$f?2d9L9?p$FlCkr%6XdM?;BTH4-)C$z5m?#hRoOGkEWKuVj!-Q>ma`tF_6 zZRj7B3eElP9x}Iac&c#Tnpe8LejM5gB}Xj`pDGThB92id&zi|g0C9}kH0jl+dik{2Z2d0*r5 zMaGC$flh94)cFUB$$d5vphYk;k7nhI`!ayWj8_RhI^O0Pe+txChnSMR0le5pa~4#G z#=;mB%mciySq@0n1xik&r{A=M48~gymS)@j&X)9SNlsQ!!m#z!!C8TuwUkhaXAuDq z%B{=^RtXw(@}DVB*L%Nkoudz|4itg|!RDP8I-+{gU=Ib%U}(hcYxcVZp8Bsh08Juk zkcenJ`$g^r6u7}_7cl$NjW=%j@na!RC&qVu3AT;f6Azj`|2yo*|J-8l%Az#|WJr2m zEbxztdg?q=lsa>_73+P4jGbgErq*$%vDsXjCaG>!=<`G{>=;0^F8uPimbIS~afN*# z42dN=9mcWX#VBm?>j+Qmbpp@t+WEJdYM&#lH%w9+e0?*wlIT0TmYif*uM&!9e1ekG zuk`zEfAUmB4u=0$Fvn8LbRrc-n9PMw+c>nc1Ayx8@5RV>nAm*Skf zqg@+Ity_8-jrb6$jUji>xgo+)uDRD&*QWJ(IEp~&<3f81&m$TwBSba|pBj+G-(hT9 z%CEf_r6btAw^een$GEobFL@O09ibhN>@)GIme8$-bc?5)Q2AF?rS|$0}(gXoL2&R(hymnX5u#F?bUOuf5h}8Be=Dg zQa5aGgtJhq@oO6WNZYj;PtWzXouHcwQyZ@*2z0cl=M8qA_z@WEYFY1^;%p`HClz5l zA@ch4IiD@x%FpH*q0ggJs} zIIo}(VVhw*^Y4zq00zjRud2iFN)_yKmg>0wa)BCY66K?zvx_%o*4vHol;Q&QM}#{- zw;T05;Fh~)5x>`(S{17WgX&&wDYcH+suiQo-d3fME_Fw{c<+dAx2TRk>pMxkecK@! zeVTx6sR0_nFADwQThOd#q<0Eq@PnBL#j>a>b^k$r5k0i-j;WKB6&4{!HCU4iS2i>q z6PF#SgEfjUsB=eM%Mz+?#x~&jA1tBPZK8CtDo~Ri-32*5Jke%%P7Rp}bW8Lt(f+laXfL+q6g{NW3fYFkgj#}b$% z96uJDJmSftOFcyqzoABA*<8nBqp+C@-I0zm;dcIRGQ~DeX5aKQ3x(-m_vYs^NS5XhcjCTKi@5I2oDVX_<7pG zad)Hi_(X9KGTK^wX-Q#0wyf|7Ws&F3jLJg+uFRdE(G zPbdLs7{oZ`F4Zmr^|O-E-IqRVP&Ioq>-ws;i)o0uj4<_%2#$&B`OD8-=^JrbkkF^; z-Bsv^T>n?~_s8@AGVQ~q-eLTdr`w-UaD z%y)mrN#SmyFg$E!mUy=KG2R78Vz1IgEoB5T|IPs%(aRAo&*LtP8(Z-@qt(FCF=^fs z!{rPDS{$jy<7-MBJ-igUBlAp&RIzRu6|T|i4P8F58&*%fjsLw8+lXh2`D1$G@)OzT z(7M2}NbsH78DDc%5c>7C3e@%p%b06|&G^9{Pk-;OJv+)2OzwIiI_48r; z%yIme6vdJVE)yF9phTw1mtX{P9JSFjf0}_z&qU|g_{Cir`9x`f?B1S$+ehE-niTbK z{*Vo|AFyfY`5q*9X1|ftw6=RE#J-ZUa&G(Rj>2!@q{u%J-O#*B#r@vj?~>Yog3W9B zmTxNZ(Gdu=8t}}n@*)%7gxT{H_FH*}2l&;FT9qVgtvXdeH9Z5Yl^bHI$R-g4rsjAu zL{Ozh6~(Z?jQblDtIl-1B z?cFGMfrcQu!*SQVS6`gL;{)UvlCwANgcSU~5f9M&3{E|O!WvCh3J_Q(H-Y5|LxQ8WQ+l!=0BsE$-s-IH!HmkjcbYD6x-HnnL9_b0#jIsqNO-Zy_7 zL#3iRG^~AMebin9l6qpHGXi~&;^$=asVy2>~UkYmWl&X{hS8xlV9yAGC*>C&Ai>pByl>>s;7$*<+Po|DvV(6;Ek#CT7R zTB1-N!rfpzFEfKsk*AHccS?}mVdu&%s}idpm(*O2UN7R>S-fn>1CellWV?#3Az7hL zfD*|;?ndl=#ZsS*vsIfI!{RM#GtI7gq>grw~-nCK?#8Inn0XQtd zF2;*#WW2K>Y-ar3{qTv2v(_*y;|1j7f3MQLvgrFNc9n<)SSCT+aXCvL^dm8PNj~E` zHyDUxYnQ68JaiKqg2F6*OpwQB;&HX>zE#dzlfnZupm8gc5tmT-4qzqZJWNF_B2a;N z7+}6%f)ZA@5eI1Z#g=`ahPet7?u2-e;MyfyfU_ruA zLS8YDE@73L9bNJ`F&hT4u_6J##>>+B``Ih%BUv#}wNW%0$3!5Mhr0X@7BLCLVtFExZ6yv6}j5e3Uztd9kV=qk_|cGVT?bdM2I5I zI|424iE}muGGww?1wBSQ4u*35RtUs^O#iWrsJxx}nO#%d2yt0v^Ki?olM?Jyk$`c=|!Vmt^XG$OE zm}=&CWX|5Qk-E36?A~BT?FbYOehIn}ZDg!nGWPjMetu{=;@#qQr2G>$zO=#a?>`hT zRIETd^yX8_Gt!sDn5W!d63L$__l%t=1lU#XDUC4w0)-nSDI7y~K#st;Hyp9G{6UdU zB&Eu4et}Y>(h#MLUAcg@jdBwsI>D?0%B~;-4!W)k4L15NlWaqVuF;n+p!2dbhN+jr z_cn))SalLt@0fuPPY)P~#QUNRvQApv-Rm|=Pw*7H3dx@%0tv-m3b6K8x`jy$WNHjb z_U&sFN)3L$8;2(3P}e{+$8H?Bbc8Uk$QV>*&q*6mqaadPb0G@#k&p{wjWKF>8m*Cu z7#^Lmi9r^_vFbHCL$eyvI8oN8+1t$VBPY2^YOf%9wCc;G>MG{u#VW>@lY@K?<1}42 z^k?T)tvo2>A4aqq;`g;K{-f&jahtmIPfdQ7&~^ zI%uatEIZ5M-Lr@4Qm^JbtIx`YR`}{hcR(5CGgVCS@^_i&TZAH6gUca*Ik3KFD(YGb zi&FA03aJ^{iHy3XLYP&t21Zxd4?8){?i@!)Rdo{>5Z_};6y=8|b~y958vaS7M1Um1 zTwykRv3}5kwwohVeR~cX=tZdMPu0>pg@`)>-(YlFcF_RK9Z|7tX!%GJ2TF0I)Q(mY zJ7C4oZBnX0ubvU@&D0Y#;m4+(aqi0An+Z$h=-nI0;OR5P+g9vZ~|J~17~oIhd; zb7sOp2}BwF>|tm|7lRh1y9UG%Jbp z?q^&2Ho>H1#>bBg>d-^B#3~ZZxaAR|U$N+OWG_s1tkk)(S64u0ERox>7H}CoME9om zzoLg8&+i;JDqmhhlb6>}+T}I$dg) zT&&dI3j>1G7Mh1k_DEWGsKrQIu&Y8S^$M8sL8lnE)?tGtgH|s+VNwn)DeUDH$}aSP zA(K}5K@h6bWZs)qYi}Z+wKo{~_cav4ts86}%ln=(x|r>Kh9U?iR2)ID`8jo+DVz9wsq)H{R5WF=W z%Aq0wbV%;0jEoNw>zh^=Qy5|;o8{EUs|Vo*W??KUrsh&YN@f;04h~@SkcErQl*OM| zQJQWlb}UTmEIf)8i#vvwuR%b*|EaeN5jocypXF4JVKLS=N3+CijyB%UXHGYfQ+uFu zcY7huWueIWLwL=k?*i@GT-aP)nD{?9HS_Mr>BX?6bMVzPcO#s6CAUXqsGc7IqFhfR zj-bWyx3LnV$H#!;#_d|0t4Ad?Z$~q#SNClMLDVI0#Qng=kJrjDd%|P?Lo->fS<&d% zLz{chiP77l6h`z3drlk`(;Ko(%B&p@U`5s1za^{7ZC^c*JKqkVE1vvvD955)=aWCT z8KI!G2v*dmgUA?1a?jff)8N$XVkh^qi~13!2OX{aiKdPh|H)3ErH+m<4Mn4L8t zaKeGMD?`$WrDt?ipIUowGl@epm^qfKSKK$BuP-urw_-&?N8uyC(Yr7<86(dt)ctt;779L zL#bt>mg9K-h=-)j1#6UH;e@A@O|yrO!?Q8p&>zjWa?Q$Sd^h3{(3F_q*zCeN&{ARA z_sOsJabe4}``~);sojGjg|MkFVKYJ~CYj%9}k^nEY*zjf>q+$9ovSq60?hFR48G9~HhCB3o6d5zKUyvB9E*3K!3-KM*H#-jS0gO${c&7|Vxu$Gd|3U-z`g_`zLhcQ78Gc-A%O?OHt$#vS2=7#%>)J2XU zHR2DFex&mNCuxFU`VSG?bYE$9SPD$qnV2Vw`t~Sx1|LOT8u&=Jvc3MZ&a8CUmaAi3 zS3>kG-NJ>3%?YZ&nPD~ifh~2$DUMrb>hw9W#q>5L>XW74VbZ6QS^<}Tt_a850Qe`6 zpYB9p=zp#dIHT`ac`@MRIa(a)UYrv_aCy=$I?(DV{%_CQ^ALa9^W)w>>#a4lwgRa4 z;;Sq!!Il^5Zll-37#odB>`KQsj_g=oJ@zY}_m;6$T5t4e__!&3)*Otx7gCRzK}b6B z!q_R;PQj;T^tR|#}t+ZB2=4?Ikmq+ zlmU5H$bgjIGt4q6{hj0rZkYf_O7oeG(=P}cxDg?d5;&eVn`o5|I!as~+)Dw7Yb?Z+Fa z<&1>}q1mhjIbM^nH92Y9*!#t<$`T-6irWI|*UWK?dyU7BJ1Mh)j@5J!6y;A4>l*i}}YvFa**{b1jB z@nO$Fncmv-N7WC#wJ$mXHb z89Q>%F)*EqDb%nHcaZS5-A~7;=7(4VXySyyCV;l5tO)~uWzlR)J04%+Wm7v?=Jg>r zXW7>=w{Ru;<8j$(#1dUC^!AdqF!9~RNW;B&ry2Ld_+mn!)U6}o#`ZTDfv?dHJz>4_ zXgY;Y6P7Uc1!s8dWaAW;>PVm|khtco2?N2nYB4;PHeyQjys^zz1T~c9K5o32a3rN{XrX7vsBQ$FhalmY zp9*-%+T3p2ZjVIW5oyQ4U`tPUXJtf{@p;+6szf#_c8?PRRo?oqK0$f>*@TyrZH(h% zhvFG;X-B70PuF@gar?1 zkHcDbA|jQ^S@-+#i#M8;AN(qr+XT@rW#inU&)X+wKYx+G-uOFW7`&N91DtH0^ zTk7h-xmyRel#4($Ff^X6JrkM4re>-UOV}qa`4|-R7=xXeUARg`88xa(v4mQJwY+q~ z6?oJmviYJbdg6+Ok^}qZ&o~Fan=eZmg@k^UGp4fNkRHx*yHFb5A6QYsnl%%vkAxbc zG>s;Fe=N}RC@KTY>7y7y2vQeMdDmdZoLcLJKI1A8gkXcVqyu8@*Ris^!<_oLOc0tM zID(_48r`DMoX!q|I!sT%;Wk}IlM`%R$I=$E5Fs=g;xO$u9&UGBHW>wS{)f?vxv6`%dHK9ESPMn>)3H=R z*BAxkGXCHv?BY=U>R`d)lFytW<(fhI1bcGT%M#C^dEc3C>vQHBgR(`Wd&Dh=8}9kZ zmf(#9uMV>lRs~Gb_9-wyHmCQTZm<1CoBG_tR_Qpq_>X^3C2Z2g8<>DoI58-yBRDQg z^~G5M5ssF^iQSg~D*`8QCI*2?ZX^!n>dGGPL92_y2CSx_5chO2siv!Cc?&$R=3b9| zIr#8cs|38AZI7gd9!`7L)%Z&>Hm|q(H7xlb&%OC8c7mOC(fNMIG#{ctWB>WJPkeeX zZ`(9Edu6h!+wHT*5iM+lx0^fnPQeQPoIAU^f6@$&<@%R5&Th8cPJ4I!S8iM97TvjAr=h#>M|+*E&TiF&XPZJ-J9 z+Z)ty@C^xusu%GXi2L^O>Pk@eOM)2esMz3G+@ci7A#O< zxIN7A%C@gm18x~rcF+$;&snEdCC$q|6u56sKiD6(U>S?&z2{$!XGtC^#~_ku4abvR zZ5+i?R~kHA^4Nwk$lawO@^W0ZY=dPUT(ArXbh=5BvGgdmqj@ z483-5j)mUinkz8)r1ME#{pDK&LyQk=P}t*(X|tH7Z%6WHw}wON@C(O(#k^xn?pWFY z-Brda5!34jXHh?Qz*pF}|9$`OEJPX2j3Q_wkETAy6I<7Mm-9m4MDp4|pj)K}h4nC3 zC-6nMVzteO2q#$e5@3Oy@v=TZCp~3TPd8v7Pz{*(X29>2TwHq4gBY|cVG(RP{24e~ zO5yvI@Vi4kFbYEPKtL?MG7zUmnQ5NP)`Rtp7tya+O%NF{gEdI>hZyvT_Ga0S${3nS zu80q1`L4+tgAK6!Kvwmsv~e{GqFTKM@b<`S0fE=rxkkggir^ARbY^)HjHhD#)ES>b zKyj_>zMBs#$$fCb%!tTHC6*hW^n=R`hlN=w4j;z*SX%63F@ovA<1nV!%5r9^zE0o- z_1>b{+s1OrJ;-KCYB1N^RyscC`fP3SJHOef!ZQCk+L)?dz5)BEXdMQe!y1{CbEtvnOiG=pd8ByZ3WL5=OX?_#2g_V0(^4v_V zM2fxaN2Ld=bPD+Je5pO0K8`Y@oAAB%2%+2jZ0dNbn_SNH12>yj!Il5qYwho4)vOp| zla5a?9;3&9VP+t4HR~{EjprhG1hfHimz{k(!kP-W64MPX_J-D?a5;R=TMza9Bjk#z zKUd2mKc<0s>b6qSV6ob~tK0OOQ=)z6UNh}?@Gus-7p>TvNE;vw|H`@lsdMImvQbe; z^}?n+^Y+7)lF@}Q1qDz=FPr;(eAdF+llJbzex%~Xj!5$VB5SJtL)L&<3IDMI9%9lA zkf*J^%NVu?o2s>#3J3*RTMD3xXe)q5{^(@j7C>Oxpm)(i#K;LSagRcg>_?Z00U=Fm zMzjO9W=6ITqm~#5){^sNJqjeyT6u&a^XPa4&}(U=(RS~}QSs?!3_N4fGbwn@q9-0* zCe&BCTJk?uK*2%EmxEG+;+>Py@Ap%hr49H0ltbR%q{qah2$nyWsqE1?RH6^`;RN6- z1_!8wAjvH`FrV+UuBl`UpT22Tn3Rse_oq0G^lfik5VgM--kN@lq04;a)9g!@k%M9d z!?D9+2_vJN8c%ymi@_TPR$*ng9jvBkQtf7rP#NyCmVU_|Hb-Gm-c7532U7=uY$^%; zopA()<*s{+&~UZv9lFseo;P9Ew5bbWGZCp-5es#9*V*AK8;`Ki&g4gEC{O704{xjE0GTuvNWyybQN!LRw>i_(pm;S@S2u~0Oxj+GY^Xk}4q@yti&Dw`pA=g@!{72lX%0j(T;Eq zdwn!2cKvZU;H>;$?t=A&;me8dvghD3^{jzU-@q4Du0rnQT1b?Va;q6=unzx=C4kNq zj6jZ1Z2skxFu8MZ9>b=hGCg>F6s=s87SX?X{+0vl+gpbr+_X>n4LhW z{+?*>VhZGh`xCjVL5Mo|9HTHXmsn6$GoLUtQMa&AasU%`c&tn&rE;9?ETziAEUjp^ zB5bX4&SlwQE@mX`kk|rx-aA>?zXFHBp|j0IEvn4fj<&4oy4}Of5ezlFS$US_8eCLp zv|P zHXm?Agze#EQx7htp6!BPC>l2n5H0msCH+-%Le7vIlX-c4{3 zsqQEW5h{-~9uupTcgi6vIUq|=8D3x>UDt{8olhYEG8YM-b;O|BGhp5*wV+M~rjF54 z6}Fx+WEhjS*k%H|&Uflmo}NtKWIpZSXwb5f$z1|I=?lt)`YGe;W2p(>lmP5nz|Zgj z90bRLL$l*(nX`0p$lNT0DuF)JgGM3akj^ZXq3;(EC1d_wJi17Ao>Ak@U>`sYp9qSh zk>P=4lW2vkk%9*QEx6Hm=moLrxCM`#$pnp*CYV;}XJR#(6K`yNX%VoPIyel&PJ4gF zV;QP+@CpSI>E^+M+yJku(PU1L=X6b&kC)ZMG7GQSVKIDodbTMdkW{m{DV7M9PCs7& z`$Hs@ddcGIb((3GcRscjgI|3=9u-!^I0=6EM?1YPu)GA*r2#ZniCtX>N!m4noM^$v zA*CH*fV0S-<)Y8ok9GpLCyZ{KN3}Qwcy@d_tB40GjljgU=hhW2U+;Aw31_W4 znBfha9YpY5(XQ6vm3<^0z4GIt1DqEA>I@t|q*M>0B+_}fjRw$Uh?n*lI?rf(b_VSf z``kJF%dsIy;{}k;)ag;d>_jh{MAKk!-OWWqGmSy1kx&|__{hFNo94iQ8W96^ zMSk8j%{IU>K(hmK;;Y#OSbAzt@KC9ZPWaK7ndc~JYFUJ7YFkZaOSzg()|9zhS%z5z zfTH_C3^?1FylXy_b0<=ip<{FXMy7!%#%;E&4}bMR`LjQ6jL-}w`&mz}#RtrVb#(>U zxkA$QO<%_Pu{?gIVIPPL@(Br=K882moR{I*aaoM0F8BwBhoI%|?*e-``#$!2H!GU` z8tlzd)Xi8lE5F^jq_l5)XS-Xp4uDIyaP-erEPf4=`twznr2a}#y_eq)h45UFC!^j~ z@+jUXBTsF(dLjWZW%upu$2q;!0b-o3N zC5#wybt*<@Slb*UI|27T4*;4@2!6Go2gD%iyDMjC^$IYLoR^E?h(-D?XRz%e!Z=gF4I`bNyu?6 zG)KIz4c;8LX$_DwWAgwsO?>p)6_EPd6VW$=wJHwpuO2H2!61$VU7fNwsIy^t+SCQW zvjP@zNfxM@@%R?R5No<7qi>eorDAMWJ*a9n0VX<`OiUVzSWQU9n}F{S6+JoxO{E$UUl;&W z(spMWvd06GmH6)Y@0O2W8Lega|930z9>S>Dl6%?8SN+927>RF%lI?&Q(WF6;K^}-0EbY-LNIPuJp^=&3yO6FLeK5@ky-kvd#a?q(PO%mwK{F_UDSphOv$kNyX_?|0qfz~<9 znOb1OgB_C41PeroW_e(kc;-dK^uNzH3-(v7K~x5)SV)yC*B}VT?sA4d!%dz7^0!7` zKwzF@lH3=n*aH?h!;wiiNYF^3^i9SOm6X8Gp`V$RMNW)6*l20`SWHcVl*4$<#y2$3 z1>-f&N2BrKPq1i=6-%wyhLp4i#LaaW46L=VJ&X_YoeE#jbd+?1Jg^#l34!^JaaOEp z#X%e18O+teD+1;o)_|6xh{2j0vWfqK>Y9BIA*lvGgV*6z*Y}}l@_DNhbS(8mJ0?|7 zP!D&lv~#{pM*oQw-@`r+f4Rnc;fqCeUcvb>yx_Gp;3>(dBpHNbbbas+%L!#@uDqA~ zh$+>m$mY)p??|SLqhqUXP%cwc)}~5p{{%8LZzDurfaVME+A5N$o7@d=WcfuG)dN!R zH-Em}xuo8~>SE)Y9^E-Tz0Ick2||Re;DyM#S8xHlUKMauB@ zY=%(jh@B@;_EEx;{;RrzLofR*>Jl!!?}9ZT`M3F6;L=zr^qz_l;Y`5YNy(nOsS8Nc z8R+0bb9{9$QaKVqyk_F;a!RBI1x=G`2PKV>8Xiro*`bh}nYomo83@v_SbaJG^ZTT2-FgLUT=c2nyiTpnxYd5bDEzVoCrj@IdIEU z@7VxQM`>$}Xy(B}SLk%^!Z0X!Y<0xW=l&r3S9Dx`xK}}}Xbyhr!F9w_#Or_&9)eV^ zy8u50|E4PLLuw}`!}=ZoCr-pdhGw;TCAfk{>?JKPcp42u8@~C;r#k+*2F`zvY*~b> zJYS3t4j`yTz8w-cGn$@#VZvb_f5IffviZ0Hc3AG|T+G`>?}nMfqlLG%D`4ctVU#jC zTk@Nk{p0h6VUZWJUw6-%ipT>>);3Phe*Mn|m=kE--a|Lf%j7*d2vITHPZ8jAPr zWQH)14Zz~;__@*zQ~@Yj@ac!ax-S%DN)-@5M^Uplasde=sMbI|iJdniQXMH^B33my zXXDTwNlD<+so1=e#td_z{R|^yp#Mw+!))+V3ui}gf>7WMcKz>)&B6xa25sVolp2ko zk3w)-%rJ|2PIAbsbVXr^YG6>waDV=C4QCi!-k9owP;QiD8L)p4wd zkt7}5w#2BEq)o{R0zg){0}@$Gfo_MYXpB^&h?TMXH7-^olg0MmnPKP^e(IHt0%w{P z{Q?Yxv*b0XQ}|`sOg?mXx1FtxFS7d(?ojM*Lp%85x*hF8nZM1{R*9c|_+ej=108D8 zXTceQmrN0Z;K_i4W=Vat$32PGPr~{AsT9vwr=#3$oi14?OZ}9rB9#{{?sA90;f% zUIDG=BNW#Tp;*dZtCKwXXK?^Oq*xGxi!|<}XW=eY!`;&Al!d^U)d7S=CqS>dh3YX3 zoUl8z+;X29f7{@*9i{Ye+O=-{+YwWt|G2*?4cWUO3DvvtP*JA)_K}Z8qXy;zWNsGj zXula>2>8q4ZEHx?)Xug-3)#Emj=AsQiY;j}?|QZlqoY3VsI<=hCj#O1@#;ZKz}fPp z;~WLS?{u6(RE-ZvVbVO1+96qI2+pz!{Ri$gq3;**w)WQ$9<(TwOO+%Amgw7imt$Pq z6;bLPNM1nz5k_e}btV~W#fDO`wq?TD5WS-I2^`?CEAtH!c*_3;39V200)wSzU1e+Y zdCE?N>3Bw@lc|^^stcxZi1q~w#z+{3Xv|6(QBlu{50009RT$Xs{H!>fHGd&HD3Jq^ z0Y_m_^^t6iE#(n!44wvxOe&?(UVZ0|ETg~#GFj1Qyj;n5U zwmPDzx@Prok|w~ecC8{{-U{9mI6ciX9%>!oFdQ*Io!Wn6VLE5yrbFFeT~tpGq!%In zs!A7~!@eO;cQ@nBc5)$R=7>O&@;DFZ|`qR(1dXYv~Add zdbaU<#ebc0K)Qa&^|4b5lM~A1_AZ>B&l_tC%x=sSJam z{TPt}*iU@A{xKXAXwQOZM{8X2`{#ehYm%cdBb%h;ZRwpd6fLhH_5so_0mU2A#Xxy* z?JLOh80i;~qi9XeE8jmP62xvX#=YJ&jVcOy^AMYCl?2|GW`5ufWcW-M+kf^QDpk%?j{gYoQm1p~85l&ZxLDt!y6X~o?@E$O{S$`!E7r9#DXw`#n(3?g2*boOD8y1j zi<_c^l@{T9os`M%4gO}nI9}329}(iUO|w2=YRFWftKjqOsZ#hhEpPU9uDr~r=B{1E zHnEIJnyLM1YxFIRZ++&-+A3DYRq=+-M$fFTFLF1-`t#PMeu_M-rhkfEilXg*u#?Yf z+#Jv#+}d3FZ)k33m>`_NEa`mwFgpUUxkWM@gKz_0&dCLU0X}@xc(=+uEjZ_AYFtc- zxTj#vHy8%{+1Z2O{TwyBkC51UjWjh$UL(rbKDny{RYtKg9A?nLT(m}jy zFfPvp;lEa^;o>&S%g*s9i##u2YfiJ}Qe~8cvCcu}$FjeDb!yai zM`J$PheFIv2Ca@gR{&MRYW1rfne$4*{TUD(q+vS=?{4t6!qG%g?HO`U|NF}o!2zm* zWrxRM#on?A5syi;${tO8pPlCCfnLFl;NV9z#__*0s z0b#vb^;jCOt6BgL|6d$H=geR1z%RGX=bEY61>9L~gycej&hlcPp251PVcn5m65%?{ znNE54vJ$^qeJqUq`Y5#=Lqm?#y@*BYlhZ?8-9wA-_2V85`2Dc>{&7nW;!X|+R1J0-#WRa{L47)_~5ivWaZ}> zhI}4+iB*!q@##JrgsmggJRss5M@Xs`XwE{^o|pmXB_a1tL~B9MDHpnQ;+?zKyP2<%x*1wub79tXEx4rRVGu@Z!xMzsaa$&~Fy*-cMg_%F)B zPjMgfXpZkLo(R)KEz5eK?k(2e;)(wx`-(V02mx`i*JdY^jZ0{UN?P@%dIR3c0~G_7 zA11}HwAC+8e8+O1DP1clfUpM-v#Ke$5luCS@?WXybWpoj7+w%;8;9JBCh$1X;!C-n z_qHVY+lQeF;XFcgMJTa4YDDIKmG?H2CSXkH|F}K$4PTN**!Gj_f4CFYyks9~MaI*b>c1Kw8-2@) zE~|BIA`aS4^qCb9rO}P?qK_>w-o|7TLlP2VJ}-eT{-Fd=yFUnxiT(xlH@gQvl^uV* z{`}*jW#?@3mvSJ;!DB=@yRhEkyOe91|&hGq(m(!>YJN$syB%Euo?P(0vew z-#kvZLBsv z$;o=E_=S&cIK0Hq_GqojbB;Er>(U_I6JlQn9}Qcu^1B-`7ZLU;n+W2Jo*}}lU?sNzaxq> zfJ$hogs#*ni=?^QJaEZx@%lGBB8!JM0d^G#S0xOE|AAN(!N*yDJBw(rb#{Yv45N}| zA>~+30t`h~lX4OL2^H?uhoNf~bSyj_h5-|HHC7I^byV-hzJ)zTDZkUMncWN@pMnf1 z%5~si5cX+kNd4SqgF^Fb0f)aX<0OU>Kw)h)1k7P>jVN2(3jGVDf0FA~w!xHJqvNW{ zFDEy^RFl`aTU&=eZ>c!->}-R=?AiQO9F(7EfElLy=g8|-uW^J*UoVC|9YqYdSNRj+ zploGCct6(kihg*#$OOU9&Rys!4aP*$3eq*|%SEe6vV@n5eJc*bGAz9eqt^_D zrT8%D1WS7J^k!-#vx7$3v@3m?DP>@sgh3Twzv*U!NZ&Z}%@w`M`7ZJPam7A*phsVg zokk3)2a=kdY;uGA3>Xy#v{7lax_OtG~E&G=rz&?yA(j8wsm@F}{b>i} z*bPNiFj0AY=O8-zmkQnZ;Nf}9FGr`(RZC|o??P73@ghI`vGgk?_GvHzRlzPafgHAr z8SdgXmc)ysgYvzG0FG5J<=zh9?v1N)G9!EChosm6yjJc&P$In=*w-<4l?x&+T1V%@ z0FZ`+bEs#W6SITJZWd=B#cB?pZf0~UzxJ5#Yk~J;1I0pm;+rKx1C$3zA_J!Oe`nYZ zO6g^}RfrB!*{?|t1?S<#(@KLpl+~a~_I(JRC^ZuLa@-BHwty-|p17hNfu+C8!<yGVAP$>~Z36*Px<4=YQLrGE$?PLEFoFp^9Kn6C zXpfEMAwev0v)^W%bjI6|PbR$=wv_H_%bGhi?8>oaaDU^!#^C7waQ#M1$0JXM$_M`j z@fWPsEoFjd31F>4>i=lQcLGyS5WEqlE;urnKwwC8tSowGGv*-iS{D(bx4;B9Ryx#7 zC;JrY;94l_ERNQckJ6FRyOu}5t(;EeHO2;+-RQTYs3)wy)CU*96vvM$P9@`?zeg$kOcNPjJ3KjSWnE0z z-@H)Xy$r^C_jsS6HBjOL+PbR&C&Z-t@*vdG?Lo&4+CYl1J+MiJsJ(nZwwjsX%g`zT z52^g^M+SXPG*WJmdTNPh)`?xzaH64E^h=FEa>`N@yrCA6H-SM9q=J1YwyeaGo$&5z z95w1Y1-D2K6dN~2hjEqnW*lO`uv3%KV+6^D7=SA&WNzx@?NliW6dLFQZ3UyMmuoUZ z`xBpKjrX@8%Epq+U{J;Zid5Ak+kr*Hc!GyR-Bi+$N7FRaPeOZ~dP~H3Y!W44GL|DG zWo&LCN#snove_9y`@k9LB&Gp8(X3Dd9x3Q(BqrPEuSm#~KQQ=mPkEYiTz0a6a2Xgw ztu)11qr~tJu2%6b@&C-E(HG39&K@1e`?O zYE4UP{BhAbJiY_83=fd}!Uw`GMFlS36yBnkbk{FOZdDLk|NP z9oG|uzox#X!XTD-@Tt$^lx&T}-StleEz}iUsI0inUp6vb42~d@=A!fEA+)N5MiV`7 zNTnHMU`%Ta3WiF<)Nn$u{54VFID>`(Gm;%oqiqsec13?oQi;i6^k9<5a4d99$i$2W z1x89yUsxDhWN$f`OdL8mArgTb-`3E$6?@a<(oypWnJ#N!kcsbZHqqm8+>+m492|>QlN&JkK@Z>l(H2}?43P?W>JE{&?B-F(@&XTjijw}8 z(Bc8=wLUJm}&cW&JLc-!gq?A+0* z(oeRu7i-&KpW+nodHVbWL;jNB5ED>93{eHmaZuH?{xgEM;d3ekxLHBcO5Aggez7$$ zo0dP_zy>g*9B4>)ta9DIK8`~`5H8ZHNs0aU2awi*G#(1l!uf)EU>)Irz+uf;AkL)n zQwAHE>TUrSk$Pp^V|X7(#R%vHzO1_XKC`J&sDEriNled-xj|f?O0q*@Fw1^H)QHOb z(zRi*Au?n(up?x~0L6uQGCN4P1d{p{d_fDl+r)t6a2%0`~RgNfS0oOUmt~!1tt#hEgob#Xffb4>rQMrQ%VXrAqm1S zEoni0d>8>|M1foc@J=QZkcDp}>j7kce>usYTY839it9K1nG|kKEz~tF%Ge3je!fxP zymLHf1V4`GT;7RPeBdOI+P=OQdA?nw|7>_YT)yzuJHE2n?SrU~Ds;Iui(4lOPyScy z{N#2~!q;_hOHL8_yPZ`qaDQVSw9^Y5Lcrge)=+#E=+v!>9Th-i6px!))Sv*&6^TvV z(vYh7ILrt&!ux4|A|fcbKLyc-nNdZ3QzLQYD&m$-_>z&h3S ztTPOj4oM{uK)4r3(Zo)*r0Qb* zs$VoDX--Y9>3dFw`+@Y|RBDKq#Zg!pXreE!u>DtPPybyxlSikwvqh;k_WB4Ac&_^0 zaw$lDb8@UKiGh9mkCohd55LpK@bG=ypy#_ADeJd&pZ9x(ekfzDGAo|il=_j1==Z4x zhr@G)4^|xN76R&`F4l+d_I&&7AGK~rY8%iw-MJsYGu~2UY&Am?lw`hWA z=BIJ%Syw>1~S+W z>B~LpTRUJlKT6gwZ1oCbXqJAh`*vzW>Sh^hkN4{jn}=sYL&rg_Gyb)6&8>tn-Ae&- zijG!XU}j#3yuHzD^0M~LpdU((!y*kIz8;>l{5($XU-*1-O$Ycf$MyAlwBn+OcEIPO zA3vII-<~cyI&Tr__yuVl5%@}g*_DLDRmu%m;BC9U9K`L|QYfiPdXp3q43ScRsexMx zDLT=ex(E#uK{IOgJvU>buAn$O@DE0MGHF#vT7bM=XasY&Sm$kyo-sjhPMz}wHpI+e>?LtD+l4xG?M^aQ$kE_Tm^F0xPwC=Skxs)e93I$zW;`vPv6ZBsU zf8GB88k#gqCVV6xEr9OOHy^g8&;A~J&+LXXE8V7Z=$YS@El6D?P3`*}BD27nAW|~C zehBXLDe`HYUyGD4Y{^aNU)ec5`{>_V(zS6ArTAnKbccTZ-`%0hkmg;11!$`%j2Na8 zNR5KEjksPAwWI9?2;0i5C6H=?6AEOiCa^20b`zvNMaQ0^5|!GXxh72)g_|yIcSeC9 zb$3N)00W4z-C*k$N4juyeHV1#>Jf(}acc)tr-Hlp++z6OQ`(pC^;K3^3F-w;Gz#nL z{r+!$|2I&&}_1^ZQQAPFf&I)%}0jA@}>-{Qlnj?gdox zMm(tHVrU(-3r6^VpPS!(+V37(fjM4>GowEHo<3Jv-^!R*)1R6G+4|g>voDAIt~?sz z-M=z%bmRBA`Tf56{q{7Wj}sOkzt{CbEV1DF?{o887dRI9t=at6Y<}x$zu%s}wa(u< w^zR<}_l%(a_x$Gf{O0#;Enc|9_|;^|@A=K|x##bh^lv@wH!tx2cb4P-0=EB3Bme*a literal 0 HcmV?d00001 diff --git a/benchmarks/com4_avaArzlerAlmForestNumba/com4_b44fd62ee8_20260812_164438_travelLengthMax.tif b/benchmarks/com4_avaArzlerAlmForestNumba/com4_b44fd62ee8_20260812_164438_travelLengthMax.tif new file mode 100644 index 0000000000000000000000000000000000000000..4dc8ec5ff03bd02fec93cc34d8146207eddf6ffd GIT binary patch literal 54147 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 literal 0 HcmV?d00001 diff --git a/benchmarks/com4_avaArzlerAlmForestNumba/com4_b44fd62ee8_20260812_164438_zdelta.tif b/benchmarks/com4_avaArzlerAlmForestNumba/com4_b44fd62ee8_20260812_164438_zdelta.tif new file mode 100644 index 0000000000000000000000000000000000000000..42564caaf3e212466f7fd2de2eeb17da08af1522 GIT binary patch literal 57815 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