From fc6c1b2557d1a247182e0ed3b5d9936dc6e1f902 Mon Sep 17 00:00:00 2001 From: "Sharlon N. Regales" Date: Thu, 11 Dec 2025 10:19:35 +0100 Subject: [PATCH 01/16] Add returns dependency Returns will be use to wrap function in container that can be passed around in rail. Thus this will be use for railway function methodology --- pyproject.toml | 1 + uv.lock | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 16d923d1..e1b7c431 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ dependencies = [ "pydantic>=2.12.4", "uvicorn>=0.38.0", "scratch-core", + "returns>=0.26.0", ] # https://docs.astral.sh/uv/concepts/projects/workspaces/ diff --git a/uv.lock b/uv.lock index 417c313e..a2a0f927 100644 --- a/uv.lock +++ b/uv.lock @@ -1744,6 +1744,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bd/60/50fbb6ffb35f733654466f1a90d162bcbea358adc3b0871339254fbc37b2/requirements_parser-0.13.0-py3-none-any.whl", hash = "sha256:2b3173faecf19ec5501971b7222d38f04cb45bb9d87d0ad629ca71e2e62ded14", size = 14782, upload-time = "2025-05-21T13:42:04.007Z" }, ] +[[package]] +name = "returns" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c2/6dda7ef39464568152e35c766a8b49ab1cdb1b03a5891441a7c2fa40dc61/returns-0.26.0.tar.gz", hash = "sha256:180320e0f6e9ea9845330ccfc020f542330f05b7250941d9b9b7c00203fcc3da", size = 105300, upload-time = "2025-07-24T13:11:21.772Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/4d/a7545bf6c62b0dbe5795f22ea9e88cc070fdced5c34663ebc5bed2f610c0/returns-0.26.0-py3-none-any.whl", hash = "sha256:7cae94c730d6c56ffd9d0f583f7a2c0b32cfe17d141837150c8e6cff3eb30d71", size = 160515, upload-time = "2025-07-24T13:11:20.041Z" }, +] + [[package]] name = "rich" version = "14.2.0" @@ -1984,6 +1996,7 @@ dependencies = [ { name = "fastapi", extra = ["standard"] }, { name = "loguru" }, { name = "pydantic" }, + { name = "returns" }, { name = "scratch-core" }, { name = "uvicorn" }, ] @@ -2010,6 +2023,7 @@ requires-dist = [ { name = "fastapi", extras = ["standard"], specifier = ">=0.119.0" }, { name = "loguru", specifier = ">=0.7.3" }, { name = "pydantic", specifier = ">=2.12.4" }, + { name = "returns", specifier = ">=0.26.0" }, { name = "scratch-core", editable = "packages/scratch-core" }, { name = "uvicorn", specifier = ">=0.38.0" }, ] From 9eed14e2e0babd18fddd05019a1d89053c9b25c5 Mon Sep 17 00:00:00 2001 From: "Sharlon N. Regales" Date: Thu, 11 Dec 2025 10:42:43 +0100 Subject: [PATCH 02/16] create railway logger decorator --- packages/scratch-core/src/utils/logger.py | 94 ++++++++++++++++ packages/scratch-core/tests/conftest.py | 21 ++++ .../scratch-core/tests/utils/test_logger.py | 105 ++++++++++++++++++ 3 files changed, 220 insertions(+) create mode 100644 packages/scratch-core/src/utils/logger.py create mode 100644 packages/scratch-core/tests/utils/test_logger.py diff --git a/packages/scratch-core/src/utils/logger.py b/packages/scratch-core/src/utils/logger.py new file mode 100644 index 00000000..06e9a30a --- /dev/null +++ b/packages/scratch-core/src/utils/logger.py @@ -0,0 +1,94 @@ +from enum import Enum +from functools import wraps +import logging +from typing import Any, Callable, Final +from itertools import chain + +from loguru import logger +from returns.io import IOFailure, IOResult, IOSuccess +from returns.result import Failure, Result, Success + +VERBOSE: Final[bool] = False + + +def _debug_function_signature(func: Callable[..., Any], *args, **kwargs): + """Print the function signature and return value""" + signature = ", ".join( + chain( + (repr(arg) for arg in args), + (f"{key}={repr(value)}" for key, value in kwargs.items()), + ) + ) + logger.debug(f"Calling {func.__name__}({signature})") + + +class FailureLevel(Enum): + WARNING = logging.WARNING + ERROR = logging.ERROR + CRITICAL = logging.CRITICAL + + +def log_failure(failure_message: str, failure_level: FailureLevel, error: str) -> None: + logger.debug(f"{failure_message}: {error}") + match failure_level: + case FailureLevel.WARNING: + logger.warning(failure_message) + case FailureLevel.ERROR: + logger.error(failure_message) + case FailureLevel.CRITICAL: + logger.critical(failure_message) + + +def _log_io_container( + result: IOResult, + failure_message: str, + success_message: str | None, + failure_level: FailureLevel, +) -> None: + match result: + case IOSuccess(): + if success_message: + logger.info(success_message) + case IOFailure(error): + log_failure(failure_message, failure_level, str(error)) + + +def _log_container( + result: Result, + failure_message: str, + success_message: str | None, + failure_level: FailureLevel, +) -> None: + match result: + case Success(): + if success_message: + logger.info(success_message) + case Failure(error): + log_failure(failure_message, failure_level, str(error)) + + +def log_railway_function( + failure_message: str, + success_message: str | None = None, + failure_level: FailureLevel = FailureLevel.ERROR, +): + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + if VERBOSE: + _debug_function_signature(func, *args, **kwargs) + result = func(*args, **kwargs) + match result: + case IOResult(): + _log_io_container( + result, failure_message, success_message, failure_level + ) + case Result(): + _log_container( + result, failure_message, success_message, failure_level + ) + return result + + return wrapper + + return decorator diff --git a/packages/scratch-core/tests/conftest.py b/packages/scratch-core/tests/conftest.py index e2c7d418..f4c38e42 100644 --- a/packages/scratch-core/tests/conftest.py +++ b/packages/scratch-core/tests/conftest.py @@ -1,6 +1,10 @@ +from pathlib import Path +import logging + import numpy as np import pytest from PIL import Image +from loguru import logger from image_generation.data_formats import ScanImage from parsers.data_types import load_scan_image @@ -8,6 +12,23 @@ from .constants import SCANS_DIR +TEST_ROOT = Path(__file__).parent + + +class PropagateHandler(logging.Handler): + """Handler that propagates loguru records to standard logging.""" + + def emit(self, record: logging.LogRecord) -> None: + logging.getLogger(record.name).handle(record) + + +@pytest.fixture +def caplog(caplog): + """Fixture to enable caplog to capture loguru logs.""" + handler_id = logger.add(PropagateHandler(), format="{message}") + yield caplog + logger.remove(handler_id) + @pytest.fixture(scope="session") def scan_image_array() -> ScanMap2DArray: diff --git a/packages/scratch-core/tests/utils/test_logger.py b/packages/scratch-core/tests/utils/test_logger.py new file mode 100644 index 00000000..0812c2ae --- /dev/null +++ b/packages/scratch-core/tests/utils/test_logger.py @@ -0,0 +1,105 @@ +from collections.abc import Set +import logging +from typing import Callable, Final +import pytest +from unittest.mock import patch +from returns.io import IOFailure, IOResult, IOSuccess +from returns.result import Failure, Result, Success + +from utils.logger import log_railway_function + + +SUCCESS_MESSAGE: Final[str] = "Operation succeeded" +FAILURE_MESSAGE: Final[str] = "Operation failed" +ERROR_VALUE: Final[Exception] = RuntimeError("Something went wrong") + + +@log_railway_function(failure_message=FAILURE_MESSAGE, success_message=SUCCESS_MESSAGE) +def some_io_function(should_succeed: bool): + if should_succeed: + return IOSuccess(42) + return IOFailure(ERROR_VALUE) + + +@log_railway_function(failure_message=FAILURE_MESSAGE, success_message=SUCCESS_MESSAGE) +def some_function(should_succeed: bool): + if should_succeed: + return Success(42) + return Failure(ERROR_VALUE) + + +@log_railway_function(failure_message=FAILURE_MESSAGE, success_message=SUCCESS_MESSAGE) +def some_complex_function(a, *, b, c=3): + """My function docstring.""" + return Success({"a": a, "x": [b, c]}) + + +@pytest.mark.parametrize( + "function, should_succeed, message, level", + ( + pytest.param(some_function, True, SUCCESS_MESSAGE, {"INFO"}, id="Success"), + pytest.param( + some_function, False, FAILURE_MESSAGE, {"DEBUG", "ERROR"}, id="Failure" + ), + pytest.param(some_io_function, True, SUCCESS_MESSAGE, {"INFO"}, id="IOSuccess"), + pytest.param( + some_io_function, False, FAILURE_MESSAGE, {"DEBUG", "ERROR"}, id="IOFailure" + ), + ), +) +def test_log_railway_function_capture_log_message( + function: Callable[[bool], Result | IOResult], + should_succeed: bool, + message: str, + level: Set[str], + caplog: pytest.LogCaptureFixture, +): + """Test that IOSuccess logs an info message.""" + with caplog.at_level(logging.DEBUG): + _ = function(should_succeed) + + assert message in caplog.text + # Verify only INFO level is logged (not ERROR or DEBUG) + assert {record.levelname for record in caplog.records} == level + + +@pytest.mark.parametrize("success_message", (None, "")) +def test_empty_success_message_does_not_log_on_success( + success_message: str | None, caplog: pytest.LogCaptureFixture +): + """Test that None/empty success_message doesn't log info on success.""" + + @log_railway_function( + failure_message=FAILURE_MESSAGE, success_message=success_message + ) + def empty_success_message_func(): + return IOSuccess(100) + + with caplog.at_level(logging.DEBUG): + _ = empty_success_message_func() + + assert not {record.levelname for record in caplog.records} + + +def test_decorator_is_not_destructive(): + """Test that decorator does not modify function's result.""" + result = some_complex_function(1, b=2, c=5) + + assert isinstance(result, Success) + assert result.unwrap() == {"a": 1, "x": [2, 5]} + + +def test_decorator_preserves_function_metadata(): + """Test decorator preserves function metadata.""" + assert some_complex_function.__name__ == "some_complex_function" + assert some_complex_function.__doc__ == "My function docstring." + + +@patch("utils.logger.VERBOSE", True) +def test_verbose_mode_logs_function_signature(caplog: pytest.LogCaptureFixture): + """Test that VERBOSE mode logs the function signature.""" + + with caplog.at_level(logging.DEBUG): + _ = some_complex_function(1, b=2, c=5) + + assert "Calling some_complex_function(1, b=2, c=5)" in caplog.text From 4a20096ea94880323d188707609ba9e4bbe7b8de Mon Sep 17 00:00:00 2001 From: "Sharlon N. Regales" Date: Thu, 11 Dec 2025 10:52:10 +0100 Subject: [PATCH 03/16] Rewrite tests with the railway way containers in mind We no longer are working with the raw value, since it's unclear at runtime if a failure occured or not. Thus the raw value will be encapsulated in a container that can be passed around until the entended reciever consumes it; think of a mail currier package. State of the container can be Success or Failure, but that can be unpack lazily --- packages/scratch-core/tests/conftest.py | 34 +++- .../container_models/test_coerce_to_array.py | 68 +++++++ .../container_models/test_surface_normals.py | 31 +++ .../scratch-core/tests/helper_function.py | 10 + .../tests/parsers/test_load_scan_image.py | 90 +++++++++ .../scratch-core/tests/parsers/test_x3p.py | 124 ++++++++++++ .../scratch-core/tests/renders/conftest.py | 55 ++++++ .../tests/renders/test_calculate_lighting.py | 157 +++++++++++++++ .../renders/test_compute_surface_normals.py | 185 ++++++++++++++++++ .../tests/renders/test_display.py | 19 ++ .../tests/renders/test_multiple_lights.py | 116 +++++++++++ .../renders/test_normalize_intensity_map.py | 65 ++++++ .../resources/baseline_images/circle.png | Bin 0 -> 36660 bytes 13 files changed, 945 insertions(+), 9 deletions(-) create mode 100644 packages/scratch-core/tests/container_models/test_coerce_to_array.py create mode 100644 packages/scratch-core/tests/container_models/test_surface_normals.py create mode 100644 packages/scratch-core/tests/helper_function.py create mode 100644 packages/scratch-core/tests/parsers/test_load_scan_image.py create mode 100644 packages/scratch-core/tests/parsers/test_x3p.py create mode 100644 packages/scratch-core/tests/renders/conftest.py create mode 100644 packages/scratch-core/tests/renders/test_calculate_lighting.py create mode 100644 packages/scratch-core/tests/renders/test_compute_surface_normals.py create mode 100644 packages/scratch-core/tests/renders/test_display.py create mode 100644 packages/scratch-core/tests/renders/test_multiple_lights.py create mode 100644 packages/scratch-core/tests/renders/test_normalize_intensity_map.py create mode 100755 packages/scratch-core/tests/resources/baseline_images/circle.png diff --git a/packages/scratch-core/tests/conftest.py b/packages/scratch-core/tests/conftest.py index f4c38e42..fce1cdfb 100644 --- a/packages/scratch-core/tests/conftest.py +++ b/packages/scratch-core/tests/conftest.py @@ -9,8 +9,8 @@ from image_generation.data_formats import ScanImage from parsers.data_types import load_scan_image from utils.array_definitions import ScanMap2DArray, MaskArray +from .helper_function import unwrap_result -from .constants import SCANS_DIR TEST_ROOT = Path(__file__).parent @@ -31,11 +31,22 @@ def caplog(caplog): @pytest.fixture(scope="session") -def scan_image_array() -> ScanMap2DArray: +def scans_dir() -> Path: + """Path to resources scan directory.""" + return TEST_ROOT / "resources" / "scans" + + +@pytest.fixture(scope="session") +def baseline_images_dir() -> Path: + """Path to resources baseline images directory.""" + return TEST_ROOT / "resources" / "baseline_images" + + +@pytest.fixture(scope="session") +def scan_image_array(baseline_images_dir: Path) -> ScanMap2DArray: """Build a fixture with ground truth image data.""" - gray = Image.open(SCANS_DIR / "circle.png").convert("L") - data = np.asarray(gray, dtype=np.float64) - return data + gray = Image.open(baseline_images_dir / "circle.png").convert("L") + return np.asarray(gray, dtype=np.float64) @pytest.fixture(scope="session") @@ -45,17 +56,22 @@ def scan_image(scan_image_array: ScanMap2DArray) -> ScanImage: @pytest.fixture(scope="session") -def scan_image_replica() -> ScanImage: +def scan_image_replica(scans_dir: Path) -> ScanImage: """Build a `ScanImage` object`.""" - return load_scan_image(scan_file=SCANS_DIR / "Klein_non_replica_mode.al3d") + return unwrap_result( + load_scan_image( + scans_dir / "Klein_non_replica_mode.al3d", + step_size_x=1, + step_size_y=1, + ) + ) @pytest.fixture(scope="session") def scan_image_with_nans(scan_image_replica: ScanImage) -> ScanImage: - """Build a `ScanImage` object`.""" - scan_image = scan_image_replica.model_copy(deep=True) # add random NaN values rng = np.random.default_rng(42) + scan_image = scan_image_replica.model_copy(deep=True) scan_image.data[rng.random(size=scan_image.data.shape) < 0.1] = np.nan return scan_image diff --git a/packages/scratch-core/tests/container_models/test_coerce_to_array.py b/packages/scratch-core/tests/container_models/test_coerce_to_array.py new file mode 100644 index 00000000..4479c1f0 --- /dev/null +++ b/packages/scratch-core/tests/container_models/test_coerce_to_array.py @@ -0,0 +1,68 @@ +import numpy as np +import pytest + +from container_models.base import coerce_to_array + + +@pytest.mark.parametrize( + "dtype, input_sequence", + [ + pytest.param(np.float64, [1.5, 2.5, 3.5], id="list float64"), + pytest.param(np.uint8, [10, 20, 30], id="list uint8"), + pytest.param(np.float32, (1.0, 2.0, 3.0), id="tuple float32"), + pytest.param(np.float64, [[1.0, 2.0], [3.0, 4.0]], id="nested list"), + pytest.param(np.uint8, [255], id="single value"), + pytest.param(np.float64, [], id="empty"), + ], +) +def test_coerce_sequence_to_array(dtype, input_sequence): + """Test converting sequences (lists, tuples, nested lists) to arrays.""" + result = coerce_to_array(dtype, input_sequence) + assert isinstance(result, np.ndarray) + assert result.dtype == dtype + assert np.array_equal(result, np.array(input_sequence, dtype)) + + +@pytest.mark.parametrize( + "dtype, input_value", + [ + pytest.param(np.float64, np.array([1], dtype=np.int32), id="existing_array"), + pytest.param(np.float64, None, id="None"), + ], +) +def test_passthrough_behavior(dtype, input_value): + """Test that certain inputs are returned as-is without conversion.""" + result = coerce_to_array(dtype, input_value) + assert ( + result is input_value + if input_value is None + else np.array_equal(result, input_value) # type: ignore + ) + + +@pytest.mark.parametrize( + "dtype, input_value", + [ + pytest.param(np.uint8, [256], id="uint8 overflow"), + pytest.param(np.uint8, [-1], id="uint8 underflow"), + pytest.param(np.int8, [128], id="int8 overflow"), + pytest.param(np.int8, [-129], id="int8 underflow"), + ], +) +def test_coerce_out_of_range_list_raises_error(dtype, input_value): + """Test that out-of-range list values raise ValueError.""" + with pytest.raises(ValueError, match=r"Array's value\(s\) out of range"): + coerce_to_array(dtype, input_value) + + +@pytest.mark.parametrize( + "dtype, input_value", + [ + pytest.param(np.float16, ["not", "numbers"], id="list_of_strings"), + pytest.param(np.int32, [1, "mixed", 3], id="mixed_types"), + ], +) +def test_coerce_invalid_list_data_raises_error(dtype, input_value): + """Test that non-numeric data raises ValueError.""" + with pytest.raises(ValueError): + coerce_to_array(dtype, input_value) diff --git a/packages/scratch-core/tests/container_models/test_surface_normals.py b/packages/scratch-core/tests/container_models/test_surface_normals.py new file mode 100644 index 00000000..0eb87e9d --- /dev/null +++ b/packages/scratch-core/tests/container_models/test_surface_normals.py @@ -0,0 +1,31 @@ +import numpy as np +from pydantic import ValidationError +import pytest + +from container_models.surface_normals import SurfaceNormals + + +@pytest.mark.parametrize( + "nx, ny, nz", + [ + pytest.param((100, 100), (80, 100), (100, 100), id="ny shorter width"), + pytest.param((100, 100), (100, 80), (100, 100), id="ny shorter height"), + pytest.param((80, 100), (100, 100), (100, 100), id="nx shorter width"), + pytest.param((100, 80), (100, 100), (100, 100), id="nx shorter height"), + pytest.param((100, 100), (100, 100), (80, 100), id="nz shorter width"), + pytest.param((100, 100), (100, 100), (100, 80), id="nz shorter height"), + ], +) +def test_surface_normals_invalid_shapes( + nx: tuple[int, int], ny: tuple[int, int], nz: tuple[int, int] +): + # act and assert + with pytest.raises( + ValidationError, + match=r"All normal vector components must have the same shape", + ): + SurfaceNormals( + x_normal_vector=np.zeros(nx), + y_normal_vector=np.zeros(ny), + z_normal_vector=np.zeros(nz), + ) diff --git a/packages/scratch-core/tests/helper_function.py b/packages/scratch-core/tests/helper_function.py new file mode 100644 index 00000000..35f92526 --- /dev/null +++ b/packages/scratch-core/tests/helper_function.py @@ -0,0 +1,10 @@ +from returns.io import IOResultE, IOSuccess +from returns.result import Success + + +def unwrap_result[T](result: IOResultE[T]) -> T: + match result: + case IOSuccess(Success(value)) | Success(value): + return value + case _: + assert False, "failed to unwrap" diff --git a/packages/scratch-core/tests/parsers/test_load_scan_image.py b/packages/scratch-core/tests/parsers/test_load_scan_image.py new file mode 100644 index 00000000..7545baec --- /dev/null +++ b/packages/scratch-core/tests/parsers/test_load_scan_image.py @@ -0,0 +1,90 @@ +from math import ceil +from pathlib import Path + +import numpy as np +import pytest +from scipy.constants import micro +from surfalize import Surface + +from parsers import load_scan_image +from returns.pipeline import is_successful + +from ..helper_function import unwrap_result + + +@pytest.fixture(scope="class") +def filepath(scans_dir: Path, request: pytest.FixtureRequest): + return scans_dir / request.param + + +@pytest.mark.parametrize( + "filepath", + [ + "Klein_non_replica_mode.al3d", + "Klein_non_replica_mode_X3P_Scratch.x3p", + ], + indirect=True, +) +class TestLoadScanImage: + @pytest.mark.parametrize("step_x, step_y", [(1, 1), (10, 10), (25, 25), (25, 50)]) + def test_load_scan_data_matches_size( + self, filepath: Path, step_x: int, step_y: int + ) -> None: + # Arrange + surface = Surface.load(filepath) + # Act + result = load_scan_image(filepath, step_x, step_y) + scan_image = unwrap_result(result) + + # Assert + assert scan_image.data.shape == ( + ceil(surface.data.shape[0] / step_y), + ceil(surface.data.shape[1] / step_x), + ) + + @pytest.mark.parametrize( + ("step_x", "step_y"), + [ + pytest.param(10, 10, id="default value"), + pytest.param(1, 10, id="only step y"), + pytest.param(10, 1, id="only x"), + pytest.param(10, 5, id="different x and y"), + ], + ) + def test_scan_map_updates_scales( + self, filepath: Path, step_x: int, step_y: int + ) -> None: + # arrange + surface = Surface.load(filepath) + # Act + result = load_scan_image(filepath, step_x, step_y) + scan_image = unwrap_result(result) + + # Assert + assert np.isclose(scan_image.scale_x, surface.step_x * step_x * micro) + assert np.isclose(scan_image.scale_y, surface.step_y * step_y * micro) + + @pytest.mark.parametrize( + "step_x, step_y", [(-2, 2), (0, 0), (0, 3), (2, -1), (-1, -1), (1e3, 1e4)] + ) + def test_load_scan_data_rejects_incorrect_sizes( + self, filepath: Path, step_x: int, step_y: int + ) -> None: + # act + result = load_scan_image(filepath, step_x, step_y) + # assert + assert not is_successful(result) + + +# TODO: find a better test methology +def test_load_scan_data_matches_baseline_output( + baseline_images_dir: Path, scans_dir: Path +) -> None: + # arrange + filepath = scans_dir / "Klein_non_replica_mode.al3d" + verified = np.load(baseline_images_dir / "replica_subsampled.npy") + # act + result = load_scan_image(filepath, step_size_x=10, step_size_y=15) + scan_image = unwrap_result(result) + # assert + assert np.allclose(scan_image.data, verified, equal_nan=True, atol=1.0e-5) diff --git a/packages/scratch-core/tests/parsers/test_x3p.py b/packages/scratch-core/tests/parsers/test_x3p.py new file mode 100644 index 00000000..3d3f535a --- /dev/null +++ b/packages/scratch-core/tests/parsers/test_x3p.py @@ -0,0 +1,124 @@ +from re import compile, escape +from pathlib import Path +from unittest.mock import patch + +import pytest +from returns.io import IOSuccess, IOFailure +from returns.result import Failure + +from container_models.scan_image import ScanImage +from parsers import parse_to_x3p, save_x3p +from x3p import X3Pfile + + +def is_good_fail_logs(message: str, log: str) -> bool: + log_pattern = compile( + rf"DEBUG.*{escape(message)}:[\s\S]*?" + rf"ERROR.*{escape(message)}" + ) + return log_pattern.search(log) is not None + + +@pytest.mark.parametrize( + "function", + ( + "_set_record1_entries", + "_set_record2_entries", + "_set_record3_entries", + "_set_binary_data", + ), +) +class TestParseToX3PFailure: + def test_parse_to_x3p_returns_failure(self, function: str, scan_image: ScanImage): + """Test that parse_to_x3p returns Failure when sub-functions fails.""" + with patch(f"parsers.x3p.{function}") as mocker: + mocker.side_effect = RuntimeError("Some Error") + result = parse_to_x3p(scan_image) + + assert isinstance(result, Failure) + assert isinstance(result.failure(), RuntimeError) + + def test_parse_to_x3p_logs_on_failure( + self, function: str, scan_image: ScanImage, caplog: pytest.LogCaptureFixture + ): + """Test that parse_to_x3p logs when sub-functions fails.""" + with patch(f"parsers.x3p.{function}") as mocker: + mocker.side_effect = RuntimeError("Some Error") + with caplog.at_level("DEBUG"): + _ = parse_to_x3p(scan_image) + + assert is_good_fail_logs("Failed to parse image X3P", caplog.text), ( + "Logs don't match expected format." + ) + + +class TestX3PSave: + @pytest.fixture(scope="class") + def x3p(self, scan_image: ScanImage) -> X3Pfile: + return parse_to_x3p(scan_image).unwrap() + + def test_save_to_x3p_returns_failure_when_write_fails(self, x3p: X3Pfile): + """Test that save returns IOFailure when write operation fails.""" + + # Use a path that will cause write to fail (read-only or invalid) + result = save_x3p(x3p, output_path=Path("nonexistent_dir/test.x3p")) + assert isinstance(result, IOFailure) + assert "No such file or directory" in str(result.failure()) + + def test_save_x3p_logs_on_failure( + self, x3p: X3Pfile, caplog: pytest.LogCaptureFixture + ): + """Test that save logs when io fails.""" + output_path = Path("nonexistent_dir/test.x3p") + + with caplog.at_level("DEBUG"): + _ = save_x3p(x3p, output_path) + + assert is_good_fail_logs("Failed to write X3P file", caplog.text), ( + "Logs don't match expected format." + ) + + def test_save_x3p_logs_on_success( + self, x3p: X3Pfile, tmp_path: Path, caplog: pytest.LogCaptureFixture + ): + """Test that save logs on happy path.""" + output_path = tmp_path / "test.x3p" + + with caplog.at_level("INFO"): + _ = save_x3p(x3p, output_path) + + assert compile("Successfully written X3P").search(caplog.text), ( + "Logs don't match expected format." + ) + + def test_save_x3p_returns_success_on_valid_input( + self, x3p: X3Pfile, tmp_path: Path + ): + """Test that save_to_x3p returns IOSuccess(None) when save succeeds.""" + output_path = tmp_path / "test.x3p" + result = save_x3p(x3p, output_path=output_path) + assert result == IOSuccess(output_path) + assert output_path.exists() + + +def test_parse_to_x3p_on_success( + caplog: pytest.LogCaptureFixture, scan_image: ScanImage +): + """Test that parse_to_x3p logs INFO on successful parsing.""" + with caplog.at_level("INFO"): + result = parse_to_x3p(scan_image) + + # TODO: How do I test that X3P is a valid object? + assert isinstance(result.unwrap(), X3Pfile) + + +def test_parse_to_x3p_logs_on_success( + caplog: pytest.LogCaptureFixture, scan_image: ScanImage +): + """Test that parse_to_x3p logs INFO on successful parsing.""" + with caplog.at_level("INFO"): + _ = parse_to_x3p(scan_image) + + assert compile("Successfully parse array to x3p").search(caplog.text), ( + "Logs don't match expected format." + ) diff --git a/packages/scratch-core/tests/renders/conftest.py b/packages/scratch-core/tests/renders/conftest.py new file mode 100644 index 00000000..b8657f0b --- /dev/null +++ b/packages/scratch-core/tests/renders/conftest.py @@ -0,0 +1,55 @@ +import numpy as np +import pytest + +from container_models.light_source import LightSource +from container_models.scan_image import ScanImage + + +TEST_IMAGE_SIZE = 10 +TEST_IMAGE_CENTER = TEST_IMAGE_SIZE // 2 + + +@pytest.fixture(scope="module") +def light_source() -> LightSource: + """Single light from 45 degrees azimuth and elevation.""" + return LightSource(azimuth=45, elevation=45) + + +@pytest.fixture(scope="module") +def observer() -> LightSource: + """Observer looking straight down from +Z direction.""" + return LightSource(azimuth=0, elevation=90) + + +@pytest.fixture(scope="module") +def varied_normals_scan_image() -> ScanImage: + """ScanImage with 3D surface normals (varied orientation).""" + return ScanImage( + data=np.stack( + [ + np.full((TEST_IMAGE_SIZE, TEST_IMAGE_SIZE), 0.7), # nx + np.full((TEST_IMAGE_SIZE, TEST_IMAGE_SIZE), 0.6), # ny + np.full((TEST_IMAGE_SIZE, TEST_IMAGE_SIZE), 0.2), # nz + ], + axis=-1, + ), + scale_x=1.0, + scale_y=1.0, + ) + + +@pytest.fixture(scope="module") +def flat_normals_scan_image() -> ScanImage: + """ScanImage with 3D surface normals (all pointing up +Z).""" + return ScanImage( + data=np.stack( + [ + np.zeros((TEST_IMAGE_SIZE, TEST_IMAGE_SIZE)), # nx + np.zeros((TEST_IMAGE_SIZE, TEST_IMAGE_SIZE)), # ny + np.ones((TEST_IMAGE_SIZE, TEST_IMAGE_SIZE)), # nz + ], + axis=-1, + ), + scale_x=1.0, + scale_y=1.0, + ) diff --git a/packages/scratch-core/tests/renders/test_calculate_lighting.py b/packages/scratch-core/tests/renders/test_calculate_lighting.py new file mode 100644 index 00000000..65149b09 --- /dev/null +++ b/packages/scratch-core/tests/renders/test_calculate_lighting.py @@ -0,0 +1,157 @@ +import numpy as np +import pytest + +from container_models.light_source import LightSource +from container_models.scan_image import ScanImage +from renders.shading import calculate_lighting +from scipy.constants import micro + + +def test_shape( + varied_normals_scan_image: ScanImage, + observer: LightSource, + light_source: LightSource, +) -> None: + # Act + out = calculate_lighting(light_source, observer, varied_normals_scan_image) + # Assert + expected_shape = ( + varied_normals_scan_image.data.shape[0], + varied_normals_scan_image.data.shape[1], + ) + assert out.data.shape == expected_shape + + +def test_value_range( + varied_normals_scan_image: ScanImage, + observer: LightSource, + light_source: LightSource, +) -> None: + # Act + out = calculate_lighting(light_source, observer, varied_normals_scan_image) + + # Assert + assert np.all(out.data >= 0) + assert np.all(out.data <= 1) + + +def test_constant_normals_give_constant_output( + varied_normals_scan_image: ScanImage, + observer: LightSource, + light_source: LightSource, +) -> None: + # Act + out = calculate_lighting(light_source, observer, varied_normals_scan_image) + + # Assert + assert np.allclose(out.data, out.data[0, 0]) + + +def test_bump_changes_values( + observer: LightSource, + light_source: LightSource, + flat_normals_scan_image: ScanImage, +) -> None: + """Test that the shader reacts per pixel by giving a bump in the normals.""" + # Arrange + bump_surface = flat_normals_scan_image.model_copy(deep=True) + center = flat_normals_scan_image.data.shape[0] // 2 + bump_surface.data[center, center, 2] = 1.3 # Modify z-normal + + # Act + out = calculate_lighting(light_source, observer, bump_surface) + + # Assert + center_ = out.data[center, center] + border = out.data[center + 1, center + 1] + assert not np.allclose(center_, border), ( + "Center pixel should differ from border pixel due to bump." + ) + + +@pytest.mark.parametrize( + "light_source,nx,ny,nz", + [ + pytest.param( + LightSource(azimuth=0, elevation=0), + np.ones((10, 10)), + np.zeros((10, 10)), + np.zeros((10, 10)), + id="Light pointing -X, normal pointing +X", + ), + pytest.param( + LightSource(azimuth=180, elevation=0), + -np.ones((10, 10)), + np.zeros((10, 10)), + np.zeros((10, 10)), + id="Light pointing +X, normal pointing -X", + ), + pytest.param( + LightSource(azimuth=270, elevation=0), + np.zeros((10, 10)), + np.ones((10, 10)), + np.zeros((10, 10)), + id="Light pointing -Y, normal pointing +Y", + ), + pytest.param( + LightSource(azimuth=90, elevation=0), + np.zeros((10, 10)), + -np.ones((10, 10)), + np.zeros((10, 10)), + id="Light pointing +Y, normal pointing -Y", + ), + pytest.param( + LightSource(azimuth=0, elevation=90), + np.zeros((10, 10)), + np.zeros((10, 10)), + -np.ones((10, 10)), + id="Light pointing +Z, normal pointing -Z", + ), + ], +) +def test_diffuse_clamps_to_zero( + light_source: LightSource, + nx: np.ndarray, + ny: np.ndarray, + nz: np.ndarray, + observer: LightSource, +) -> None: + """Opposite direction → diffuse should be 0.""" + # Arrange + normals_scan = ScanImage( + data=np.stack([nx, ny, nz], axis=-1), + scale_x=1.0, + scale_y=1.0, + ) + + # Act + out = calculate_lighting(light_source, observer, normals_scan) + + # Assert + assert np.all(out.data == 0), "values should be 0." + + +def test_specular_maximum_case( + observer: LightSource, flat_normals_scan_image: ScanImage +) -> None: + """If light, observer, and normal all align, specular should be maximal.""" + + # Act + out = calculate_lighting(observer, observer, flat_normals_scan_image) + + # Assert + assert np.allclose(out.data, 1.0), "(diffuse=1, specular=1), output = (1+1)/2 = 1" + + +def test_lighting_known_value( + varied_normals_scan_image: ScanImage, + observer: LightSource, + light_source: LightSource, +) -> None: + expected_constant = 0.04571068 + + # Act + out = calculate_lighting(light_source, observer, varied_normals_scan_image) + + # Assert + assert np.allclose(out.data, expected_constant, atol=micro) diff --git a/packages/scratch-core/tests/renders/test_compute_surface_normals.py b/packages/scratch-core/tests/renders/test_compute_surface_normals.py new file mode 100644 index 00000000..92a1c336 --- /dev/null +++ b/packages/scratch-core/tests/renders/test_compute_surface_normals.py @@ -0,0 +1,185 @@ +from functools import partial +import numpy as np +import pytest +from scipy.constants import milli + +from container_models.scan_image import ScanImage +from renders import compute_surface_normals +from container_models.base import MaskArray + + +IMAGE_SIZE = 20 +BUMP_SIZE = 6 +BUMP_HEIGHT = 4 +BUMP_CENTER = IMAGE_SIZE // 2 +BUMP_SLICE = slice(BUMP_CENTER - BUMP_SIZE // 2, BUMP_CENTER + BUMP_SIZE // 2) +NoScaleScanImage = partial(ScanImage, scale_x=1, scale_y=1) + + +@pytest.fixture +def inner_mask() -> MaskArray: + """Mask of all pixels except the 1-pixel border.""" + mask = np.zeros((IMAGE_SIZE, IMAGE_SIZE), dtype=bool) + mask[1:-1, 1:-1] = True + return mask + + +@pytest.fixture +def outer_mask(inner_mask: MaskArray) -> MaskArray: + """Inverse of inner_mask: the NaN border.""" + return ~inner_mask + + +def are_normals_allclose( + normals_scan: ScanImage, + mask: MaskArray, + expected: tuple[float, float, float], +) -> bool: + """Assert nx, ny, nz at mask match expected 3-tuple.""" + nx = normals_scan.data[..., 0] + ny = normals_scan.data[..., 1] + nz = normals_scan.data[..., 2] + return ( + np.allclose(nx[mask], expected[0], atol=milli) + and np.allclose(ny[mask], expected[1], atol=milli) + and np.allclose(nz[mask], expected[2], atol=milli) + ) + + +def is_all_nan(normals_scan: ScanImage, mask: MaskArray) -> np.bool_: + """All channels must be NaN within mask.""" + nx = normals_scan.data[..., 0] + ny = normals_scan.data[..., 1] + nz = normals_scan.data[..., 2] + return ( + np.isnan(nx[mask]).all() + and np.isnan(ny[mask]).all() + and np.isnan(nz[mask]).all() + ) + + +def has_nan(normals_scan: ScanImage, mask: MaskArray) -> np.bool_: + """No channel should contain NaN within mask.""" + nx = normals_scan.data[..., 0] + ny = normals_scan.data[..., 1] + nz = normals_scan.data[..., 2] + return ( + np.isnan(nx[mask]).any() + and np.isnan(ny[mask]).any() + and np.isnan(nz[mask]).any() + ) + + +@pytest.fixture(scope="module") +def flat_nutral_image() -> ScanImage: + return NoScaleScanImage(data=np.zeros((IMAGE_SIZE, IMAGE_SIZE))) + + +def test_slope_has_nan_border( + inner_mask: MaskArray, outer_mask: MaskArray, flat_nutral_image: ScanImage +) -> None: + """ + The image is 1 pixel smaller on all sides due to the slope calculation. + This is filled with NaN values to get the same shape as original image + """ + # Act + surface_normals = compute_surface_normals(flat_nutral_image).unwrap() + + # Assert + assert not has_nan(surface_normals, inner_mask) + assert is_all_nan(surface_normals, outer_mask) + + +def test_flat_surface_returns_flat_surface( + inner_mask: MaskArray, flat_nutral_image: ScanImage +) -> None: + """Given a flat surface the depth map should also be flat.""" + + # Act + surface_normals = compute_surface_normals(flat_nutral_image).unwrap() + + # Assert + assert are_normals_allclose(surface_normals, inner_mask, (0, 0, 1)) + + +@pytest.mark.parametrize( + "step_x, step_y", + [ + pytest.param(2.0, 0.0, id="step increase in x"), + pytest.param(0.0, 2.0, id="step increase in y"), + pytest.param(2.0, 2.0, id="step increase in x and y"), + pytest.param(2.0, -2.0, id="positive and negative steps"), + pytest.param(-2.0, -2.0, id="negative x and y steps"), + ], +) +def test_linear_slope(step_x: float, step_y: float, inner_mask: MaskArray) -> None: + """Test linear slopes in X, Y, or both directions.""" + # Arrange + x_vals = np.arange(IMAGE_SIZE) * step_x + y_vals = np.arange(IMAGE_SIZE) * step_y + input_image = ScanImage( + data=y_vals[:, None] + x_vals[None, :], scale_x=1, scale_y=1 + ) + norm = np.sqrt(step_x**2 + step_y**2 + 1) + expected = (-step_x / norm, step_y / norm, 1 / norm) + + # Act + surface_normals = compute_surface_normals(input_image).unwrap() + + # Assert + assert are_normals_allclose(surface_normals, inner_mask, expected) + + +@pytest.fixture +def image_with_bump() -> ScanImage: + data = np.zeros((IMAGE_SIZE, IMAGE_SIZE), dtype=int) + data[BUMP_SLICE, BUMP_SLICE] = BUMP_HEIGHT + return NoScaleScanImage(data=data) + + +def test_location_slope_is_where_expected( + inner_mask: MaskArray, + image_with_bump: ScanImage, +) -> None: + """Check that slope calculation is localized to the bump coordination an offset of 1 is used for the slope.""" + # Arrange + bump_mask = np.zeros_like(image_with_bump.data, dtype=bool) + bump_mask[ + BUMP_SLICE.start - 1 : BUMP_SLICE.stop + 1, + BUMP_SLICE.start - 1 : BUMP_SLICE.stop + 1, + ] = True + outside_bump_mask = ~bump_mask & inner_mask + + # Act + surface_normals = compute_surface_normals(image_with_bump).unwrap() + nx = surface_normals.data[..., 0] + ny = surface_normals.data[..., 1] + nz = surface_normals.data[..., 2] + + # Assert + assert np.any(np.abs(nx[bump_mask]) > 0), "nx should have slope inside bump" + assert np.any(np.abs(ny[bump_mask]) > 0), "ny should have slope inside bump" + assert np.any(np.abs(nz[bump_mask]) != 1), "nz should deviate from 1 inside bump" + + assert are_normals_allclose(surface_normals, outside_bump_mask, (0, 0, 1)) + + +def test_corner_of_slope(image_with_bump: ScanImage) -> None: + """Test if the corner of the slope is an extension of x, y""" + # Arrange + corner = ( + BUMP_CENTER - BUMP_SIZE // 2, + BUMP_CENTER - BUMP_SIZE // 2, + ) + expected_corner_value = 1 / np.sqrt( + (BUMP_HEIGHT // 2) ** 2 + (BUMP_HEIGHT // 2) ** 2 + 1 + ) + + # Act + surface_normals = compute_surface_normals(image_with_bump).unwrap() + nz = surface_normals.data[..., 2] + + # Assert + assert nz[corner[0], corner[1]] == expected_corner_value, ( + "corner of x and y should have unit normal of x and y" + ) diff --git a/packages/scratch-core/tests/renders/test_display.py b/packages/scratch-core/tests/renders/test_display.py new file mode 100644 index 00000000..e23e8e9d --- /dev/null +++ b/packages/scratch-core/tests/renders/test_display.py @@ -0,0 +1,19 @@ +from pathlib import Path +import numpy as np +import pytest +from numpy.testing import assert_array_almost_equal + +from renders import get_array_for_display +from container_models.scan_image import ScanImage + + +@pytest.mark.integration +def test_get_image_for_display_matches_baseline_image( + scan_image_with_nans: ScanImage, baseline_images_dir: Path +): + # arrange + verified = np.load(baseline_images_dir / "display_array.npy") + # act + display_image = get_array_for_display(scan_image_with_nans).unwrap() + # assert + assert_array_almost_equal(display_image.data, verified) diff --git a/packages/scratch-core/tests/renders/test_multiple_lights.py b/packages/scratch-core/tests/renders/test_multiple_lights.py new file mode 100644 index 00000000..fba09d1e --- /dev/null +++ b/packages/scratch-core/tests/renders/test_multiple_lights.py @@ -0,0 +1,116 @@ +from functools import partial +import numpy as np +import pytest +from returns.pipeline import is_successful + +from container_models.light_source import LightSource +from container_models.scan_image import ScanImage +from renders.shading import apply_multiple_lights + +no_scale_apply_multiple_lights = partial(apply_multiple_lights, scale_x=1, scale_y=1) + + +@pytest.fixture(scope="module") +def multiple_lights(light_source) -> tuple[LightSource, LightSource, LightSource]: + """Multiple lights from different angles.""" + return ( + light_source, + LightSource(azimuth=135, elevation=45), + LightSource(azimuth=225, elevation=45), + ) + + +def test_empty_light_list_returns_failure( + flat_normals_scan_image: ScanImage, + observer: LightSource, +) -> None: + """Test that an empty light list returns a Failure result.""" + # Act + result = no_scale_apply_multiple_lights(flat_normals_scan_image, [], observer) + + # Assert + assert not is_successful(result) + + +def test_constant_normals_give_constant_output( + flat_normals_scan_image: ScanImage, + multiple_lights: tuple[LightSource, ...], + observer: LightSource, +) -> None: + """Test that constant normals produce constant output across the image.""" + # Act + result = no_scale_apply_multiple_lights( + flat_normals_scan_image, multiple_lights, observer + ) + + # Assert + scan_image = result.unwrap() + # All pixels should have the same value + assert np.allclose(scan_image.data, scan_image.data[0, 0]) + + +def test_more_lights_increase_brightness( + flat_normals_scan_image: ScanImage, + observer: LightSource, + light_source: LightSource, + multiple_lights: tuple[LightSource, ...], +) -> None: + """Test that adding more lights increases total brightness.""" + + # Act + result_one = no_scale_apply_multiple_lights( + flat_normals_scan_image, (light_source,), observer + ) + result_two = no_scale_apply_multiple_lights( + flat_normals_scan_image, multiple_lights, observer + ) + + # Assert + brightness_one = np.mean(result_one.unwrap().data) + brightness_two = np.mean(result_two.unwrap().data) + assert brightness_two > brightness_one + + +def test_light_from_opposing_sides( + flat_normals_scan_image: ScanImage, + observer: LightSource, +) -> None: + """Test that lights from opposite horizontal directions produce symmetric results.""" + # Arrange - Two lights at opposite azimuths but same elevation + lights_opposite = [ + LightSource(azimuth=0, elevation=45), + LightSource(azimuth=180, elevation=45), + ] + + # Act + result = no_scale_apply_multiple_lights( + flat_normals_scan_image, lights_opposite, observer + ) + + # Assert + scan_image = result.unwrap() + # For flat surface normals pointing up, opposite lights should contribute equally + assert np.all(scan_image.data >= 0) + + +def test_spatial_variation_with_bumpy_surface( + observer: LightSource, + light_source: LightSource, + flat_normals_scan_image: ScanImage, +) -> None: + """Test that surface variation creates intensity variation.""" + # Arrange - Create a surface with a bump in the center + normals = flat_normals_scan_image.model_copy(deep=True) + center = normals.data.shape[0] // 2 + normals.data[center, center, 0] = 0.5 # x-normal + normals.data[center, center, 2] = 0.866 # z-normal (to keep it normalized) + + # Act + result = no_scale_apply_multiple_lights(normals, (light_source,), observer) + + # Assert + scan_image = result.unwrap() + center_value = scan_image.data[center, center] + corner_value = scan_image.data[0, 0] + # Center and corner should have different intensities + assert not np.isclose(center_value, corner_value) diff --git a/packages/scratch-core/tests/renders/test_normalize_intensity_map.py b/packages/scratch-core/tests/renders/test_normalize_intensity_map.py new file mode 100644 index 00000000..6c315685 --- /dev/null +++ b/packages/scratch-core/tests/renders/test_normalize_intensity_map.py @@ -0,0 +1,65 @@ +from functools import partial +import numpy as np +import pytest + +from container_models.scan_image import ScanImage +from renders import normalize_2d_array + +TEST_IMAGE_WIDTH = 10 +TEST_IMAGE_HEIGHT = 12 +TOLERANCE = 1e-5 + +NoScaleScanImage = partial(ScanImage, scale_x=1, scale_y=1) + + +@pytest.mark.parametrize( + "start_value, slope", + [ + pytest.param(10, 100.0, id="test bigger numbers are reduced"), + pytest.param(-200, 10.0, id="test negative numbers are upped"), + pytest.param(100, 0.01, id="small slope is streched over the range"), + ], +) +def test_bigger_numbers(start_value: int, slope: float) -> None: + # Arrange + row = (start_value + slope * np.arange(TEST_IMAGE_WIDTH)).astype(np.float64) + image = NoScaleScanImage( + data=np.tile(row, (TEST_IMAGE_HEIGHT, 1)).astype(np.float64) + ) + max_val = 255 + min_val = 20 + # Act + normalized_image = ( + normalize_2d_array(image, scale_max=max_val, scale_min=min_val).unwrap().data + ) + + # Assert + assert normalized_image.max() <= max_val + assert normalized_image.min() >= min_val + assert normalized_image[0, 0] == normalized_image.min() + assert normalized_image[9, 9] == normalized_image.max() + + +def test_already_normalized_image() -> None: + # Arrange + max_value = 255 + min_val = 20 + image = np.linspace( + min_val, max_value, num=TEST_IMAGE_WIDTH * TEST_IMAGE_HEIGHT + ).reshape(TEST_IMAGE_WIDTH, TEST_IMAGE_HEIGHT) + + # Act + normalized = ( + normalize_2d_array( + NoScaleScanImage(data=image), scale_max=max_value, scale_min=min_val + ) + .unwrap() + .data + ) + + # Assert + assert np.all(normalized >= min_val) + assert np.all(normalized <= max_value) + assert np.allclose(image, normalized, atol=TOLERANCE), ( + "should be the same output as the already normalized input" + ) diff --git a/packages/scratch-core/tests/resources/baseline_images/circle.png b/packages/scratch-core/tests/resources/baseline_images/circle.png new file mode 100755 index 0000000000000000000000000000000000000000..4b2c87ffd59a82218e3a399439b72157db939ab8 GIT binary patch literal 36660 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 z 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 literal 0 HcmV?d00001 From 99ee3f9f1c063ecf0bc790fe6b77c0b37a5ad260 Mon Sep 17 00:00:00 2001 From: "Sharlon N. Regales" Date: Thu, 11 Dec 2025 10:56:41 +0100 Subject: [PATCH 04/16] Rewrite functions scratch-core with returns Writen module docstrings for new modules with the assistant of Claud (AI) --- .../src/container_models/__init__.py | 15 ++ .../scratch-core/src/container_models/base.py | 59 +++++++ .../src/container_models/light_source.py | 48 ++++++ .../src/container_models/scan_image.py | 26 +++ .../src/container_models/surface_normals.py | 31 ++++ .../scratch-core/src/conversion/__init__.py | 34 +++- packages/scratch-core/src/parsers/__init__.py | 44 ++++- packages/scratch-core/src/parsers/loaders.py | 57 +++++++ packages/scratch-core/src/parsers/x3p.py | 83 +++++++--- packages/scratch-core/src/renders/__init__.py | 29 ++++ packages/scratch-core/src/renders/image_io.py | 93 +++++++++++ .../src/renders/normalizations.py | 145 +++++++++++++++++ packages/scratch-core/src/renders/shading.py | 153 ++++++++++++++++++ 13 files changed, 788 insertions(+), 29 deletions(-) create mode 100644 packages/scratch-core/src/container_models/__init__.py create mode 100644 packages/scratch-core/src/container_models/base.py create mode 100644 packages/scratch-core/src/container_models/light_source.py create mode 100644 packages/scratch-core/src/container_models/scan_image.py create mode 100644 packages/scratch-core/src/container_models/surface_normals.py create mode 100644 packages/scratch-core/src/parsers/loaders.py create mode 100644 packages/scratch-core/src/renders/__init__.py create mode 100644 packages/scratch-core/src/renders/image_io.py create mode 100644 packages/scratch-core/src/renders/normalizations.py create mode 100644 packages/scratch-core/src/renders/shading.py diff --git a/packages/scratch-core/src/container_models/__init__.py b/packages/scratch-core/src/container_models/__init__.py new file mode 100644 index 00000000..5087a228 --- /dev/null +++ b/packages/scratch-core/src/container_models/__init__.py @@ -0,0 +1,15 @@ +""" +Immutable data container models for railway-oriented programming pipelines. + +This module provides Pydantic-based data models that are propagated through railway +functions in functional pipelines. These models serve as type-safe, validated containers +for scientific and imaging data, ensuring data integrity as it flows through processing +pipelines. + +Notes +----- +These models are designed specifically for railway-oriented programming where data +flows through a sequence of transformations. The immutability ensures that each +function in the pipeline receives unmodified input, preventing side effects and +making pipelines easier to reason about and debug. +""" diff --git a/packages/scratch-core/src/container_models/base.py b/packages/scratch-core/src/container_models/base.py new file mode 100644 index 00000000..367842cf --- /dev/null +++ b/packages/scratch-core/src/container_models/base.py @@ -0,0 +1,59 @@ +from collections.abc import Sequence +from functools import partial +from typing import Annotated, TypeAlias + +from numpy import array, bool_, float64, number, uint8 +from numpy.typing import DTypeLike, NDArray +from pydantic import BaseModel, BeforeValidator, ConfigDict, PlainSerializer + + +def serialize_ndarray[T: number](array_: NDArray[T]) -> list[T]: + """Serialize numpy array to a Python list for JSON serialization.""" + return array_.tolist() + + +def coerce_to_array[T: number]( + dtype: DTypeLike, value: Sequence[T] | NDArray[T] | None +) -> NDArray[T] | None: + """ + Coerce input to dtype numpy array. + + Handles JSON deserialization where Python creates int64 integers by default. + """ + if isinstance(value, Sequence): + try: + return array(value, dtype=dtype) + except OverflowError as ofe: + raise ValueError("Array's value(s) out of range") from ofe + + return value + + +ScanMapRGBA: TypeAlias = Annotated[ + NDArray[uint8], + BeforeValidator(partial(coerce_to_array, uint8)), + PlainSerializer(serialize_ndarray), +] + +ScanMap2DArray = ScanVectorField2DArray = UnitVector3DArray = Annotated[ + NDArray[float64], + BeforeValidator(partial(coerce_to_array, float64)), + PlainSerializer(serialize_ndarray), +] + +MaskArray = Annotated[ + NDArray[bool_], + BeforeValidator(partial(coerce_to_array, bool_)), + PlainSerializer(serialize_ndarray), +] + + +class ConfigBaseModel(BaseModel): + """Base model with common configuration for all pydantic models in this project.""" + + model_config = ConfigDict( + frozen=True, + extra="forbid", + arbitrary_types_allowed=True, + regex_engine="rust-regex", + ) diff --git a/packages/scratch-core/src/container_models/light_source.py b/packages/scratch-core/src/container_models/light_source.py new file mode 100644 index 00000000..2e4f37cc --- /dev/null +++ b/packages/scratch-core/src/container_models/light_source.py @@ -0,0 +1,48 @@ +from functools import cached_property +import numpy as np +from pydantic import Field +from .base import UnitVector3DArray, ConfigBaseModel + + +class LightSource(ConfigBaseModel): + """ + Representation of a light source using an angular direction (azimuth and elevation) + together with a derived 3D unit direction vector. + """ + + azimuth: float = Field( + ..., + description="Horizontal angle in degrees measured from the –x axis in the x–y plane. " + "0° is –x direction, 90° is +y direction, 180° is +x direction.", + examples=[90, 45, 180], + ge=0, + le=360, + ) + elevation: float = Field( + ..., + description="Vertical angle in degrees measured from the x–y plane. " + "0° is horizontal, +90° is upward (+z), –90° is downward (–z).", + examples=[90, 45, 180], + ge=-90, + le=90, + ) + + @cached_property + def unit_vector(self) -> UnitVector3DArray: + """ + Returns the unit direction vector [x, y, z] corresponding to the azimuth and + elevation angles. The conversion follows a spherical-coordinate convention: + azimuth defines the horizontal direction, and elevation defines the vertical + tilt relative to the x–y plane. + """ + azimuth = np.deg2rad(self.azimuth) + elevation = np.deg2rad(self.elevation) + vec = np.array( + [ + -np.cos(azimuth) * np.cos(elevation), + np.sin(azimuth) * np.cos(elevation), + np.sin(elevation), + ] + ) + vec.setflags(write=False) + return vec diff --git a/packages/scratch-core/src/container_models/scan_image.py b/packages/scratch-core/src/container_models/scan_image.py new file mode 100644 index 00000000..5dfbea4f --- /dev/null +++ b/packages/scratch-core/src/container_models/scan_image.py @@ -0,0 +1,26 @@ +from pydantic import Field +from .base import ScanMap2DArray, ConfigBaseModel + + +class ScanImage(ConfigBaseModel): + """ + A 2D image/array of floats. + + Used for: depth maps, intensity maps, single-channel images. + Shape: (height, width) + """ + + data: ScanMap2DArray + scale_x: float = Field(..., gt=0.0, description="pixel size in meters (m)") + scale_y: float = Field(..., gt=0.0, description="pixel size in meters (m)") + meta_data: dict | None = None + + @property + def width(self) -> int: + """The image width in pixels.""" + return self.data.shape[1] + + @property + def height(self) -> int: + """The image height in pixels.""" + return self.data.shape[0] diff --git a/packages/scratch-core/src/container_models/surface_normals.py b/packages/scratch-core/src/container_models/surface_normals.py new file mode 100644 index 00000000..b6481c1b --- /dev/null +++ b/packages/scratch-core/src/container_models/surface_normals.py @@ -0,0 +1,31 @@ +from typing import Self +from pydantic import model_validator +from .base import ScanVectorField2DArray, ConfigBaseModel + + +class SurfaceNormals(ConfigBaseModel): + """ + Normal vectors per pixel in a 3-layer field. + + Represents a surface-normal map with components (nx, ny, nz) stored in the + last dimension. Shape: (height, width, 3). + """ + + x_normal_vector: ScanVectorField2DArray + y_normal_vector: ScanVectorField2DArray + z_normal_vector: ScanVectorField2DArray + + @model_validator(mode="after") + def validate_same_shape(self) -> Self: + """Validate that all normal vector components have the same shape.""" + x_shape = self.x_normal_vector.shape + y_shape = self.y_normal_vector.shape + z_shape = self.z_normal_vector.shape + + if not (x_shape == y_shape == z_shape): + raise ValueError( + f"All normal vector components must have the same shape. " + f"Got x: {x_shape}, y: {y_shape}, z: {z_shape}" + ) + + return self diff --git a/packages/scratch-core/src/conversion/__init__.py b/packages/scratch-core/src/conversion/__init__.py index f9cfda49..2644987f 100644 --- a/packages/scratch-core/src/conversion/__init__.py +++ b/packages/scratch-core/src/conversion/__init__.py @@ -1,3 +1,33 @@ -from conversion.subsample import subsample_array +""" +Staging area for MATLAB-to-Python converted code. -__all__ = ("subsample_array",) +This module serves as a temporary dumping ground for newly translated MATLAB code +before it undergoes full integration into the main codebase. Code placed here is +in a transitional state and may not yet conform to project standards, architectural +patterns, or best practices. + +Purpose +------- +The conversion module provides a designated space where developers can: +1. Place initial MATLAB-to-Python translations without disrupting the main codebase +2. Test and validate converted algorithms in isolation +3. Iteratively refactor and improve code quality before final integration +4. Document conversion notes, gotchas, and MATLAB-specific behaviors + +Workflow +-------- +1. **Convert**: Translate MATLAB code to Python and place it in this module +2. **Validate**: Verify the converted code produces correct results +3. **Refactor**: Adapt code to project standards (type hints, Pydantic models, etc.) +4. **Integrate**: Move refined code to appropriate modules (pipelines, preprocessors, etc.) +5. **Remove**: Delete the staging code once integration is complete + +After migration, the staging code in this module should be deleted. + +Warnings +-------- +- DO NOT import from this module in production code +- DO NOT depend on code in this module for long-term functionality +- Code here may be incomplete, buggy, or subject to breaking changes +- This module should remain empty or nearly empty in a mature codebase +""" diff --git a/packages/scratch-core/src/parsers/__init__.py b/packages/scratch-core/src/parsers/__init__.py index 30d48943..74b705d9 100644 --- a/packages/scratch-core/src/parsers/__init__.py +++ b/packages/scratch-core/src/parsers/__init__.py @@ -1,8 +1,46 @@ -from .data_types import load_scan_image -from .x3p import X3PMetaData, save_to_x3p +""" +File parsing and serialization utilities for scan image data. + +This module provides functions for loading, parsing, and saving scan image data +in various file formats, with automatic conversion to and from the internal +ScanImage container model. All parsers are designed to work within railway-oriented +programming pipelines, returning Result/IOResult containers for safe error handling. + +The module handles two primary workflows: +1. **Loading**: Parse external file formats into ScanImage containers +2. **Saving**: Convert ScanImage containers to external file formats + +File Format Support +------------------- +**Input Formats** (via load_scan_image): +- AL3D: Alicona 3D surface files (with custom patch) +- X3P: ISO 25178-72 XML format for surface texture data +- Automatic unit conversion to meters (SI base unit) +- Optional subsampling via step_size parameters + +**Output Formats**: +- X3P: ISO 25178-72 XML format for surface texture data +- Configurable metadata via X3PMetaData + +Railway Integration +------------------- +All parser functions return Result or IOResult containers and are decorated +with logging functionality. This enables seamless integration into functional +pipelines with automatic error propagation: + +Notes +----- +- All spatial measurements are standardized to meters (m) for consistency +- Parsers use railway-oriented programming patterns for robust error handling +- Custom file format support can be added via surfalize FileHandler registration +""" + +from .loaders import load_scan_image +from .x3p import X3PMetaData, save_x3p, parse_to_x3p __all__ = ( "load_scan_image", - "save_to_x3p", + "parse_to_x3p", + "save_x3p", "X3PMetaData", ) diff --git a/packages/scratch-core/src/parsers/loaders.py b/packages/scratch-core/src/parsers/loaders.py new file mode 100644 index 00000000..f10e1e6b --- /dev/null +++ b/packages/scratch-core/src/parsers/loaders.py @@ -0,0 +1,57 @@ +from pathlib import Path + +import numpy as np +from returns.io import impure_safe +from surfalize import Surface +from surfalize.file import FileHandler +from surfalize.file.al3d import MAGIC + +from container_models.scan_image import ScanImage +from utils.logger import log_railway_function + +from .patches.al3d import read_al3d +from scipy.constants import micro + + +# register the patched method as a parser +FileHandler.register_reader(suffix=".al3d", magic=MAGIC)(read_al3d) + + +@log_railway_function( + "Failed to load image file", + "Successfully loaded scan file", +) +@impure_safe +def load_scan_image( + scan_file: Path, step_size_x: int = 1, step_size_y: int = 1 +) -> ScanImage: + """ + Load a scan image from a file and optionally subsample it. Parsed values will be converted to meters (m). + + :param scan_file: The path to the file containing the scanned image data. + :param step_size_x: Denotes the number of steps to skip in X-direction (default: 1, no subsampling). + :param step_size_y: Denotes the number of steps to skip in Y-direction (default: 1, no subsampling). + :returns: An instance of `ScanImage`, optionally subsampled. + """ + surface = Surface.load(scan_file) + data = np.asarray(surface.data, dtype=np.float64) * micro + step_x = surface.step_x * micro + step_y = surface.step_y * micro + height, width = data.shape + + if not (0 < step_size_x < width and 0 < step_size_y < height): + raise ValueError( + f"Step size should be positive and smaller than the image size: {(height, width)}" + ) + + if step_size_x > 1 or step_size_y > 1: + return ScanImage( + data=data[::step_size_y, ::step_size_x], + scale_x=step_x * step_size_x, + scale_y=step_y * step_size_y, + meta_data=surface.metadata, + ) + + return ScanImage( + data=data, scale_x=step_x, scale_y=step_y, meta_data=surface.metadata + ) diff --git a/packages/scratch-core/src/parsers/x3p.py b/packages/scratch-core/src/parsers/x3p.py index cf5ed692..695746af 100644 --- a/packages/scratch-core/src/parsers/x3p.py +++ b/packages/scratch-core/src/parsers/x3p.py @@ -1,14 +1,15 @@ import datetime as dt +from functools import partial from pathlib import Path from typing import NamedTuple import numpy as np +from returns.pipeline import flow from x3p import X3Pfile - -from image_generation.data_formats import ScanImage -from loguru import logger -from surfalize.exceptions import CorruptedFileError -from parsers.exceptions import ExportError +from returns.result import safe +from returns.io import impure_safe +from container_models.scan_image import ScanImage +from utils.logger import log_railway_function class X3PMetaData(NamedTuple): @@ -25,9 +26,8 @@ class X3PMetaData(NamedTuple): measurement_type: str = "NonContacting" -def _to_x3p(image: ScanImage, meta_data: X3PMetaData) -> X3Pfile: - x3p = X3Pfile() - # set Record1 entries +def _set_record1_entries(x3p: X3Pfile, image: ScanImage) -> X3Pfile: + """Set Record1 entries (axes configuration).""" x3p.record1.set_featuretype("SUR") x3p.record1.axes.CX.set_axistype("I") x3p.record1.axes.CX.set_increment(image.scale_x) @@ -36,7 +36,11 @@ def _to_x3p(image: ScanImage, meta_data: X3PMetaData) -> X3Pfile: x3p.record1.axes.CY.set_increment(image.scale_y) x3p.record1.axes.CY.set_datatype("D") x3p.record1.axes.CZ.set_datatype("D") - # set Record2 entries + return x3p + + +def _set_record2_entries(x3p: X3Pfile, meta_data: X3PMetaData) -> X3Pfile: + """Set Record2 entries (metadata).""" x3p.record2.set_date(dt.datetime.now(tz=dt.UTC).strftime("%Y-%m-%dT%H:%M:%S")) # type: ignore x3p.record2.set_calibrationdate(meta_data.calibration_date) # type: ignore if meta_data.author: @@ -48,23 +52,54 @@ def _to_x3p(image: ScanImage, meta_data: X3PMetaData) -> X3Pfile: x3p.record2.instrument.set_version(meta_data.instrument_version) # type: ignore x3p.record2.probingsystem.set_identification(meta_data.identificaton) # type: ignore x3p.record2.probingsystem.set_type(meta_data.measurement_type) # type: ignore - # set the binary data + return x3p + + +def _set_binary_data(x3p: X3Pfile, image: ScanImage) -> X3Pfile: + """Set the binary data.""" x3p.set_data(np.ascontiguousarray(image.data)) + return x3p + + +def _set_record3_entries(x3p: X3Pfile, image: ScanImage) -> X3Pfile: + """Set Record3 entries (matrix dimensions).""" # manually set the Record3 entries since these are set incorrectly in package - x3p.record3.matrixdimension.sizeX = image.data.shape[1] - x3p.record3.matrixdimension.sizeY = image.data.shape[0] + x3p.record3.matrixdimension.sizeX = image.data.shape[1] # type: ignore + x3p.record3.matrixdimension.sizeY = image.data.shape[0] # type: ignore return x3p -def save_to_x3p( - image: ScanImage, output_path: Path, meta_data: X3PMetaData | None = None -) -> None: - """Save an instance of `ScanImage` to a .x3p-file.""" - try: - _to_x3p(image, meta_data or X3PMetaData()).write(str(output_path)) - except CorruptedFileError as err: - logger.debug( - f"export failed with image:{image.data}, with metadata:{meta_data} and path:{output_path}" - ) - logger.error("Failed to save X3P file") - raise ExportError("Failed to save X3P file") from err +@log_railway_function( + "Failed to parse image X3P", + "Successfully parse array to x3p", +) +@safe +def parse_to_x3p(image: ScanImage) -> X3Pfile: + """Convert ScanImage to X3Pfile using a functional approach.""" + return flow( + X3Pfile(), + partial(_set_record1_entries, image=image), + partial(_set_record2_entries, meta_data=X3PMetaData()), + partial(_set_binary_data, image=image), + partial(_set_record3_entries, image=image), + ) + + +@log_railway_function( + "Failed to write X3P file", + "Successfully written X3P", +) +@impure_safe +def save_x3p(x3p: X3Pfile, output_path: Path) -> Path: + """Save an X3Pfile to disk. + + Args: + x3p: The X3Pfile to save + output_path: Where to save the file + + Returns: + IOResult[Path, Exception]: IOSuccess(Path) on success, IOFailure(Exception) on error + """ + + x3p.write(str(output_path)) + return output_path diff --git a/packages/scratch-core/src/renders/__init__.py b/packages/scratch-core/src/renders/__init__.py new file mode 100644 index 00000000..7c6e5449 --- /dev/null +++ b/packages/scratch-core/src/renders/__init__.py @@ -0,0 +1,29 @@ +""" +Rendering and visualization utilities for 3D surface scan data. + +This module provides functions for computing surface properties, applying lighting +models, and generating visual representations of 3D scan data. All functions are +designed for railway-oriented programming pipelines, returning Result/IOResult +containers for safe error handling. + +Notes +----- +- Surface normals are computed using central differences with NaN padding at borders +- NaN values in scan data are handled gracefully throughout the pipeline +- All lighting calculations preserve physical units and scale information +- Output images use RGBA format with transparency for invalid (NaN) regions +""" + +from .shading import apply_multiple_lights +from .normalizations import compute_surface_normals, normalize_2d_array +from .image_io import save_image, scan_to_image, get_array_for_display + + +__all__ = ( + "apply_multiple_lights", + "compute_surface_normals", + "get_array_for_display", + "normalize_2d_array", + "save_image", + "scan_to_image", +) diff --git a/packages/scratch-core/src/renders/image_io.py b/packages/scratch-core/src/renders/image_io.py new file mode 100644 index 00000000..7f94039b --- /dev/null +++ b/packages/scratch-core/src/renders/image_io.py @@ -0,0 +1,93 @@ +from pathlib import Path +from PIL.Image import Image, fromarray +import numpy as np + +from returns.io import impure_safe +from returns.result import safe + +from container_models.scan_image import ScanImage +from container_models.base import ScanMapRGBA, ScanMap2DArray +from utils.logger import log_railway_function + + +def _grayscale_to_rgba(scan_data: ScanMap2DArray) -> ScanMapRGBA: + """ + Convert a 2D grayscale array to an 8-bit RGBA array. + + The grayscale pixel values are assumed to be floating point values in the [0, 255] interval. + NaN values will be converted to black pixels with 100% transparency. + + :param scan_data: The grayscale image data to be converted to an 8-bit RGBA image. + :returns: Array with the image data in 8-bit RGBA format. + """ + gray_uint8 = np.nan_to_num(scan_data, nan=0.0).astype(np.uint8) + rgba = np.repeat(gray_uint8[..., np.newaxis], 4, axis=-1) + rgba[..., 3] = (~np.isnan(scan_data)).astype(np.uint8) * 255 + return rgba + + +def _normalize( + input_array: ScanMap2DArray, lower: float, upper: float +) -> ScanMap2DArray: + """Perform min-max normalization on the input array and scale to the [0, 255] interval.""" + if lower >= upper: + raise ValueError( + f"The lower bound ({lower}) should be smaller than the upper bound ({upper})." + ) + return (input_array - lower) / (upper - lower) * 255.0 + + +def _clip_data( + data: ScanMap2DArray, std_scaler: float +) -> tuple[ScanMap2DArray, float, float]: + """ + Clip the data so that the values lie in the interval [μ - σ * S, μ + σ * S]. + + Here the standard deviation σ is normalized by N-1. Note: NaN values are ignored and unaffected. + + :param data: The data to be clipped. + :param std_scaler: The multiplier for the standard deviation of the data to be clipped. + :returns: A tuple containing the clipped data, the lower bound, and the upper bound of the clipped data. + """ + if std_scaler <= 0.0: + raise ValueError("`std_scaler` must be a positive number.") + mean = np.nanmean(data) + std = np.nanstd(data, ddof=1) * std_scaler + upper = float(mean + std) + lower = float(mean - std) + return np.clip(data, lower, upper), lower, upper + + +@log_railway_function("Failed to retreive array for display") +@safe +def get_array_for_display( + scan_image: ScanImage, *, std_scaler: float = 2.0 +) -> ScanImage: + """ + Clip and normalize image data for displaying purposes. + + First the data will be clipped so that the values lie in the interval [μ - σ * S, μ + σ * S]. + Then the values are min-max normalized and scaled to the [0, 255] interval. + + :param image: An instance of `ScanImage`. + :param std_scaler: The multiplier `S` for the standard deviation used above when clipping the image. + :returns: An array containing the clipped and normalized image data. + """ + return ScanImage( + data=_normalize(*_clip_data(data=scan_image.data, std_scaler=std_scaler)), + scale_x=scan_image.scale_x, + scale_y=scan_image.scale_y, + ) + + +@log_railway_function("Failed to convert scan to image") +@safe +def scan_to_image(scan_image: ScanImage) -> Image: + return fromarray(_grayscale_to_rgba(scan_data=scan_image.data)) + + +@log_railway_function("Failed to save image") +@impure_safe +def save_image(image: Image, output_path: Path) -> Path: + image.save(output_path) + return output_path diff --git a/packages/scratch-core/src/renders/normalizations.py b/packages/scratch-core/src/renders/normalizations.py new file mode 100644 index 00000000..3472aa3f --- /dev/null +++ b/packages/scratch-core/src/renders/normalizations.py @@ -0,0 +1,145 @@ +from functools import partial +import numpy as np +from typing import Final, NamedTuple + +from numpy.typing import NDArray +from returns.pipeline import flow +from returns.result import safe + +from container_models.scan_image import ScanImage +from container_models.surface_normals import SurfaceNormals +from utils.logger import log_railway_function + + +class GradientComponents(NamedTuple): + """Container for gradient components with optional magnitude.""" + + x: NDArray + y: NDArray + magnitude: NDArray | None = None + + +class PhysicalSpacing(NamedTuple): + """Physical spacing between samples in x and y directions.""" + + x: float + y: float + + +# Padding configurations for gradient arrays to maintain original dimensions +_PAD_X_GRADIENT: Final[tuple[tuple[int, int], ...]] = ( + (0, 0), + (1, 1), +) # Pad left and right (columns) +_PAD_Y_GRADIENT: Final[tuple[tuple[int, int], ...]] = ( + (1, 1), + (0, 0), +) # Pad top and bottom (rows) + + +def _compute_central_diff_scales( + spacing: PhysicalSpacing, +) -> PhysicalSpacing: + """Compute scaling factors for central difference approximation: 1/(2*spacing).""" + return PhysicalSpacing(*(1 / (2 * value) for value in spacing)) + + +def _pad_gradient( + unpadded_gradient: NDArray, pad_width: tuple[tuple[int, int], tuple[int, int]] +) -> NDArray: + """Pad a gradient array with NaN values at the borders.""" + return np.pad(unpadded_gradient, pad_width, mode="constant", constant_values=np.nan) + + +def _compute_depth_gradients( + scales: PhysicalSpacing, depth_data: NDArray +) -> GradientComponents: + """Compute depth gradients (∂z/∂x, ∂z/∂y) using central differences.""" + return GradientComponents( + x=_pad_gradient( + (depth_data[:, :-2] - depth_data[:, 2:]) * scales.x, + _PAD_X_GRADIENT, + ), + y=_pad_gradient( + (depth_data[:-2, :] - depth_data[2:, :]) * scales.y, + _PAD_Y_GRADIENT, + ), + ) + + +def _add_normal_magnitude(gradients: GradientComponents) -> GradientComponents: + """Compute and attach the normal vector magnitude to gradient components.""" + magnitude = np.sqrt(gradients.x**2 + gradients.y**2 + 1) + return GradientComponents(gradients.x, gradients.y, magnitude) + + +def _normalize_to_surface_normals(gradients: GradientComponents) -> SurfaceNormals: + """Normalize gradient components to unit surface normal vectors.""" + x, y, magnitude = gradients + if magnitude is None: + raise ValueError + return SurfaceNormals( + x_normal_vector=x / magnitude, + y_normal_vector=-y / magnitude, + z_normal_vector=1 / magnitude, + ) + + +@log_railway_function( + failure_message="Failed to compute surface normals from depth data", + success_message="Successfully computed surface normal components", +) +@safe +def compute_surface_normals(scan_image: ScanImage) -> SurfaceNormals: + """ + Compute per-pixel surface normals from a 2D depth map. + + The gradients in both x and y directions are estimated using central differences, + and the resulting normal vectors are normalized per pixel. + The border are padded with NaN values to keep the same size as the input data. + + :param depth_data: 2D array of depth values with shape (Height, Width). + :param x_dimension: Physical spacing between columns (Δx) in meters. + :param y_dimension: Physical spacing between rows (Δy) in meters. + + :returns: 3D array of surface normals with shape (Height, Width, 3), where the + last dimension corresponds to (nx, ny, nz). + """ + + return flow( + PhysicalSpacing(scan_image.scale_x, scan_image.scale_y), + _compute_central_diff_scales, + partial(_compute_depth_gradients, depth_data=scan_image.data), + _add_normal_magnitude, + _normalize_to_surface_normals, + ) + + +@log_railway_function( + failure_message="Failed to normalize 2D intensity map", +) +@safe +def normalize_2d_array( + image_to_normalize: ScanImage, + scale_max: float = 255, + scale_min: float = 25, +) -> ScanImage: + """ + Normalize a 2D intensity map to a specified output range. + + The normalization is done by the steps: + 1. apply min-max normalization to grayscale data + 2. stretch / scale the normalized data from the unit range to a specified output range + + :param image_to_normalize: 2D array of input intensity values. + :param scale_max: Maximum output intensity value. Default is ``255``. + :param scale_min: Minimum output intensity value. Default is ``25``. + + :returns: Normalized 2D intensity map with values in ``[scale_min, max_val]``. + """ + imin = np.nanmin(image_to_normalize.data) + imax = np.nanmax(image_to_normalize.data) + norm = (image_to_normalize.data - imin) / (imax - imin) + return image_to_normalize.model_copy( + update={"data": scale_min + (scale_max - scale_min) * norm} + ) diff --git a/packages/scratch-core/src/renders/shading.py b/packages/scratch-core/src/renders/shading.py new file mode 100644 index 00000000..cd177f03 --- /dev/null +++ b/packages/scratch-core/src/renders/shading.py @@ -0,0 +1,153 @@ +from collections.abc import Iterable +import numpy as np +from typing import Final, NamedTuple + +from returns.pipeline import flow +from returns.result import safe + +from container_models.light_source import LightSource +from container_models.scan_image import ScanImage +from container_models.surface_normals import SurfaceNormals +from container_models.base import UnitVector3DArray, ScanMap2DArray +from utils.logger import log_railway_function + + +SPECULAR_FACTOR: Final[float] = 1.0 +PHONG_EXPONENT: Final[int] = 4 + + +class LightingComponents(NamedTuple): + """Container for lighting calculation components.""" + + light_vector: LightSource + observer_vector: LightSource + surface_normals: SurfaceNormals + half_vector: UnitVector3DArray | None = None + diffuse: ScanMap2DArray | None = None + specular: ScanMap2DArray | None = None + + +def _compute_half_vector(components: LightingComponents) -> LightingComponents: + """Compute and normalize the half-vector between light and observer directions.""" + h_vec = components.light_vector.unit_vector + components.observer_vector.unit_vector + return components._replace(half_vector=h_vec / np.linalg.norm(h_vec)) + + +def _compute_diffuse_lighting(components: LightingComponents) -> LightingComponents: + """Compute Lambertian diffuse reflection: max(N · L, 0).""" + x_light, y_light, z_light = components.light_vector.unit_vector + return components._replace( + diffuse=np.maximum( + x_light * components.surface_normals.x_normal_vector + + y_light * components.surface_normals.y_normal_vector + + z_light * components.surface_normals.z_normal_vector, + 0, + ) + ) + + +def _compute_specular_lighting(components: LightingComponents) -> LightingComponents: + """ + Compute Phong specular reflection: max(cos(2*arccos(max(N · H, 0))), 0)^n. + + Uses the half-vector H between light and observer directions. + """ + + if components.half_vector is None: + raise AttributeError + + x_half_vector, y_half_vector, z_half_vector = components.half_vector + + specular = np.maximum( + x_half_vector * components.surface_normals.x_normal_vector + + y_half_vector * components.surface_normals.y_normal_vector + + z_half_vector * components.surface_normals.z_normal_vector, + 0, + ) + specular = np.clip(specular, -1.0, 1.0) + specular = np.maximum(np.cos(2 * np.arccos(specular)), 0) ** PHONG_EXPONENT + + return components._replace(specular=specular) + + +def _combine_lighting_components( + components: LightingComponents, +) -> ScanMap2DArray: + """Combine diffuse and specular components with weighting factor.""" + + if components.diffuse is None or components.specular is None: + raise AttributeError + + return (components.diffuse + SPECULAR_FACTOR * components.specular) / ( + 1 + SPECULAR_FACTOR + ) + + +@log_railway_function("Calculating 2d maps per lighting source failed.") +def calculate_lighting( + light_vector: LightSource, + observer_vector: LightSource, + surface_normals: SurfaceNormals, +) -> ScanMap2DArray: + """ + Compute per-pixel lighting intensity from a light source and surface normals. + + Lighting is computed using Lambertian diffuse reflection combined with a + Phong specular component. + + :param light_vector: Normalized 3-element vector pointing toward the light source. + :param observer_vector: Normalized 3-element vector pointing toward the observer/camera. + :param surface_normals: 3D array of surface normals with shape (Height, Width, 3). + :param specular_factor: Weight of the specular component. Default is ``1.0``. + :param phong_exponent: Exponent controlling the sharpness of specular highlights. + Default is ``4``. + + :returns: 2D array of combined lighting intensities in ``[0, 1]`` with shape + (Height, Width). + """ + return flow( + LightingComponents(light_vector, observer_vector, surface_normals), + _compute_half_vector, + _compute_diffuse_lighting, + _compute_specular_lighting, + _combine_lighting_components, + ) + + +@log_railway_function("Failed to apply lights") +@safe +def apply_multiple_lights( + surface_normals: SurfaceNormals, + light_sources: Iterable[LightSource], + observer: LightSource, + scale_x: float, + scale_y: float, +) -> ScanImage: + """ + Apply multiple directional light sources to a surface and combine them into + a single intensity map. + + :param surface_normals: 3D array of surface normals with shape (Height, Width, 3). + :param light_vectors: Tuple of normalized 3-element light direction vectors. + :param observer_vector: Normalized 3-element vector pointing toward the observer. + :param lighting_calculator: Function used to compute lighting for a single light + source. Default is :func:`calculate_lighting`. + + :returns: ScanImage with 2D array of combined lighting intensities with shape + (Height, Width), where contributions from all lights are summed together. + """ + + return ScanImage( + data=np.nansum( + np.stack( + [ + calculate_lighting(light, observer, surface_normals) + for light in light_sources + ], + axis=-1, + ), + axis=2, + ), + scale_x=scale_x, + scale_y=scale_y, + ) From 4a6bef33eaca7580a413b81558df09669361757e Mon Sep 17 00:00:00 2001 From: "Sharlon N. Regales" Date: Thu, 11 Dec 2025 11:07:37 +0100 Subject: [PATCH 05/16] Create an abstract generic pipeline module Pipelines is an abstracted module that communicates with the returns layer. This means endpoints can call scratch-core function without caring without caring what the underlying protocol is used for failures/exceptions --- src/pipelines.py | 108 ++++++++++++++++++++++++++++++++++++++++ tests/test_pipelines.py | 26 ++++++++++ 2 files changed, 134 insertions(+) create mode 100644 src/pipelines.py create mode 100644 tests/test_pipelines.py diff --git a/src/pipelines.py b/src/pipelines.py new file mode 100644 index 00000000..51ec6362 --- /dev/null +++ b/src/pipelines.py @@ -0,0 +1,108 @@ +""" +Railway-oriented programming pipeline utilities. + +This module provides a simplified interface for executing functional pipelines using +railway-oriented programming (ROP) patterns, abstracting away the complexity of working +directly with the returns library's container types (IOResultE, ResultE, etc.). + +Railway-oriented programming is a functional error-handling pattern where operations +are chained together in a "railway" with two tracks: a success track and a failure track. +Each operation either continues on the success track or switches to the failure track, +propagating errors automatically without explicit error checking at each step. + +The main entry point, `run_pipeline`, allows developers to compose multiple operations +without manually handling container unwrapping, monadic binding, or error propagation. +It automatically: +- Handles both raw values and Container types as input +- Binds operations together using monadic composition +- Unwraps the final result or raises an HTTPException on failure + +This abstraction enables cleaner, more maintainable code by hiding the underlying +railway container mechanics while preserving type safety and functional error handling. +""" + +from collections.abc import Callable +from http import HTTPStatus +from typing import Any + +from fastapi import HTTPException +from returns.interfaces.container import ContainerN +from returns.io import IOResultE, IOSuccess +from returns.pipeline import flow +from returns.pointfree import bind +from returns.result import ResultE, Success + + +def _capture_ioresult_value[T](result: IOResultE[T] | ResultE[T], error_message: str) -> T: + match result: + case IOSuccess(Success(value)) | Success(value): + return value + case _: + raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=error_message) + + +def _pipeline_flow[T](entry_value: Any | ContainerN, *pipeline: Callable[..., Any]) -> IOResultE[T] | ResultE[T]: + first_function = None + pipeline_tasks: Any = pipeline + if not isinstance(entry_value, ContainerN): + first_function, *pipeline_tasks = pipeline + + return flow( + entry_value, + *((first_function,) if first_function else ()), + *[bind(task) for task in pipeline_tasks], + ) + + +def run_pipeline(entry_value: Any | ContainerN, *tasks: Callable[[Any], Any], error_message: str) -> Any: + """ + Execute a series of tasks in a functional pipeline and return the final result. + + This function orchestrates a railway-oriented programming pipeline using the returns + library. It takes an entry value and a sequence of tasks, executes them in order with + monadic binding, and unwraps the final result. + + Parameters + ---------- + entry_value : Any | ContainerN + The initial value to pass into the pipeline. Can be a raw value or + a Container from the returns library (IOResultE, ResultE, etc.). + *tasks : Callable[[Any], Any] + Variable number of callable tasks to execute in sequence. Each task should + accept the output of the previous task and return a Container or compatible type. + error_message : str + Custom error message to include in the HTTPException if the pipeline + fails at any step. + + Returns + ------- + Any + The unwrapped success value of type T from the final pipeline result. + + Raises + ------ + HTTPException + With status 500 (INTERNAL_SERVER_ERROR) if any task in the pipeline + fails or returns a failure Container. The exception detail will contain the + provided error_message. + + Examples + -------- + >>> def validate_user(data: dict) -> ResultE[dict]: + ... return Success(data) if data.get("id") else Failure("Invalid user") + >>> + >>> def enrich_user(data: dict) -> ResultE[dict]: + ... return Success({**data, "enriched": True}) + >>> + >>> result = run_pipeline( + ... {"id": 123}, + ... validate_user, + ... enrich_user, + ... error_message="User processing failed" + ... ) + >>> # Returns: {"id": 123, "enriched": True} + """ + return _capture_ioresult_value( + _pipeline_flow(entry_value, *tasks), + error_message, + ) diff --git a/tests/test_pipelines.py b/tests/test_pipelines.py new file mode 100644 index 00000000..73cd184b --- /dev/null +++ b/tests/test_pipelines.py @@ -0,0 +1,26 @@ +from collections.abc import Callable +from http import HTTPStatus +from pathlib import Path + +import pytest +from fastapi import HTTPException +from returns.io import impure_safe +from returns.result import safe + +from pipelines import run_pipeline + + +@pytest.mark.parametrize( + "pipeline", + [ + pytest.param((safe(lambda x: x / 0),), id="force a runtime error"), + pytest.param((impure_safe(lambda x: Path(str(x)).read_bytes()),), id="force on io error"), + ], +) +def test_pipeline_failure_raises_http_exception(pipeline: tuple[Callable, ...]) -> None: + """Test that pipeline failures raise HTTPException with status 500.""" + # Act & Assert + with pytest.raises(HTTPException, match="Failed pipeline") as exc_info: + run_pipeline(5, *pipeline, error_message="Failed pipeline") + + assert exc_info.value.status_code == HTTPStatus.INTERNAL_SERVER_ERROR From 2ed5414dcd9d9ed5c03f744699888ddf0c4f0a09 Mon Sep 17 00:00:00 2001 From: "Sharlon N. Regales" Date: Thu, 11 Dec 2025 11:10:42 +0100 Subject: [PATCH 06/16] Create some preprocessors pipelines --- packages/scratch-core/src/parsers/__init__.py | 3 +- packages/scratch-core/src/parsers/loaders.py | 50 +++++--- packages/scratch-core/src/renders/__init__.py | 4 +- packages/scratch-core/src/renders/image_io.py | 2 +- packages/scratch-core/tests/conftest.py | 2 - .../scratch-core/tests/helper_function.py | 4 +- .../tests/parsers/test_load_scan_image.py | 90 -------------- .../tests/parsers/test_loaders.py | 112 ++++++++++++++++++ .../tests/renders/test_display.py | 4 +- src/preprocessors/pipelines.py | 87 ++++++++++++++ tests/preprocessors/pipelines/conftest.py | 18 +++ .../pipelines/test_parse_scan_pipeline.py | 42 +++++++ .../pipelines/test_preview_pipeline.py | 36 ++++++ .../pipelines/test_surface_map_pipeline.py | 85 +++++++++++++ .../pipelines/test_x3p_pipeline.py | 44 +++++++ 15 files changed, 464 insertions(+), 119 deletions(-) delete mode 100644 packages/scratch-core/tests/parsers/test_load_scan_image.py create mode 100644 packages/scratch-core/tests/parsers/test_loaders.py create mode 100644 src/preprocessors/pipelines.py create mode 100644 tests/preprocessors/pipelines/conftest.py create mode 100644 tests/preprocessors/pipelines/test_parse_scan_pipeline.py create mode 100644 tests/preprocessors/pipelines/test_preview_pipeline.py create mode 100644 tests/preprocessors/pipelines/test_surface_map_pipeline.py create mode 100644 tests/preprocessors/pipelines/test_x3p_pipeline.py diff --git a/packages/scratch-core/src/parsers/__init__.py b/packages/scratch-core/src/parsers/__init__.py index 74b705d9..9372b322 100644 --- a/packages/scratch-core/src/parsers/__init__.py +++ b/packages/scratch-core/src/parsers/__init__.py @@ -35,12 +35,13 @@ - Custom file format support can be added via surfalize FileHandler registration """ -from .loaders import load_scan_image +from .loaders import load_scan_image, subsample_scan_image from .x3p import X3PMetaData, save_x3p, parse_to_x3p __all__ = ( "load_scan_image", "parse_to_x3p", "save_x3p", + "subsample_scan_image", "X3PMetaData", ) diff --git a/packages/scratch-core/src/parsers/loaders.py b/packages/scratch-core/src/parsers/loaders.py index f10e1e6b..7c87ba2a 100644 --- a/packages/scratch-core/src/parsers/loaders.py +++ b/packages/scratch-core/src/parsers/loaders.py @@ -2,6 +2,7 @@ import numpy as np from returns.io import impure_safe +from returns.result import safe from surfalize import Surface from surfalize.file import FileHandler from surfalize.file.al3d import MAGIC @@ -22,36 +23,47 @@ "Successfully loaded scan file", ) @impure_safe -def load_scan_image( - scan_file: Path, step_size_x: int = 1, step_size_y: int = 1 -) -> ScanImage: +def load_scan_image(scan_file: Path) -> ScanImage: """ - Load a scan image from a file and optionally subsample it. Parsed values will be converted to meters (m). - + Load a scan image from a file. Parsed values will be converted to meters (m). :param scan_file: The path to the file containing the scanned image data. - :param step_size_x: Denotes the number of steps to skip in X-direction (default: 1, no subsampling). - :param step_size_y: Denotes the number of steps to skip in Y-direction (default: 1, no subsampling). - :returns: An instance of `ScanImage`, optionally subsampled. + :returns: An instance of `ScanImage`. """ surface = Surface.load(scan_file) data = np.asarray(surface.data, dtype=np.float64) * micro step_x = surface.step_x * micro step_y = surface.step_y * micro - height, width = data.shape + return ScanImage( + data=data, + scale_x=step_x, + scale_y=step_y, + meta_data=surface.metadata, + ) + + +@log_railway_function( + "Failed to subsample image file", + "Successfully subsampled scan file", +) +@safe +def subsample_scan_image( + scan_image: ScanImage, step_size_x: int, step_size_y: int +) -> ScanImage: + """ + Subsample the data in a `ScanImage` instance by skipping steps in each dimension. + :param scan_image: The instance of `ScanImage` containing the 2D image data to subsample. + :param step_size_x: The number of steps to skip in the X-direction. + :param step_size_y: The number of steps to skip in the Y-direction. + :returns: An subsampled `ScanImage` with updated scales. + """ + width, height = scan_image.data.shape if not (0 < step_size_x < width and 0 < step_size_y < height): raise ValueError( f"Step size should be positive and smaller than the image size: {(height, width)}" ) - - if step_size_x > 1 or step_size_y > 1: - return ScanImage( - data=data[::step_size_y, ::step_size_x], - scale_x=step_x * step_size_x, - scale_y=step_y * step_size_y, - meta_data=surface.metadata, - ) - return ScanImage( - data=data, scale_x=step_x, scale_y=step_y, meta_data=surface.metadata + data=scan_image.data[::step_size_y, ::step_size_x].copy(), + scale_x=scan_image.scale_x * step_size_x, + scale_y=scan_image.scale_y * step_size_y, ) diff --git a/packages/scratch-core/src/renders/__init__.py b/packages/scratch-core/src/renders/__init__.py index 7c6e5449..d1078850 100644 --- a/packages/scratch-core/src/renders/__init__.py +++ b/packages/scratch-core/src/renders/__init__.py @@ -16,13 +16,13 @@ from .shading import apply_multiple_lights from .normalizations import compute_surface_normals, normalize_2d_array -from .image_io import save_image, scan_to_image, get_array_for_display +from .image_io import save_image, scan_to_image, get_scan_image_for_display __all__ = ( "apply_multiple_lights", "compute_surface_normals", - "get_array_for_display", + "get_scan_image_for_display", "normalize_2d_array", "save_image", "scan_to_image", diff --git a/packages/scratch-core/src/renders/image_io.py b/packages/scratch-core/src/renders/image_io.py index 7f94039b..5944a052 100644 --- a/packages/scratch-core/src/renders/image_io.py +++ b/packages/scratch-core/src/renders/image_io.py @@ -60,7 +60,7 @@ def _clip_data( @log_railway_function("Failed to retreive array for display") @safe -def get_array_for_display( +def get_scan_image_for_display( scan_image: ScanImage, *, std_scaler: float = 2.0 ) -> ScanImage: """ diff --git a/packages/scratch-core/tests/conftest.py b/packages/scratch-core/tests/conftest.py index fce1cdfb..d1a151ca 100644 --- a/packages/scratch-core/tests/conftest.py +++ b/packages/scratch-core/tests/conftest.py @@ -61,8 +61,6 @@ def scan_image_replica(scans_dir: Path) -> ScanImage: return unwrap_result( load_scan_image( scans_dir / "Klein_non_replica_mode.al3d", - step_size_x=1, - step_size_y=1, ) ) diff --git a/packages/scratch-core/tests/helper_function.py b/packages/scratch-core/tests/helper_function.py index 35f92526..a4d0e96a 100644 --- a/packages/scratch-core/tests/helper_function.py +++ b/packages/scratch-core/tests/helper_function.py @@ -1,8 +1,8 @@ from returns.io import IOResultE, IOSuccess -from returns.result import Success +from returns.result import ResultE, Success -def unwrap_result[T](result: IOResultE[T]) -> T: +def unwrap_result[T](result: IOResultE[T] | ResultE[T]) -> T: match result: case IOSuccess(Success(value)) | Success(value): return value diff --git a/packages/scratch-core/tests/parsers/test_load_scan_image.py b/packages/scratch-core/tests/parsers/test_load_scan_image.py deleted file mode 100644 index 7545baec..00000000 --- a/packages/scratch-core/tests/parsers/test_load_scan_image.py +++ /dev/null @@ -1,90 +0,0 @@ -from math import ceil -from pathlib import Path - -import numpy as np -import pytest -from scipy.constants import micro -from surfalize import Surface - -from parsers import load_scan_image -from returns.pipeline import is_successful - -from ..helper_function import unwrap_result - - -@pytest.fixture(scope="class") -def filepath(scans_dir: Path, request: pytest.FixtureRequest): - return scans_dir / request.param - - -@pytest.mark.parametrize( - "filepath", - [ - "Klein_non_replica_mode.al3d", - "Klein_non_replica_mode_X3P_Scratch.x3p", - ], - indirect=True, -) -class TestLoadScanImage: - @pytest.mark.parametrize("step_x, step_y", [(1, 1), (10, 10), (25, 25), (25, 50)]) - def test_load_scan_data_matches_size( - self, filepath: Path, step_x: int, step_y: int - ) -> None: - # Arrange - surface = Surface.load(filepath) - # Act - result = load_scan_image(filepath, step_x, step_y) - scan_image = unwrap_result(result) - - # Assert - assert scan_image.data.shape == ( - ceil(surface.data.shape[0] / step_y), - ceil(surface.data.shape[1] / step_x), - ) - - @pytest.mark.parametrize( - ("step_x", "step_y"), - [ - pytest.param(10, 10, id="default value"), - pytest.param(1, 10, id="only step y"), - pytest.param(10, 1, id="only x"), - pytest.param(10, 5, id="different x and y"), - ], - ) - def test_scan_map_updates_scales( - self, filepath: Path, step_x: int, step_y: int - ) -> None: - # arrange - surface = Surface.load(filepath) - # Act - result = load_scan_image(filepath, step_x, step_y) - scan_image = unwrap_result(result) - - # Assert - assert np.isclose(scan_image.scale_x, surface.step_x * step_x * micro) - assert np.isclose(scan_image.scale_y, surface.step_y * step_y * micro) - - @pytest.mark.parametrize( - "step_x, step_y", [(-2, 2), (0, 0), (0, 3), (2, -1), (-1, -1), (1e3, 1e4)] - ) - def test_load_scan_data_rejects_incorrect_sizes( - self, filepath: Path, step_x: int, step_y: int - ) -> None: - # act - result = load_scan_image(filepath, step_x, step_y) - # assert - assert not is_successful(result) - - -# TODO: find a better test methology -def test_load_scan_data_matches_baseline_output( - baseline_images_dir: Path, scans_dir: Path -) -> None: - # arrange - filepath = scans_dir / "Klein_non_replica_mode.al3d" - verified = np.load(baseline_images_dir / "replica_subsampled.npy") - # act - result = load_scan_image(filepath, step_size_x=10, step_size_y=15) - scan_image = unwrap_result(result) - # assert - assert np.allclose(scan_image.data, verified, equal_nan=True, atol=1.0e-5) diff --git a/packages/scratch-core/tests/parsers/test_loaders.py b/packages/scratch-core/tests/parsers/test_loaders.py new file mode 100644 index 00000000..46fb6564 --- /dev/null +++ b/packages/scratch-core/tests/parsers/test_loaders.py @@ -0,0 +1,112 @@ +from math import ceil +from pathlib import Path + +import numpy as np +import pytest +from returns.pipeline import is_successful +from scipy.constants import micro +from surfalize import Surface + +from container_models.scan_image import ScanImage +from parsers import load_scan_image, subsample_scan_image + +from ..helper_function import unwrap_result + + +@pytest.fixture(scope="class") +def filepath(scans_dir: Path, request: pytest.FixtureRequest): + return scans_dir / request.param + + +@pytest.mark.parametrize( + "filepath", + [ + "Klein_non_replica_mode.al3d", + "Klein_non_replica_mode_X3P_Scratch.x3p", + ], + indirect=True, +) +class TestLoadScanImage: + def test_load_scan_data_matches_size(self, filepath: Path) -> None: + # Arrange + surface = Surface.load(filepath) + # Act + result = load_scan_image(filepath) + scan_image = unwrap_result(result) + + # Assert + assert scan_image.data.shape == ( + ceil(surface.data.shape[0]), + ceil(surface.data.shape[1]), + ) + assert scan_image.scale_y == surface.step_y * micro + assert scan_image.scale_x == surface.step_x * micro + + +class TestSubSampleScanImage: + # TODO: find a better test methology + def test_subsample_matches_baseline_output( + self, baseline_images_dir: Path, scan_image_replica: ScanImage + ) -> None: + # arrange + verified = np.load(baseline_images_dir / "replica_subsampled.npy") + # act + result = subsample_scan_image(scan_image_replica, 10, 15) + subsampled = unwrap_result(result) + # assert + assert np.allclose( + subsampled.data, + verified, + equal_nan=True, + atol=0.001, + ) + + @pytest.mark.parametrize( + "step_size_x, step_size_y", [(1, 1), (10, 10), (25, 25), (25, 50)] + ) + def test_subsample_matches_size( + self, scan_image: ScanImage, step_size_x: int, step_size_y: int + ): + # Arrange + expected_height = ceil(scan_image.data.shape[0] / step_size_y) + expected_width = ceil(scan_image.data.shape[1] / step_size_x) + + # Act + result = subsample_scan_image(scan_image, step_size_x, step_size_y) + subsampled = unwrap_result(result) + + # Assert + assert subsampled.data.shape == (expected_height, expected_width) + + @pytest.mark.parametrize( + ("step_x", "step_y"), + [ + pytest.param(1, 1, id="default value"), + pytest.param(10, 1, id="only x"), + pytest.param(1, 10, id="only y"), + pytest.param(10, 5, id="different x and y"), + ], + ) + def test_subsample_updates_scan_image_scales( + self, scan_image: ScanImage, step_x: int, step_y: int + ) -> None: + # Act + result = subsample_scan_image(scan_image, step_x, step_y) + subsampled = unwrap_result(result) + + # Assert + assert np.isclose(subsampled.scale_x, scan_image.scale_x * step_x, atol=1.0e-3) + assert np.isclose(subsampled.scale_y, scan_image.scale_y * step_y, atol=1.0e-3) + + @pytest.mark.parametrize( + "step_size_x, step_size_y", + [(-2, 2), (0, 0), (0, 3), (2, -1), (-1, -1), (1e3, 1e4)], + ) + def test_subsample_rejects_incorrect_sizes( + self, scan_image: ScanImage, step_size_x: int, step_size_y: int + ): + # Act + result = subsample_scan_image(scan_image, step_size_x, step_size_y) + + # Assert + assert not is_successful(result) diff --git a/packages/scratch-core/tests/renders/test_display.py b/packages/scratch-core/tests/renders/test_display.py index e23e8e9d..808be8f9 100644 --- a/packages/scratch-core/tests/renders/test_display.py +++ b/packages/scratch-core/tests/renders/test_display.py @@ -3,7 +3,7 @@ import pytest from numpy.testing import assert_array_almost_equal -from renders import get_array_for_display +from renders import get_scan_image_for_display from container_models.scan_image import ScanImage @@ -14,6 +14,6 @@ def test_get_image_for_display_matches_baseline_image( # arrange verified = np.load(baseline_images_dir / "display_array.npy") # act - display_image = get_array_for_display(scan_image_with_nans).unwrap() + display_image = get_scan_image_for_display(scan_image_with_nans).unwrap() # assert assert_array_almost_equal(display_image.data, verified) diff --git a/src/preprocessors/pipelines.py b/src/preprocessors/pipelines.py new file mode 100644 index 00000000..2aa854c1 --- /dev/null +++ b/src/preprocessors/pipelines.py @@ -0,0 +1,87 @@ +from functools import partial +from pathlib import Path + +from container_models.scan_image import ScanImage +from parsers import load_scan_image, parse_to_x3p, save_x3p, subsample_scan_image +from renders import ( + apply_multiple_lights, + compute_surface_normals, + get_scan_image_for_display, + save_image, + scan_to_image, +) +from renders.normalizations import normalize_2d_array + +from pipelines import run_pipeline +from preprocessors.schemas import UploudScanParameters + + +def parse_scan_pipeline(scan_file: Path, parameters: UploudScanParameters) -> ScanImage: + """ + Parse a scan file and load it as a ScanImage. + + :param scan_file: The path to the scan file to parse. + :returns: The parsed scan image data. + :raises HTTPException: If the file cannot be parsed or read. + """ + return run_pipeline( + scan_file, + load_scan_image, + partial(subsample_scan_image, **parameters.as_dict(include={"step_size_x", "step_size_y"})), + error_message=f"Failed to parsed given scan file: {scan_file}", + ) + + +def x3p_pipeline(parsed_scan: ScanImage, output_path: Path) -> Path: + """ + Convert a scan image to X3P format and save it to the specified path. + + :param parsed_scan: The scan image data to convert to X3P format. + :param output_path: The file path where the X3P file will be saved. + :returns: The path to the saved X3P file. + :raises HTTPException: If conversion or saving fails. + """ + return run_pipeline( + parsed_scan, + parse_to_x3p, + partial(save_x3p, output_path=output_path), + error_message=f"Failed to create the x3p: {output_path}", + ) + + +def surface_map_pipeline(parsed_scan: ScanImage, output_path: Path, parameters: UploudScanParameters) -> Path: + """ + Generate a 3D surface map image from scan data and save it to the specified path. + + :param parsed_scan: The scan image data to generate a surface map from. + :param output_path: The file path where the surface map image will be saved. + :returns: The path to the saved surface map image file. + :raises HTTPException: If image generation or saving fails. + """ + return run_pipeline( + parsed_scan, + compute_surface_normals, + partial(apply_multiple_lights, **parameters.as_dict(exclude={"step_size_x", "step_size_y"})), + normalize_2d_array, + scan_to_image, + partial(save_image, output_path=output_path), + error_message=f"Failed to create the surface map: {output_path}", + ) + + +def preview_pipeline(parsed_scan: ScanImage, output_path: Path) -> Path: + """ + Generate a preview image from scan data and save it to the specified path. + + :param parsed_scan: The scan image data to generate a surface map from. + :param output_path: The file path where the preview image will be saved. + :returns: The path to the saved preview image file. + :raises HTTPException: If image generation or saving fails. + """ + return run_pipeline( + parsed_scan, + get_scan_image_for_display, + scan_to_image, + partial(save_image, output_path=output_path), + error_message=f"Failed to create the surface map: {output_path}", + ) diff --git a/tests/preprocessors/pipelines/conftest.py b/tests/preprocessors/pipelines/conftest.py new file mode 100644 index 00000000..dec5e480 --- /dev/null +++ b/tests/preprocessors/pipelines/conftest.py @@ -0,0 +1,18 @@ +from pathlib import Path + +import pytest +from container_models.scan_image import ScanImage + +from preprocessors.pipelines import parse_scan_pipeline +from preprocessors.schemas import UploudScanParameters + + +@pytest.fixture(scope="session") +def default_parameters() -> UploudScanParameters: + return UploudScanParameters.model_construct() + + +@pytest.fixture(scope="session") +def parsed_al3d_file(scan_directory: Path, default_parameters: UploudScanParameters) -> ScanImage: + """Parse the circle.al3d test file.""" + return parse_scan_pipeline(scan_directory / "circle.al3d", default_parameters) diff --git a/tests/preprocessors/pipelines/test_parse_scan_pipeline.py b/tests/preprocessors/pipelines/test_parse_scan_pipeline.py new file mode 100644 index 00000000..a9df1dff --- /dev/null +++ b/tests/preprocessors/pipelines/test_parse_scan_pipeline.py @@ -0,0 +1,42 @@ +from pathlib import Path + +import numpy as np +import pytest +from container_models.scan_image import ScanImage +from pydantic import ValidationError + +from preprocessors.pipelines import parse_scan_pipeline +from preprocessors.schemas import UploudScanParameters + + +@pytest.mark.integration +class TestParseScanPipeline: + @pytest.mark.parametrize( + "extension", + [".al3d", ".x3p"], + ) + def test_parse_supported_file_success( + self, extension: str, scan_directory: Path, default_parameters: UploudScanParameters + ) -> None: + """Test that supported file formats are parsed successfully.""" + # Act + result = parse_scan_pipeline((scan_directory / "circle").with_suffix(extension), default_parameters) + + # Assert + assert isinstance(result, ScanImage) + assert result.data.size > 0 + height, width = result.data.shape + assert height > 0 + assert width > 0 + assert np.isfinite(result.scale_x) + assert np.isfinite(result.scale_y) + + def test_parse_result_is_immutable(self, scan_directory: Path, default_parameters: UploudScanParameters) -> None: + """Test that ScanImage is immutable and cannot be modified after creation.""" + # Act + result = parse_scan_pipeline(scan_directory / "circle.x3p", default_parameters) + + # Assert + # ScanImage should be frozen, so attempting to modify should fail + with pytest.raises(ValidationError, match="Instance is frozen"): + result.scale_x = 999.0 diff --git a/tests/preprocessors/pipelines/test_preview_pipeline.py b/tests/preprocessors/pipelines/test_preview_pipeline.py new file mode 100644 index 00000000..27539e8e --- /dev/null +++ b/tests/preprocessors/pipelines/test_preview_pipeline.py @@ -0,0 +1,36 @@ +from pathlib import Path + +import pytest +from container_models.scan_image import ScanImage +from PIL import Image + +from preprocessors.pipelines import preview_pipeline + + +@pytest.mark.integration +class TestPreviewPipeline: + """Integration tests for preview_pipeline function.""" + + def test_generate_preview_success(self, parsed_al3d_file: ScanImage, tmp_path: Path) -> None: + """Test that a preview image is successfully generated from scan data.""" + # Arrange + output_path = tmp_path / "preview.png" + + # Act + result_path = preview_pipeline(parsed_al3d_file, output_path) + + # Assert + assert result_path == output_path + assert output_path.exists() + assert output_path.is_file() + assert output_path.stat().st_size > 0 + + def test_preview_is_valid_png_image(self, parsed_al3d_file: ScanImage, tmp_path: Path) -> None: + """Test that the generated preview file is a valid PNG image that can be opened.""" + # Act + preview = preview_pipeline(parsed_al3d_file, output_path=tmp_path / "preview.png") + + # Assert - verify we can open the PNG file + with Image.open(preview) as img: + assert img.format == "PNG" + assert img.size == parsed_al3d_file.data.shape diff --git a/tests/preprocessors/pipelines/test_surface_map_pipeline.py b/tests/preprocessors/pipelines/test_surface_map_pipeline.py new file mode 100644 index 00000000..74552dfb --- /dev/null +++ b/tests/preprocessors/pipelines/test_surface_map_pipeline.py @@ -0,0 +1,85 @@ +from pathlib import Path + +import pytest +from container_models.light_source import LightSource +from container_models.scan_image import ScanImage +from PIL import Image + +from preprocessors.pipelines import surface_map_pipeline +from preprocessors.schemas import UploudScanParameters + + +@pytest.fixture(scope="module") +def light_sources() -> tuple[LightSource, LightSource]: + """Surface map light sources.""" + return ( + LightSource(azimuth=45, elevation=45), + LightSource(azimuth=135, elevation=45), + ) + + +@pytest.fixture(scope="module") +def observer() -> LightSource: + """Observer position looking straight down from +Z direction.""" + return LightSource(azimuth=0, elevation=90) + + +@pytest.mark.integration +class TestSurfaceMapPipeline: + """Integration tests for surface_map_pipeline function.""" + + def test_generate_surface_map_success( + self, + parsed_al3d_file: ScanImage, + default_parameters: UploudScanParameters, + tmp_path: Path, + ) -> None: + """Test that a surface map image is successfully generated from scan data.""" + # Arrange + output_path = tmp_path / "surface_map.png" + + # Act + result_path = surface_map_pipeline(parsed_al3d_file, output_path, default_parameters) + + # Assert + assert result_path == output_path + assert output_path.exists() + assert output_path.is_file() + assert output_path.stat().st_size > 0 + + def test_output_is_valid_png_image( + self, + parsed_al3d_file: ScanImage, + default_parameters: UploudScanParameters, + tmp_path: Path, + ) -> None: + """Test that the generated file is a valid PNG image that can be opened.""" + # Arrange + + # Act + surface_map = surface_map_pipeline(parsed_al3d_file, tmp_path / "surfacemap.png", default_parameters) + + # Assert - verify we can open the PNG file + with Image.open(surface_map) as img: + assert img.format == "PNG" + assert img.size == parsed_al3d_file.data.shape + + def test_surface_map_with_multiple_lights(self, parsed_al3d_file: ScanImage, tmp_path: Path) -> None: + """Test surface map generation with multiple light sources.""" + # Arrange - simulate lighting from 4 cardinal directions + parameters = UploudScanParameters( # type: ignore + light_sources=( + LightSource(azimuth=0, elevation=45), # North + LightSource(azimuth=90, elevation=45), # East + LightSource(azimuth=180, elevation=45), # South + LightSource(azimuth=270, elevation=45), # West + ), + observer=LightSource(azimuth=0, elevation=90), + ) + + # Act + surface_map = surface_map_pipeline(parsed_al3d_file, tmp_path / "multi_light_surfacemap.png", parameters) + + # Assert + with Image.open(surface_map) as img: + assert img.format == "PNG" diff --git a/tests/preprocessors/pipelines/test_x3p_pipeline.py b/tests/preprocessors/pipelines/test_x3p_pipeline.py new file mode 100644 index 00000000..449e9cd4 --- /dev/null +++ b/tests/preprocessors/pipelines/test_x3p_pipeline.py @@ -0,0 +1,44 @@ +from pathlib import Path + +import numpy as np +import pytest +from container_models.scan_image import ScanImage + +from preprocessors.pipelines import parse_scan_pipeline, x3p_pipeline +from preprocessors.schemas import UploudScanParameters + + +@pytest.mark.integration +class TestX3pPipeline: + """Integration tests for x3p_pipeline function.""" + + def test_convert_scan_to_x3p_success(self, parsed_al3d_file: ScanImage, tmp_path: Path) -> None: + """Test that a ScanImage is successfully converted to X3P format.""" + # Arrange + output_path = tmp_path / "output.x3p" + + # Act + result_path = x3p_pipeline(parsed_al3d_file, output_path) + + # Assert + assert output_path == result_path + assert output_path.is_file() + assert output_path.stat().st_size > 0 + + # TODO: can we assert this differently? + def test_output_file_is_valid_x3p( + self, parsed_al3d_file: ScanImage, tmp_path: Path, default_parameters: UploudScanParameters + ) -> None: + """Test that the output file can be parsed back as a valid X3P file.""" + # Arrange + output_path = tmp_path / "output.x3p" + + # Act + x3p_pipeline(parsed_al3d_file, output_path) + + # Assert - verify we can parse the generated X3P file + reparsed_scan = parse_scan_pipeline(output_path, default_parameters) + assert isinstance(reparsed_scan, ScanImage) + assert reparsed_scan.data.shape == parsed_al3d_file.data.shape + assert np.allclose(reparsed_scan.scale_x, parsed_al3d_file.scale_x) + assert np.allclose(reparsed_scan.scale_y, parsed_al3d_file.scale_y) From 86a49bed4057e6093eb5c09445a42dd2cf3f0a94 Mon Sep 17 00:00:00 2001 From: "Sharlon N. Regales" Date: Thu, 11 Dec 2025 11:15:51 +0100 Subject: [PATCH 07/16] Update preprocessors schemas Create an upload scan parameters model that hold all the parameters to be used for the pipelines. All the parameters have a defaults and defaults will be generated if no parameters is given, thus nothing changes for the user or endpoint contract --- src/preprocessors/schemas.py | 70 +++++++++++++++++-- ...can.py => test_processed_data_location.py} | 0 .../preprocessors/schemas/test_upload_scan.py | 44 ++++-------- 3 files changed, 79 insertions(+), 35 deletions(-) rename tests/preprocessors/schemas/{test_process_scan.py => test_processed_data_location.py} (100%) diff --git a/src/preprocessors/schemas.py b/src/preprocessors/schemas.py index 33b7ee93..f7fb5441 100644 --- a/src/preprocessors/schemas.py +++ b/src/preprocessors/schemas.py @@ -2,6 +2,7 @@ from pathlib import Path from typing import Self +from container_models.light_source import LightSource from pydantic import DirectoryPath, Field, FilePath, field_validator, model_validator from models import BaseModelConfig @@ -14,22 +15,83 @@ class SupportedExtension(StrEnum): PLU = auto() +class UploudScanParameters(BaseModelConfig): + """Configuration parameters for upload scan's surface rendering process.""" + + light_sources: tuple[LightSource, ...] = Field( + ( + LightSource(azimuth=90, elevation=45), + LightSource(azimuth=180, elevation=45), + ), + description="Light sources for surface illumination rendering.", + ) + observer: LightSource = Field( + LightSource(azimuth=90, elevation=45), + description="Observer viewpoint vector for surface rendering.", + ) + scale_x: float = Field(1.0, gt=0.0, description="pixel size in meters (m)") + scale_y: float = Field(1.0, gt=0.0, description="pixel size in meters (m)") + step_size_x: int = Field(1, gt=0) + step_size_y: int = Field(1, gt=0) + + def as_dict(self, *, exclude: set[str] | None = None, include: set[str] | None = None) -> dict: + """ + Get model fields as dict with nested models intact (not serialized). + + :param exclude: Set of field names to exclude + :param include: Set of field names to include (mutually exclusive with exclude) + """ + if exclude and include: + raise ValueError("Cannot specify both 'exclude' and 'include'") + + fields = set(self.__class__.model_fields) + + if include: + fields = include + elif exclude: + fields = fields - exclude + + return {field: getattr(self, field) for field in fields} + + class UploadScan(BaseModelConfig): scan_file: FilePath = Field( ..., - description="Upload scan file.", - examples=[Path("./temp/scan.al3d"), Path("./temp/scan.x3p"), Path("./temp/scan.sur"), Path("./temp/scan.plu")], + description="Path to the input scan file. Supported formats: AL3D, X3P, SUR, PLU.", ) output_dir: DirectoryPath = Field( - ..., description="Upload output directory.", examples=[Path("./documents/project_x")] + ..., + description="Directory where processed outputs (X3P, preview, and surface map images) will be saved.", + ) + parameters: UploudScanParameters = Field( + default_factory=UploudScanParameters.model_construct, ) + @property + def surfacemap_path(self) -> Path: + return self.__output_partial_path("_surfacemap").with_suffix(".png") + + @property + def preview_path(self) -> Path: + return self.__output_partial_path("_preview").with_suffix(".png") + + @property + def x3p_path(self) -> Path: + return self.__output_partial_path().with_suffix(".x3p") + + def __output_partial_path(self, postfix: str | None = None) -> Path: + return self.output_dir / f"{self.scan_file.stem}{postfix or ''}" + @field_validator("scan_file", mode="after") @classmethod def validate_file_extension(cls, scan_file: FilePath) -> FilePath: - """Validate given file is off a supported type.""" + """Validate given file is of a supported type and not empty.""" if scan_file.suffix[1:] not in SupportedExtension: raise ValueError(f"unsupported extension: {scan_file.name}") + + if scan_file.stat().st_size == 0: + raise ValueError(f"file is empty: {scan_file.name}") + return scan_file diff --git a/tests/preprocessors/schemas/test_process_scan.py b/tests/preprocessors/schemas/test_processed_data_location.py similarity index 100% rename from tests/preprocessors/schemas/test_process_scan.py rename to tests/preprocessors/schemas/test_processed_data_location.py diff --git a/tests/preprocessors/schemas/test_upload_scan.py b/tests/preprocessors/schemas/test_upload_scan.py index 4ee49e81..c8b11e6c 100644 --- a/tests/preprocessors/schemas/test_upload_scan.py +++ b/tests/preprocessors/schemas/test_upload_scan.py @@ -10,19 +10,14 @@ @pytest.fixture -def output_dir(tmp_path: Path) -> Path: +def output_dir(tmp_path_factory: pytest.TempPathFactory) -> Path: """Create a temporary output directory.""" - output_dir = tmp_path / "output" - output_dir.mkdir() - return output_dir + return tmp_path_factory.mktemp("upload_scan_schema") -@pytest.fixture -def valid_scan_file(tmp_path: Path) -> Path: - """Create a temporary scan file with valid extension.""" - scan_file = tmp_path / "test_scan.x3p" - scan_file.touch() - return scan_file +@pytest.fixture(scope="module") +def valid_scan_file(scan_directory: Path) -> Path: + return scan_directory / "circle.x3p" @pytest.mark.parametrize( @@ -33,10 +28,10 @@ def test_all_supported_extensions(tmp_path: Path, output_dir: Path, extension: s """Test that all supported extensions are accepted.""" # Arrange scan_file = tmp_path / f"test_scan.{extension}" - scan_file.touch() + scan_file.write_text("just words") # Act - upload_scan = UploadScan(scan_file=scan_file, output_dir=output_dir) + upload_scan = UploadScan(scan_file=scan_file, output_dir=output_dir) # type: ignore # Assert assert upload_scan.scan_file == scan_file @@ -60,7 +55,7 @@ def test_unsupported_extension_raises_error(extension: str, tmp_path_factory: py # Act & Assert with pytest.raises(ValidationError) as exc_info: - UploadScan(scan_file=scan_file, output_dir=output_dir) + UploadScan(scan_file=scan_file, output_dir=output_dir) # type: ignore assert "unsupported extension" in str(exc_info.value) @@ -71,7 +66,7 @@ def test_nonexistent_scan_file_raises_error(output_dir: Path) -> None: # Act & Assert with pytest.raises(ValidationError) as exc_info: - UploadScan(scan_file=nonexistent_file, output_dir=output_dir) + UploadScan(scan_file=nonexistent_file, output_dir=output_dir) # type: ignore assert "Path does not point to a file" in str(exc_info.value) @@ -82,7 +77,7 @@ def test_nonexistent_output_dir_raises_error(valid_scan_file: Path) -> None: # Act & Assert with pytest.raises(ValidationError) as exc_info: - UploadScan(scan_file=valid_scan_file, output_dir=nonexistent_dir) + UploadScan(scan_file=valid_scan_file, output_dir=nonexistent_dir) # type: ignore assert "Path does not point to a directory" in str(exc_info.value) @@ -94,28 +89,15 @@ def test_scan_file_as_directory_raises_error(tmp_path: Path, output_dir: Path) - # Act & Assert with pytest.raises(ValidationError) as exc_info: - UploadScan(scan_file=directory, output_dir=output_dir) + UploadScan(scan_file=directory, output_dir=output_dir) # type: ignore assert "Path does not point to a file" in str(exc_info.value) -def test_output_dir_as_file_raises_error(tmp_path: Path, valid_scan_file: Path) -> None: - """Test that providing a file as output_dir raises ValidationError.""" - # Arrange - file_not_dir = tmp_path / "not_a_directory.txt" - file_not_dir.touch() - - # Act & Assert - with pytest.raises(ValidationError) as exc_info: - UploadScan(scan_file=valid_scan_file, output_dir=file_not_dir) - assert "Path does not point to a directory" in str(exc_info.value) - - def test_model_is_frozen(valid_scan_file: Path, output_dir: Path) -> None: """Test that UploadScan instances are immutable (frozen).""" # Arrange - upload_scan = UploadScan( - scan_file=valid_scan_file, - output_dir=output_dir, + upload_scan = UploadScan( # type: ignore + scan_file=valid_scan_file, output_dir=output_dir ) # Act & Assert From c46830619324e4dbf4c1f1f693372f0fb5a19b04 Mon Sep 17 00:00:00 2001 From: "Sharlon N. Regales" Date: Thu, 11 Dec 2025 11:20:40 +0100 Subject: [PATCH 08/16] Entegrate new pipeline structure to the pipeline --- src/preprocessors/pipelines.py | 6 +- src/preprocessors/router.py | 36 +--- src/preprocessors/schemas.py | 6 +- tests/conftest.py | 8 + tests/preprocessors/pipelines/conftest.py | 8 +- .../pipelines/test_parse_scan_pipeline.py | 6 +- .../pipelines/test_surface_map_pipeline.py | 8 +- .../pipelines/test_x3p_pipeline.py | 4 +- tests/preprocessors/test_router.py | 195 +++++++++++------- 9 files changed, 153 insertions(+), 124 deletions(-) diff --git a/src/preprocessors/pipelines.py b/src/preprocessors/pipelines.py index 2aa854c1..1a47adc6 100644 --- a/src/preprocessors/pipelines.py +++ b/src/preprocessors/pipelines.py @@ -13,10 +13,10 @@ from renders.normalizations import normalize_2d_array from pipelines import run_pipeline -from preprocessors.schemas import UploudScanParameters +from preprocessors.schemas import UploadScanParameters -def parse_scan_pipeline(scan_file: Path, parameters: UploudScanParameters) -> ScanImage: +def parse_scan_pipeline(scan_file: Path, parameters: UploadScanParameters) -> ScanImage: """ Parse a scan file and load it as a ScanImage. @@ -49,7 +49,7 @@ def x3p_pipeline(parsed_scan: ScanImage, output_path: Path) -> Path: ) -def surface_map_pipeline(parsed_scan: ScanImage, output_path: Path, parameters: UploudScanParameters) -> Path: +def surface_map_pipeline(parsed_scan: ScanImage, output_path: Path, parameters: UploadScanParameters) -> Path: """ Generate a 3D surface map image from scan data and save it to the specified path. diff --git a/src/preprocessors/router.py b/src/preprocessors/router.py index d30d580c..09851c2d 100644 --- a/src/preprocessors/router.py +++ b/src/preprocessors/router.py @@ -1,14 +1,8 @@ -from fastapi import APIRouter -from fastapi.exceptions import HTTPException -from image_generation.image_generation import compute_3d_image, get_array_for_display -from loguru import logger -from parsers import load_scan_image -from parsers.exceptions import ExportError -from parsers.x3p import save_to_x3p +from http import HTTPStatus -from preprocessors.helpers import export_image_pipeline -from preprocessors.models import ErrorImageGenerationModel, ParsingError +from fastapi import APIRouter +from .pipelines import parse_scan_pipeline, preview_pipeline, surface_map_pipeline, x3p_pipeline from .schemas import ProcessedDataLocation, UploadScan preprocessor_route = APIRouter( @@ -42,11 +36,7 @@ async def comparison_root() -> dict[str, str]: The endpoint parses and validates the file before running the processing pipeline. """, responses={ - 400: {"description": "parse error", "model": ParsingError}, - 500: { - "description": "image generation error", - "model": ErrorImageGenerationModel, - }, + HTTPStatus.INTERNAL_SERVER_ERROR: {"description": "image generation error"}, }, ) async def process_scan(upload_scan: UploadScan) -> ProcessedDataLocation: @@ -57,19 +47,9 @@ async def process_scan(upload_scan: UploadScan) -> ProcessedDataLocation: necessary processing steps, and produces several outputs such as an X3P file, a preview image, and a surface map saved to the output directory. """ - surface_image_path = upload_scan.output_dir / "surface_map.png" - preview_image_path = upload_scan.output_dir / "preview.png" - scan_file_path = upload_scan.output_dir / "scan.x3p" - parsed_scan = load_scan_image(upload_scan.scan_file).subsample(step_x=1, step_y=1) - try: - save_to_x3p(image=parsed_scan, output_path=scan_file_path) - except ExportError as err: - logger.error(f"Exporting x3p failed to path:{scan_file_path}, from error:{str(err)}") - raise HTTPException(status_code=500, detail=f"Failed to save the scan file {scan_file_path}: {str(err)}") - export_image_pipeline(file_path=surface_image_path, image_generator=compute_3d_image, scan_image=parsed_scan) - export_image_pipeline(file_path=preview_image_path, image_generator=get_array_for_display, scan_image=parsed_scan) + parsed_scan = parse_scan_pipeline(upload_scan.scan_file, upload_scan.parameters) return ProcessedDataLocation( - x3p_image=scan_file_path, - preview_image=preview_image_path, - surfacemap_image=surface_image_path, + x3p_image=x3p_pipeline(parsed_scan, upload_scan.x3p_path), + surfacemap_image=surface_map_pipeline(parsed_scan, upload_scan.surfacemap_path, upload_scan.parameters), + preview_image=preview_pipeline(parsed_scan, upload_scan.preview_path), ) diff --git a/src/preprocessors/schemas.py b/src/preprocessors/schemas.py index f7fb5441..65e89a5d 100644 --- a/src/preprocessors/schemas.py +++ b/src/preprocessors/schemas.py @@ -15,7 +15,7 @@ class SupportedExtension(StrEnum): PLU = auto() -class UploudScanParameters(BaseModelConfig): +class UploadScanParameters(BaseModelConfig): """Configuration parameters for upload scan's surface rendering process.""" light_sources: tuple[LightSource, ...] = Field( @@ -63,8 +63,8 @@ class UploadScan(BaseModelConfig): ..., description="Directory where processed outputs (X3P, preview, and surface map images) will be saved.", ) - parameters: UploudScanParameters = Field( - default_factory=UploudScanParameters.model_construct, + parameters: UploadScanParameters = Field( + default_factory=UploadScanParameters.model_construct, ) @property diff --git a/tests/conftest.py b/tests/conftest.py index 93c26d4d..6acdba5a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,9 @@ +from pathlib import Path + import pytest from fastapi.testclient import TestClient +from constants import PROJECT_ROOT from main import app @@ -8,3 +11,8 @@ def client(): with TestClient(app) as c: yield c + + +@pytest.fixture(scope="session") +def scan_directory() -> Path: + return PROJECT_ROOT / "packages/scratch-core/tests/resources/scans" diff --git a/tests/preprocessors/pipelines/conftest.py b/tests/preprocessors/pipelines/conftest.py index dec5e480..69a0081f 100644 --- a/tests/preprocessors/pipelines/conftest.py +++ b/tests/preprocessors/pipelines/conftest.py @@ -4,15 +4,15 @@ from container_models.scan_image import ScanImage from preprocessors.pipelines import parse_scan_pipeline -from preprocessors.schemas import UploudScanParameters +from preprocessors.schemas import UploadScanParameters @pytest.fixture(scope="session") -def default_parameters() -> UploudScanParameters: - return UploudScanParameters.model_construct() +def default_parameters() -> UploadScanParameters: + return UploadScanParameters.model_construct() @pytest.fixture(scope="session") -def parsed_al3d_file(scan_directory: Path, default_parameters: UploudScanParameters) -> ScanImage: +def parsed_al3d_file(scan_directory: Path, default_parameters: UploadScanParameters) -> ScanImage: """Parse the circle.al3d test file.""" return parse_scan_pipeline(scan_directory / "circle.al3d", default_parameters) diff --git a/tests/preprocessors/pipelines/test_parse_scan_pipeline.py b/tests/preprocessors/pipelines/test_parse_scan_pipeline.py index a9df1dff..d26169f7 100644 --- a/tests/preprocessors/pipelines/test_parse_scan_pipeline.py +++ b/tests/preprocessors/pipelines/test_parse_scan_pipeline.py @@ -6,7 +6,7 @@ from pydantic import ValidationError from preprocessors.pipelines import parse_scan_pipeline -from preprocessors.schemas import UploudScanParameters +from preprocessors.schemas import UploadScanParameters @pytest.mark.integration @@ -16,7 +16,7 @@ class TestParseScanPipeline: [".al3d", ".x3p"], ) def test_parse_supported_file_success( - self, extension: str, scan_directory: Path, default_parameters: UploudScanParameters + self, extension: str, scan_directory: Path, default_parameters: UploadScanParameters ) -> None: """Test that supported file formats are parsed successfully.""" # Act @@ -31,7 +31,7 @@ def test_parse_supported_file_success( assert np.isfinite(result.scale_x) assert np.isfinite(result.scale_y) - def test_parse_result_is_immutable(self, scan_directory: Path, default_parameters: UploudScanParameters) -> None: + def test_parse_result_is_immutable(self, scan_directory: Path, default_parameters: UploadScanParameters) -> None: """Test that ScanImage is immutable and cannot be modified after creation.""" # Act result = parse_scan_pipeline(scan_directory / "circle.x3p", default_parameters) diff --git a/tests/preprocessors/pipelines/test_surface_map_pipeline.py b/tests/preprocessors/pipelines/test_surface_map_pipeline.py index 74552dfb..3904220f 100644 --- a/tests/preprocessors/pipelines/test_surface_map_pipeline.py +++ b/tests/preprocessors/pipelines/test_surface_map_pipeline.py @@ -6,7 +6,7 @@ from PIL import Image from preprocessors.pipelines import surface_map_pipeline -from preprocessors.schemas import UploudScanParameters +from preprocessors.schemas import UploadScanParameters @pytest.fixture(scope="module") @@ -31,7 +31,7 @@ class TestSurfaceMapPipeline: def test_generate_surface_map_success( self, parsed_al3d_file: ScanImage, - default_parameters: UploudScanParameters, + default_parameters: UploadScanParameters, tmp_path: Path, ) -> None: """Test that a surface map image is successfully generated from scan data.""" @@ -50,7 +50,7 @@ def test_generate_surface_map_success( def test_output_is_valid_png_image( self, parsed_al3d_file: ScanImage, - default_parameters: UploudScanParameters, + default_parameters: UploadScanParameters, tmp_path: Path, ) -> None: """Test that the generated file is a valid PNG image that can be opened.""" @@ -67,7 +67,7 @@ def test_output_is_valid_png_image( def test_surface_map_with_multiple_lights(self, parsed_al3d_file: ScanImage, tmp_path: Path) -> None: """Test surface map generation with multiple light sources.""" # Arrange - simulate lighting from 4 cardinal directions - parameters = UploudScanParameters( # type: ignore + parameters = UploadScanParameters( # type: ignore light_sources=( LightSource(azimuth=0, elevation=45), # North LightSource(azimuth=90, elevation=45), # East diff --git a/tests/preprocessors/pipelines/test_x3p_pipeline.py b/tests/preprocessors/pipelines/test_x3p_pipeline.py index 449e9cd4..94e0b992 100644 --- a/tests/preprocessors/pipelines/test_x3p_pipeline.py +++ b/tests/preprocessors/pipelines/test_x3p_pipeline.py @@ -5,7 +5,7 @@ from container_models.scan_image import ScanImage from preprocessors.pipelines import parse_scan_pipeline, x3p_pipeline -from preprocessors.schemas import UploudScanParameters +from preprocessors.schemas import UploadScanParameters @pytest.mark.integration @@ -27,7 +27,7 @@ def test_convert_scan_to_x3p_success(self, parsed_al3d_file: ScanImage, tmp_path # TODO: can we assert this differently? def test_output_file_is_valid_x3p( - self, parsed_al3d_file: ScanImage, tmp_path: Path, default_parameters: UploudScanParameters + self, parsed_al3d_file: ScanImage, tmp_path: Path, default_parameters: UploadScanParameters ) -> None: """Test that the output file can be parsed back as a valid X3P file.""" # Arrange diff --git a/tests/preprocessors/test_router.py b/tests/preprocessors/test_router.py index aca90db0..e749ad6c 100644 --- a/tests/preprocessors/test_router.py +++ b/tests/preprocessors/test_router.py @@ -1,14 +1,14 @@ +from http import HTTPStatus from pathlib import Path import pytest -from _pytest.monkeypatch import MonkeyPatch +from container_models.light_source import LightSource from fastapi.testclient import TestClient -from image_generation.exceptions import ImageGenerationError -from parsers.exceptions import ExportError -from starlette.status import HTTP_200_OK, HTTP_500_INTERNAL_SERVER_ERROR +from PIL import Image +from starlette.status import HTTP_200_OK -from constants import PROJECT_ROOT from preprocessors import ProcessedDataLocation, UploadScan +from preprocessors.schemas import UploadScanParameters def test_pre_processors_placeholder(client: TestClient) -> None: @@ -20,80 +20,121 @@ def test_pre_processors_placeholder(client: TestClient) -> None: assert response.json() == {"message": "Hello from the pre-processors"}, "A placeholder response should be returned" -@pytest.mark.integration -def test_proces_scan(client: TestClient, tmp_path: Path) -> None: - # Arrange - scan_file = PROJECT_ROOT / "packages/scratch-core/tests/resources/scans/circle.x3p" - input_model = UploadScan( - scan_file=scan_file, - output_dir=tmp_path, - ) +@pytest.mark.e2e +class TestProcessScanEndpoint: + """End-to-end tests for the /process-scan endpoint.""" - # Act - response = client.post("/preprocessor/process-scan", json=input_model.model_dump(mode="json")) + def test_process_scan_success_with_al3d_file( + self, client: TestClient, scan_directory: Path, tmp_path: Path + ) -> None: + """Test successful scan processing with AL3D input file.""" + # Arrange + scan_file = scan_directory / "circle.al3d" + request_data = UploadScan(scan_file=scan_file, output_dir=tmp_path) - # Assert - expected_response = ProcessedDataLocation( - preview_image=input_model.output_dir / "preview.png", - surfacemap_image=input_model.output_dir / "surface_map.png", - x3p_image=input_model.output_dir / "scan.x3p", - ) - assert response.status_code == HTTP_200_OK, "endpoint is alive" - response_model = expected_response.model_validate(response.json()) - assert response_model == expected_response - - -@pytest.mark.parametrize( - ("target_path", "error_kind", "expected_status", "expected_detail"), - [ - pytest.param( - "preprocessors.router.save_to_x3p", - ExportError, - HTTP_500_INTERNAL_SERVER_ERROR, - "Failed to save the scan file", - id="save_to_x3p failes", - ), - pytest.param( - "preprocessors.router.get_array_for_display", - ImageGenerationError, - HTTP_500_INTERNAL_SERVER_ERROR, - "Failed to generate preview", - id="Failed to generate preview image", - ), - pytest.param( - "preprocessors.router.compute_3d_image", - ImageGenerationError, - HTTP_500_INTERNAL_SERVER_ERROR, - "Failed to generate surface_map", - id="Failed to generate 3d image", - ), - ], -) -@pytest.mark.integration -def test_process_scan_failures( # noqa - client: TestClient, - tmp_path: Path, - monkeypatch: MonkeyPatch, - target_path: str, - error_kind: type[Exception], - expected_status: int, - expected_detail: str, -) -> None: - # Arrange - def failing_function(*args, **kwargs) -> None: - raise error_kind("Test error") - - monkeypatch.setattr(target_path, failing_function) - - scan_file = PROJECT_ROOT / "packages/scratch-core/tests/resources/scans/Klein_non_replica_mode.al3d" - input_model = UploadScan(scan_file=scan_file, output_dir=tmp_path) + # Act + response = client.post( + "/preprocessor/process-scan", + json=request_data.model_dump(mode="json"), + ) - # Act - response = client.post( - "/preprocessor/process-scan", - json=input_model.model_dump(mode="json"), + # Assert - verify response + assert response.status_code == HTTPStatus.OK + result = ProcessedDataLocation.model_validate(response.json()) + + # Assert - verify image files are valid PNGs + with Image.open(result.preview_image) as img: + assert img.format == "PNG" + + with Image.open(result.surfacemap_image) as img: + assert img.format == "PNG" + + def test_process_scan_output_filenames_match_input( + self, client: TestClient, scan_directory: Path, tmp_path: Path + ) -> None: + """Test that output filenames are derived from input filename.""" + # Arrange + scan_file = scan_directory / "circle.al3d" + request_data = UploadScan(scan_file=scan_file, output_dir=tmp_path) + + # Act + response = client.post( + "/preprocessor/process-scan", + json=request_data.model_dump(mode="json"), + ) + + # Assert + result = ProcessedDataLocation.model_validate(response.json()) + + # Verify filenames contain the input stem "circle" + assert result.x3p_image.name == "circle.x3p" + assert result.preview_image.name == "circle_preview.png" + assert result.surfacemap_image.name == "circle_surfacemap.png" + + def test_process_scan_with_custom_light_sources( + self, client: TestClient, scan_directory: Path, tmp_path: Path + ) -> None: + """Test scan processing with custom light source configuration.""" + # Arrange + scan_file = scan_directory / "circle.al3d" + request_data = UploadScan( + scan_file=scan_file, + output_dir=tmp_path, + parameters=UploadScanParameters( # type: ignore + light_sources=( + LightSource(azimuth=0, elevation=90), + LightSource(azimuth=90, elevation=45), + LightSource(azimuth=180, elevation=45), + LightSource(azimuth=270, elevation=45), + ), + observer=LightSource(azimuth=0, elevation=90), + ), + ) + + # Act + response = client.post( + "/preprocessor/process-scan", + json=request_data.model_dump(mode="json"), + ) + + # Assert + assert response.status_code == HTTPStatus.OK + result = ProcessedDataLocation.model_validate(response.json()) + + # Verify surface map was generated with custom lighting + assert result.surfacemap_image.exists() + with Image.open(result.surfacemap_image) as img: + assert img.format == "PNG" + + @pytest.mark.parametrize( + ("filename", "side_effect"), + [ + pytest.param("nonexistent.x3p", "None", id="nonexistent file"), + pytest.param("unsuported.txt", "write_text is_unsupported_file", id="unsuported file"), + pytest.param("empty.x3p", "touch", id="empty file"), + ], ) + def test_process_scan_bad_file( + self, + filename: str, + side_effect: str, + client: TestClient, + tmp_path: Path, + ) -> None: + """Test that Pydantic validation rejects nonexistent files.""" + # Arrange + path = tmp_path / filename + method, *args = side_effect.strip().split() + if func := getattr(path, method, None): + func(*args) - # Assert - assert response.status_code == expected_status - assert expected_detail in response.json()["detail"] + # Act - send raw JSON to bypass Pydantic model construction + response = client.post( + "/preprocessor/process-scan", + json={"scan_file": str(path), "output_dir": str(tmp_path)}, + ) + + # Assert - Pydantic validation should catch this + assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY + error_detail = response.json()["detail"] + assert any("scan_file" in str(err) for err in error_detail) From ba6e75fef917e5a637720905186ec5ea6e837018 Mon Sep 17 00:00:00 2001 From: "Sharlon N. Regales" Date: Thu, 11 Dec 2025 11:27:31 +0100 Subject: [PATCH 09/16] Cleanup old/unused scratch core code --- .../scratch-core/src/conversion/exceptions.py | 6 - .../scratch-core/src/conversion/subsample.py | 23 -- .../src/image_generation/__init__.py | 3 - .../src/image_generation/data_formats.py | 231 ------------------ .../src/image_generation/exceptions.py | 5 - .../src/image_generation/image_generation.py | 67 ----- .../src/image_generation/translations.py | 222 ----------------- .../scratch-core/src/parsers/data_types.py | 31 --- .../scratch-core/src/parsers/exceptions.py | 5 - .../src/utils/array_definitions.py | 9 - packages/scratch-core/tests/constants.py | 7 - .../test_display/preview_image.png | Bin 205105 -> 0 bytes .../tests/conversion/test_subsample.py | 71 ------ .../surfaceplot_default.png | Bin 228915 -> 0 bytes .../test_calculate_lighting.py | 179 -------------- .../test_compute_surface_normals.py | 179 -------------- .../image_generation/test_convert_image.py | 40 --- .../image_generation/test_data_formats.py | 29 --- .../image_generation/test_image_generation.py | 28 --- .../image_generation/test_multiple_lights.py | 55 ----- .../test_normalize_intensity_map.py | 51 ---- .../image_generation/test_translations.py | 119 --------- .../tests/parsers/test_parsers.py | 68 ------ packages/scratch-core/tests/utils.py | 13 - 24 files changed, 1441 deletions(-) delete mode 100644 packages/scratch-core/src/conversion/exceptions.py delete mode 100644 packages/scratch-core/src/conversion/subsample.py delete mode 100644 packages/scratch-core/src/image_generation/__init__.py delete mode 100644 packages/scratch-core/src/image_generation/data_formats.py delete mode 100644 packages/scratch-core/src/image_generation/exceptions.py delete mode 100644 packages/scratch-core/src/image_generation/image_generation.py delete mode 100644 packages/scratch-core/src/image_generation/translations.py delete mode 100644 packages/scratch-core/src/parsers/data_types.py delete mode 100644 packages/scratch-core/src/parsers/exceptions.py delete mode 100644 packages/scratch-core/src/utils/array_definitions.py delete mode 100644 packages/scratch-core/tests/constants.py delete mode 100644 packages/scratch-core/tests/conversion/baseline_images/test_display/preview_image.png delete mode 100644 packages/scratch-core/tests/conversion/test_subsample.py delete mode 100644 packages/scratch-core/tests/image_generation/baseline_images/test_image_generation/surfaceplot_default.png delete mode 100644 packages/scratch-core/tests/image_generation/test_calculate_lighting.py delete mode 100644 packages/scratch-core/tests/image_generation/test_compute_surface_normals.py delete mode 100644 packages/scratch-core/tests/image_generation/test_convert_image.py delete mode 100644 packages/scratch-core/tests/image_generation/test_data_formats.py delete mode 100644 packages/scratch-core/tests/image_generation/test_image_generation.py delete mode 100644 packages/scratch-core/tests/image_generation/test_multiple_lights.py delete mode 100644 packages/scratch-core/tests/image_generation/test_normalize_intensity_map.py delete mode 100644 packages/scratch-core/tests/image_generation/test_translations.py delete mode 100644 packages/scratch-core/tests/parsers/test_parsers.py delete mode 100644 packages/scratch-core/tests/utils.py diff --git a/packages/scratch-core/src/conversion/exceptions.py b/packages/scratch-core/src/conversion/exceptions.py deleted file mode 100644 index e73e93ea..00000000 --- a/packages/scratch-core/src/conversion/exceptions.py +++ /dev/null @@ -1,6 +0,0 @@ -class NegativeStdScalerException(Exception): - pass - - -class ConversionError(Exception): - pass diff --git a/packages/scratch-core/src/conversion/subsample.py b/packages/scratch-core/src/conversion/subsample.py deleted file mode 100644 index dcdc44c8..00000000 --- a/packages/scratch-core/src/conversion/subsample.py +++ /dev/null @@ -1,23 +0,0 @@ -from utils.array_definitions import ScanMap2DArray - - -def subsample_array( - scan_data: ScanMap2DArray, step_size: tuple[int, int] -) -> ScanMap2DArray: - """ - Subsample the data in a `ScanImage` instance by skipping `step_size` steps. - - :param scan_image: The instance of `ScanImage` containing the 2D image data to subsample. - :param step_size: Denotes the number of steps to skip in each dimension. The first integer - corresponds to the subsampling step size in the X-direction, and the second integer to - the step size in the Y-direction. - - """ - step_x, step_y = step_size - width, height = scan_data.shape - if step_x >= width or step_y >= height: - raise ValueError("Step size should be smaller than the image size") - if step_x <= 0 or step_y <= 0: - raise ValueError("Step size must be a tuple of positive integers") - - return scan_data[::step_y, ::step_x].copy() diff --git a/packages/scratch-core/src/image_generation/__init__.py b/packages/scratch-core/src/image_generation/__init__.py deleted file mode 100644 index c9c4dc85..00000000 --- a/packages/scratch-core/src/image_generation/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .image_generation import compute_3d_image, get_array_for_display - -__all__ = ["compute_3d_image", "get_array_for_display"] diff --git a/packages/scratch-core/src/image_generation/data_formats.py b/packages/scratch-core/src/image_generation/data_formats.py deleted file mode 100644 index a28d9f1e..00000000 --- a/packages/scratch-core/src/image_generation/data_formats.py +++ /dev/null @@ -1,231 +0,0 @@ -import numpy as np -from numpydantic.ndarray import NDArray -from PIL.Image import Image, fromarray -from pydantic import BaseModel, ConfigDict, Field -from loguru import logger -from .exceptions import ImageGenerationError -from conversion.exceptions import ConversionError -from conversion.subsample import subsample_array -from image_generation.translations import ( - apply_multiple_lights, - compute_surface_normals, - grayscale_to_rgba, - normalize_2d_array, -) -from utils.array_definitions import ( - ScanMap2DArray, - ScanTensor3DArray, - ScanVectorField2DArray, - UnitVector3DArray, -) - - -class ImageContainer(BaseModel): - data: NDArray - scale_x: float = Field(..., gt=0.0, description="pixel size in meters (m)") - scale_y: float = Field(..., gt=0.0, description="pixel size in meters (m)") - meta_data: dict | None = None - - -class LightSource(BaseModel): - """ - Representation of a light source using an angular direction (azimuth and elevation) - together with a derived 3D unit direction vector. - - Angle conventions: - - Azimuth: horizontal angle measured in the x–y plane. - 0° corresponds to the –x direction, and the angle increases counter-clockwise - toward the +y direction. - - - Elevation: vertical angle measured relative to the x–y plane. - 0° is horizontal, +90° points straight upward (+z), and –90° points straight - downward (–z). - """ - - model_config = ConfigDict( - frozen=True, - extra="forbid", - ) - azimuth: float = Field( - ..., - description="Horizontal direction angle in degrees. " - "Measured in the x–y plane from the –x axis, increasing counter-clockwise.", - examples=[90, 45, 180], - ge=0, - le=360, - ) - elevation: float = Field( - ..., - description="Vertical angle in degrees measured from the x–y plane. " - "0° is horizontal, +90° is upward (+z), –90° is downward (–z).", - examples=[90, 45, 180], - ge=-90, - le=90, - ) - - @property - def unit_vector(self) -> UnitVector3DArray: - """ - Returns the unit direction vector [x, y, z] corresponding to the azimuth and - elevation angles. The conversion follows a spherical-coordinate convention: - azimuth defines the horizontal direction, and elevation defines the vertical - tilt relative to the x–y plane. - """ - azimuth = np.deg2rad(self.azimuth) - elevation = np.deg2rad(self.elevation) - return np.array( - [ - -np.cos(azimuth) * np.cos(elevation), - np.sin(azimuth) * np.cos(elevation), - np.sin(elevation), - ] - ) - - -class ScanImage(ImageContainer, arbitrary_types_allowed=True): - """ - A 2D image/array of floats. - - Used for: depth maps, intensity maps, single-channel images. - Shape: (height, width) - """ - - data: ScanMap2DArray - - @property - def width(self) -> int: - """The image width in pixels.""" - return self.data.shape[1] - - @property - def height(self) -> int: - """The image height in pixels.""" - return self.data.shape[0] - - def subsample(self, step_x: int, step_y: int) -> "ScanImage": - """Subsample the data in a `ScanMap2D` instance by skipping `step_size` steps.""" - logger.debug(f"Subsampling data with step size ({step_x}, {step_y})") - try: - array = subsample_array(scan_data=self.data, step_size=(step_x, step_y)) - except ValueError as e: - logger.error(f"Error subsampling data: {e}") - raise ImageGenerationError(f"Error subsampling data: {e}") from e - return ScanImage( - data=array, - scale_x=self.scale_x * step_x, - scale_y=self.scale_y * step_y, - ) - - def compute_normals( - self, x_dimension: float, y_dimension: float - ) -> "SurfaceNormals": - """ - Compute per-pixel surface normals from a 2D depth map. - - :param x_dimension: Represents the distance between 2 pixels in meters in x direction. - :param y_dimension: Represents the distance between 2 pixels in meters in x direction. - - :returns: Normal vectors per pixel in a 3-layer field. Layers are [x,y,z] - """ - logger.debug(f"Compute normals with x:{x_dimension}, y:{y_dimension}") - try: - return SurfaceNormals( - data=compute_surface_normals(self.data, x_dimension, y_dimension), - scale_x=self.scale_x, - scale_y=self.scale_y, - ) - except ValueError as e: - logger.error(f"Error computing surface normals: {e}") - raise ImageGenerationError(f"Error computing surface normals: {e}") from e - - def normalize(self, scale_max: float = 255, scale_min: float = 25) -> "ScanImage": - """ - Normalize a 2D intensity map to a specified output range. - - :param scale_max: Maximum output intensity value. Default is ``255``. - :param scale_min: Minimum output intensity value. Default is ``25``. - - :returns: Normalized 2D intensity map with values in ``[scale_min, max_val]``. - """ - logger.debug( - f"Normalizing scan image array with min:{scale_min}; max:{scale_max}" - ) - return ScanImage( - data=normalize_2d_array( - self.data, scale_max=scale_max, scale_min=scale_min - ), - scale_x=self.scale_x, - scale_y=self.scale_y, - ) - - def image(self) -> Image: - """ - Convert a 2D intensity map to an image. - - :returns: Image representation of the 2D intensity map. - """ - logger.debug("creating Image from ScanImage array.") - try: - return fromarray(grayscale_to_rgba(scan_data=self.data)) - except ValueError as err: - logger.error(f"Could not convert data to an RGBA image.: err{str(err)}") - raise ConversionError("Could not convert data to an RGBA image.") from err - - -class MultiIlluminationScan(ImageContainer, arbitrary_types_allowed=True): - """ - Multiple 2D scans captured under different illumination conditions. - - Shape: (height, width, n_lights) where the last axis represents - different lighting directions applied to the same surface. - """ - - data: ScanTensor3DArray - - def reduce_stack(self, merge_on_axis: int = 2) -> ScanImage: - """Combine stacked 2d scan maps → (Height × Width).""" - logger.debug(f"Flatten the multi 2d scan image stack on axis:{merge_on_axis}") - return ScanImage( - data=np.nansum(self.data, axis=merge_on_axis), - scale_x=self.scale_x, - scale_y=self.scale_y, - ) - - -class SurfaceNormals(ImageContainer, arbitrary_types_allowed=True): - """Normal vectors per pixel in a 3-layer field. - - Represents a surface-normal map with components (nx, ny, nz) stored in the - last dimension. Shape: (height, width, 3).""" - - data: ScanVectorField2DArray - - def apply_lights( - self, - light_vectors: tuple[UnitVector3DArray, ...], - observer: UnitVector3DArray = LightSource(azimuth=0, elevation=90).unit_vector, - ) -> "MultiIlluminationScan": - """ - Apply one or more light vectors to the surface-normal field. - - Computes intensity values for each light direction (and an optional observer - direction) and returns a stacked result as an Image3DArray. - - :param light_vectors: LightSource objects defining azimuth and elevation as a unit vector. - :param observer: LightSource object defining azimuth and elevation as a unit vector as the observer. - Defaults to azimuth=0, elevation=90 - - :returns: Normalized 2D intensity map with shape (Height, Width), suitable for - """ - logger.debug( - f"Add n:{light_vectors.count} lights to the scan_image array, with observer vector:{observer}" - ) - return MultiIlluminationScan( - data=apply_multiple_lights( - surface_normals=self.data, - light_vectors=light_vectors, - observer_vector=observer, - ), - scale_x=self.scale_x, - scale_y=self.scale_y, - ) diff --git a/packages/scratch-core/src/image_generation/exceptions.py b/packages/scratch-core/src/image_generation/exceptions.py deleted file mode 100644 index ab1e854e..00000000 --- a/packages/scratch-core/src/image_generation/exceptions.py +++ /dev/null @@ -1,5 +0,0 @@ -class ImageGenerationError(Exception): - """Raised when an error occurs during image generation.""" - - def __init__(self, message: str): - super().__init__(message) diff --git a/packages/scratch-core/src/image_generation/image_generation.py b/packages/scratch-core/src/image_generation/image_generation.py deleted file mode 100644 index 03ef9e7d..00000000 --- a/packages/scratch-core/src/image_generation/image_generation.py +++ /dev/null @@ -1,67 +0,0 @@ -from typing import ParamSpec, Protocol - -from image_generation.translations import clip_data, normalize -from image_generation.data_formats import LightSource, ScanImage, UnitVector3DArray - -P = ParamSpec("P") - - -class ImageGenerator(Protocol[P]): - def __call__( - self, - scan_image: ScanImage, - *args: P.args, - **kwargs: P.kwargs, - ) -> ScanImage: ... - - -def compute_3d_image( - scan_image: ScanImage, - *, - light_sources: tuple[UnitVector3DArray, ...] = ( - LightSource(azimuth=90, elevation=45).unit_vector, - LightSource(azimuth=180, elevation=45).unit_vector, - ), -) -> ScanImage: - """ - Render a 3D image from 2D scan data using directional lighting. - - This function performs the complete processing pipeline: - - 1. Compute per-pixel surface normals from the depth map. - 2. Apply multiple directional lights and sum the intensities to obtain the per-pixel intensities. - 3. Normalize and scale the computed pixel intensities to a specified output range. - - :param scan_image: ScanImage, the data array with shape (Height, Width). - :param light_sources: Tuple of LightSource objects defining azimuth and elevation as a unit vector. If omitted, - two default lights are used: (azimuth=90°, elevation=45°) - and (azimuth=180°, elevation=45°). - - :returns: ScanImage with the data rendered as a 3D image with the shape (Height, Width), - """ - return ( - scan_image.compute_normals(scan_image.scale_x, scan_image.scale_y) - .apply_lights(light_sources) - .reduce_stack() - .normalize() - ) - - -def get_array_for_display( - scan_image: ScanImage, *, std_scaler: float = 2.0 -) -> ScanImage: - """ - Clip and normalize image data for displaying purposes. - - First the data will be clipped so that the values lie in the interval [μ - σ * S, μ + σ * S]. - Then the values are min-max normalized and scaled to the [0, 255] interval. - - :param scan_image: An instance of `ScanImage`. - :param std_scaler: The multiplier `S` for the standard deviation used above when clipping the image. - :returns: ScanImage with the data rendered as a 3D image with the shape (Height, Width), - """ - clipped, lower, upper = clip_data(data=scan_image.data, std_scaler=std_scaler) - normalized = normalize(clipped, lower, upper) - return ScanImage( - data=normalized, scale_x=scan_image.scale_x, scale_y=scan_image.scale_y - ) diff --git a/packages/scratch-core/src/image_generation/translations.py b/packages/scratch-core/src/image_generation/translations.py deleted file mode 100644 index 707a0037..00000000 --- a/packages/scratch-core/src/image_generation/translations.py +++ /dev/null @@ -1,222 +0,0 @@ -from typing import Protocol - -import numpy as np - -from conversion.exceptions import NegativeStdScalerException -from utils.array_definitions import ( - ScanMap2DArray, - ScanTensor3DArray, - ScanVectorField2DArray, - UnitVector3DArray, - ScanMapRGBA, -) - - -def compute_surface_normals( - depth_data: ScanMap2DArray, - x_dimension: float, - y_dimension: float, -) -> ScanVectorField2DArray: - """ - Compute per-pixel surface normals from a 2D depth map. - - The gradients in both x and y directions are estimated using central differences, - and the resulting normal vectors are normalized per pixel. - The border are padded with NaN values to keep the same size as the input data. - - :param depth_data: 2D array of depth values with shape (Height, Width). - :param x_dimension: Physical spacing between columns (Δx) in meters. - :param y_dimension: Physical spacing between rows (Δy) in meters. - - :returns: 3D array of surface normals with shape (Height, Width, 3), where the - last dimension corresponds to (nx, ny, nz). - """ - factor_x = 1 / (2 * x_dimension) - factor_y = 1 / (2 * y_dimension) - - hx = (depth_data[:, :-2] - depth_data[:, 2:]) * factor_x - hy = (depth_data[:-2, :] - depth_data[2:, :]) * factor_y - hx = np.pad(hx, ((0, 0), (1, 1)), mode="constant", constant_values=np.nan) - hy = np.pad(hy, ((1, 1), (0, 0)), mode="constant", constant_values=np.nan) - norm = np.sqrt(hx * hx + hy * hy + 1) - - nx = hx / norm - ny = -hy / norm - nz = 1 / norm - return np.stack([nx, ny, nz], axis=-1) - - -def calculate_lighting( - light_vector: UnitVector3DArray, - observer_vector: UnitVector3DArray, - surface_normals: ScanVectorField2DArray, - specular_factor: float = 1.0, - phong_exponent: int = 4, -) -> ScanMap2DArray: - """ - Compute per-pixel lighting intensity from a light source and surface normals. - - Lighting is computed using Lambertian diffuse reflection combined with a - Phong specular component. - - :param light_vector: Normalized 3-element vector pointing toward the light source. - :param observer_vector: Normalized 3-element vector pointing toward the observer/camera. - :param surface_normals: 3D array of surface normals with shape (Height, Width, 3). - :param specular_factor: Weight of the specular component. Default is ``1.0``. - :param phong_exponent: Exponent controlling the sharpness of specular highlights. - Default is ``4``. - - :returns: 2D array of combined lighting intensities in ``[0, 1]`` with shape - (Height, Width). - """ - h_vec = light_vector + observer_vector - h_norm = np.linalg.norm(h_vec) - h_vec /= h_norm - - nx, ny, nz = ( - surface_normals[..., 0], - surface_normals[..., 1], - surface_normals[..., 2], - ) - - diffuse = np.maximum( - light_vector[0] * nx + light_vector[1] * ny + light_vector[2] * nz, 0 - ) - - specular = np.maximum(h_vec[0] * nx + h_vec[1] * ny + h_vec[2] * nz, 0) - specular = np.clip(specular, -1.0, 1.0) - specular = np.maximum(np.cos(2 * np.arccos(specular)), 0) ** phong_exponent - - return (diffuse + specular_factor * specular) / (1 + specular_factor) - - -class LightingCalculator(Protocol): - def __call__( - self, - light_vector: UnitVector3DArray, - observer_vector: UnitVector3DArray, - surface_normals: ScanVectorField2DArray, - specular_factor: float = 1.0, - phong_exponent: int = 4, - ) -> ScanMap2DArray: ... - - -def apply_multiple_lights( - surface_normals: ScanVectorField2DArray, - light_vectors: tuple[UnitVector3DArray, ...], - observer_vector: UnitVector3DArray, - lighting_calculator: LightingCalculator = calculate_lighting, -) -> ScanTensor3DArray: - """ - Apply multiple directional light sources to a surface and stack the - resulting intensity maps. - - :param surface_normals: 3D array of surface normals with shape (Height, Width, 3). - :param light_vectors: Tuple of normalized 3-element light direction vectors. - :param observer_vector: Normalized 3-element vector pointing toward the observer. - :param lighting_calculator: Function used to compute lighting for a single light - source. Default is :func:`calculate_lighting`. - - :returns: 3D array of lighting intensities with shape (Height, Width, N), where - N is the number of lights. - """ - return np.stack( - [ - lighting_calculator(light, observer_vector, surface_normals) - for light in light_vectors - ], - axis=-1, - ) - - -def normalize_2d_array( - image_to_normalize: ScanMap2DArray, - scale_max: float = 255, - scale_min: float = 25, -) -> ScanMap2DArray: - """ - Normalize a 2D intensity map to a specified output range. - - The normalization is done by the steps: - 1. apply min-max normalization to grayscale data - 2. stretch / scale the normalized data from the unit range to a specified output range - - :param image_to_normalize: 2D array of input intensity values. - :param scale_max: Maximum output intensity value. Default is ``255``. - :param scale_min: Minimum output intensity value. Default is ``25``. - - :returns: Normalized 2D intensity map with values in ``[scale_min, max_val]``. - """ - imin = np.nanmin(image_to_normalize) - imax = np.nanmax(image_to_normalize) - norm = (image_to_normalize - imin) / (imax - imin) - return scale_min + (scale_max - scale_min) * norm - - -def _validate_array_is_normalized( - scan_data: ScanMap2DArray, min_value: int = 0, max_value: int = 255 -) -> None: - """Validate that the input array is normalized. - - :param scan_data: 2D array of input intensity values. - :param min_value: Minimum intensity value. Default is ``0``. - :param max_value: Maximum intensity value. Default is ``255``. - :raises ValueError: If any value is outside ``min_value`` and ``max_value`` - """ - valid_mask = ~np.isnan(scan_data) - if valid_mask.any(): - valid_data = scan_data[valid_mask] - if np.any((valid_data < min_value) | (valid_data > max_value)): - raise ValueError( - f"scan_data contains values outside [{min_value}:{max_value}] range. " - f"Found min={np.nanmin(scan_data)}, max={np.nanmax(scan_data)}" - ) - - -def grayscale_to_rgba(scan_data: ScanMap2DArray) -> ScanMapRGBA: - """ - Convert a 2D grayscale array to an 8-bit RGBA array. - - NaN values will be converted to black pixels with 100% transparency. - - :param image: The grayscale image data to be converted to an 8-bit RGBA image. - :returns: Array with the image data in 8-bit RGBA format. - """ - _validate_array_is_normalized(scan_data, min_value=0, max_value=255) - rgba = np.empty(shape=(*scan_data.shape, 4), dtype=np.uint8) - rgba[..., :-1] = np.expand_dims(scan_data.astype(np.uint8), axis=-1) - rgba[..., -1] = ~np.isnan(scan_data) * 255 - return rgba - - -def normalize( - input_array: ScanMap2DArray, lower: float, upper: float -) -> ScanMap2DArray: - """Perform min-max normalization on the input array and scale to the [0, 255] interval.""" - if lower >= upper: - raise ValueError( - f"The lower bound ({lower}) should be smaller than the upper bound ({upper})." - ) - return (input_array - lower) / (upper - lower) * 255.0 - - -def clip_data( - data: ScanMap2DArray, std_scaler: float -) -> tuple[ScanMap2DArray, float, float]: - """ - Clip the data so that the values lie in the interval [μ - σ * S, μ + σ * S]. - - Here the standard deviation σ is normalized by N-1. Note: NaN values are ignored and unaffected. - - :param data: The data to be clipped. - :param std_scaler: The multiplier for the standard deviation of the data to be clipped. - :returns: A tuple containing the clipped data, the lower bound, and the upper bound of the clipped data. - """ - if std_scaler <= 0.0: - raise NegativeStdScalerException("`std_scaler` must be a positive number.") - mean = np.nanmean(data) - std = np.nanstd(data, ddof=1) * std_scaler - upper = float(mean + std) - lower = float(mean - std) - clipped = np.clip(data, lower, upper) - return clipped, lower, upper diff --git a/packages/scratch-core/src/parsers/data_types.py b/packages/scratch-core/src/parsers/data_types.py deleted file mode 100644 index 29f0d6f8..00000000 --- a/packages/scratch-core/src/parsers/data_types.py +++ /dev/null @@ -1,31 +0,0 @@ -from pathlib import Path - -import numpy as np -from surfalize import Surface -from surfalize.file import FileHandler -from surfalize.file.al3d import MAGIC - -from image_generation.data_formats import ScanImage - -from .patches.al3d import read_al3d - -UNIT_CONVERSION_FACTOR = 1e-6 # conversion factor from micrometers (um) to meters (m) - -# register the patched method as a parser -FileHandler.register_reader(suffix=".al3d", magic=MAGIC)(read_al3d) - - -def load_scan_image(scan_file: Path) -> ScanImage: - """ - Load a scan image from a file. Parsed values will be converted to meters (m). - - :param scan_file: The path to the file containing the scanned image data. - :returns: An instance of `ScanImage`. - """ - surface = Surface.load(scan_file) - return ScanImage( - data=np.asarray(surface.data, dtype=np.float64) * UNIT_CONVERSION_FACTOR, - scale_x=surface.step_x * UNIT_CONVERSION_FACTOR, - scale_y=surface.step_y * UNIT_CONVERSION_FACTOR, - meta_data=surface.metadata, - ) diff --git a/packages/scratch-core/src/parsers/exceptions.py b/packages/scratch-core/src/parsers/exceptions.py deleted file mode 100644 index 3e6318b8..00000000 --- a/packages/scratch-core/src/parsers/exceptions.py +++ /dev/null @@ -1,5 +0,0 @@ -class ExportError(Exception): - """Raised when an error occurs during export.""" - - def __init__(self, message: str): - super().__init__(message) diff --git a/packages/scratch-core/src/utils/array_definitions.py b/packages/scratch-core/src/utils/array_definitions.py deleted file mode 100644 index f5dfaced..00000000 --- a/packages/scratch-core/src/utils/array_definitions.py +++ /dev/null @@ -1,9 +0,0 @@ -from numpy import bool_, float64, uint8 -from numpy.typing import NDArray - -ScanMap2DArray = NDArray[float64] -ScanMapRGBA = NDArray[uint8] -ScanTensor3DArray = NDArray[float64] -ScanVectorField2DArray = NDArray[float64] -UnitVector3DArray = NDArray[float64] -MaskArray = NDArray[bool_] diff --git a/packages/scratch-core/tests/constants.py b/packages/scratch-core/tests/constants.py deleted file mode 100644 index c3af85e8..00000000 --- a/packages/scratch-core/tests/constants.py +++ /dev/null @@ -1,7 +0,0 @@ -from pathlib import Path - -TEST_ROOT = Path(__file__).parent -SCANS_DIR = TEST_ROOT / "resources" / "scans" -BASELINE_IMAGES_DIR = TEST_ROOT / "resources" / "baseline_images" - -PRECISION = 1e-16 diff --git a/packages/scratch-core/tests/conversion/baseline_images/test_display/preview_image.png b/packages/scratch-core/tests/conversion/baseline_images/test_display/preview_image.png deleted file mode 100644 index 27beae1ed65e5e1217879602b3ce2ba04ddc1ba8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 205105 zcmeEuhd-8m`~PLnjFPCt6%`o?Ey|vutVEHSj1)ph$ShkLh|G+ZkrXL=rD3ZmiU!Im zMb_{A>3N>tU-7+P_w3d+&hv8|$9o;;C4&Q6D_GaEQYe%aI@;<+6bhXXg+gn_%!pq} z@eX#wKjb|%%sh=xJ9wV8@iFIVx ze%m(J|Nj$Gr#((=(<>G>!K<*iX`7#+P&jSKUo`njc}^4>3Pnd<#l$D|`=XcUp~HU~ z;}l)eA3s@XTkjV9{)&7V3ANh_kDlj&h*+( zBu)kl3<&)B68q@IYrYey56-*l_B_aCd3vmnsZN^x-=BKhGZvPg(ER5=*z5ciizqgH z;s5^3ePc~i`@bJ6uAvS6@Bc`$4PjvZ-|uf=WaIkJkB8etuHY5=-wW}qlTiNe>x7H# zGz#!T2{J%H&@3s5COZ0!I=>H0hz3%_&;Q#92zjYuzw|th4FMMu!pDOe2-MfYM z?3r5@pBFz`CNVxxRhaz9eMpmi{cf&9w@qqh#(IlSyxy=kV1aKSU|y0^{qEh`o}M0` zKjQ+F4EvVL7h_|kN+(9U?+iV^DozP1tXf+8^UF)M)@zbeua%aC#Kmaj7?7_TF zs*;jm;Nr}hl%1}u#>U3OU%oi^O#S)Ol&gQE$hB|hud!avqh%*m)Ya)r{V+)9KTvE6>mKsxnjB?kc!{QlFijweElHZdPK?vRN-l zd5jX2C0bNcqUzwVnNnR-L!+Xi@;G2ozP7g3+QA|G+@Eo$-zzA$bgygTv+lnx6*CaH zEI{Gn;i1C$#7U^%a$!7n>N+7w{z?0=*R_5e0ch;=ia(~#*Iy9y}UI1{r8Uc z+>63hbGjB09==ggal^zwRi1Cm+8wG990JmQzZ_py_&q)LU;wx16V5{Lojc;Gsj2bt z@yda8*Q6{X-G>^fa&kO)U9}@e)|NbYuwy=9;$cJtlV#td(*vr7y=u7a+qQ91+RD$I z`2POM_1GWZzFq91!71qWXWY`yAxi#h`I)b_HNgzp2M+XDFj10ryKh`CATafE*MPy%u0LCL z@8&yrb8qJ2%cSje4^F&hq^#fVM)4Z$67#eOpR)>=6k`9$@Al;d|4PBDTMuhXHz@@npFVdCLbMb5J3JMBu-o0}!}jdwaX6WrZ9iLnm=_QE4eX#pm2PI!a|_rS$0!dnwg8f<<0qJ*q$MT9nJ+%5bk- z85|l)ua_iEvvcQ8jbn6Fdd--%x01IsM8w5$K73nWpA$bzQ5JZFYn^rC^-YZp!ZI=| z$+!RV@`1x!bYnT`MSr8z-(?Gz1V-OiIj_{Y~*X?1fyzfS!cF^b`S$M=c#CU|~v2@5as*_|K+LmQ_Nb)t?%o3?Dil z5{>*g@amf6QO74&uJBk?1qiI#a-e>wDNe^C(^TVYNgsdC^XBc__9>bWzE!G&U(*A_ zvqZIZbweIK+7-Kg53RVkIEAs;xa_1k3dClx`ofzBCpb~zOq6eZkGA#q_qRPef6mOz ztStZsGm3m0<_kGFBL1_JG?7_t#ZSC{M4ql%G(Gcl)AUorZ9k`{J1%k_Jb2K|+`N4{ zRzP7RY7*trHsVQPpU^>ZS=-uXy|pqkaDor7xA!?1w%|sNYnjz0O zPkCl)(aEGUvrAwEZ zU%$>EeCJLPp(oGLZAHb;RBcI;G=rV(Ol)#$ zD)UStxig))`gOxCi8Pd!mX?&y(|Z=b+&C>1nsN5=$+soeNrA$urzJZ1!{>UZ3b;-8 z>_1X$tAXZtW7p{b55bNS9A8youOJ)x?c(BWf5zX}eSiC4M_TsZnX#c4$+C`Z88pY9 z`LnpVxa8crMculU70vnLrAwJ!f%89hd5(6CBsM)NDhkTZ7XI+;{GDVgOp30%=Bt#J zX0sihB`5PUu2>sN4!&JeY|*1fc?n-LGKA_I8#ki+q4+iX%3P!N=9YG_^UKhW_1w#z z*s#xUu-7WEq4oN35&!#M>x*x1;TQc`eHuJ$aCbv*sp&nV}w zdAS$>moORE_)7A930w_w{gYqFvIsrX{p%|MN;QB@u}vLgfBBh+`mmJANw*jwmK7@+ zP?XDehPn%_1c4ybmX{Vuce8lneXn_D?XFYNm{}H2{Z@D1K2(Pu=#+fx)~4@2esHf_ z7lr26!TcMI*s<$vXXmQOtPITmFilsQ3;>SZK9fQe*RQv65r%E)D3%qTHBZh?NW1h% z#OQV|H{f1R)^RGel@=Cm!`Y=!@(!jq;zSnv{&b*JCEwtL^VSkjBz1q4aNzQ2{uaw0_l!{|$1R#Zs ztZc&7t6?R_TR-7IPfkzQVm3Qjt;D&maA?d@le7j3*x$KovtBj&pbouGkrC*VV+&(%u~4r5!>@ppWDoE(_k18MtBYYBdn(rK&G2mgqAHP;mPMtcHByB^B&z?2nuY2L! z_;@TD!)|%`jhi-wec&!X)f6jG9Jn0tvN9_Gh$4BZCpkH}=J|70j1gu|&Zd}kyR1*2 zK6u~IC@5b;&Btf=;=UUSXQhr+1;iY>V~XZNbMxj+pI_g&{r&y91OzPY6^4~Brj|W; z(5ctg*O&P4$cL{$>oJx^Nr3|2gBzsNS-FSaFfF%>f;HzbYZ9zj_vh0brs=z>sP`a~|SiWI4WBKY-c`sIBtyp!O zFUNBuJ%fN}Z{C=pQCyy-unQERf6!6TvZHWJ5@$(QeDU(7lcktqV>G|or@B|KI9{hK z<^2|v_he>aVcC7=%Zn`yT8~b>k4#;j{;az&_ceWZvxq_ma6r<&b2~3_Qie4*S>9h- z_<68mvyngF{I@EfDcY~E)3q>W-M+usUg?HMaV&W8OIr6H^(EH zIyZaVV?}r9xTU+JR&4ifvsEXIZkvdCw7;wLl{GgR-m-Bc?dzNSgwO7y(uZ>hL;@3Z z$}cS~HNSclUR9-3ZY^o?m8>h)p|C}2)_U@w6a|kL% zr>3rd{Nza;XxOV)uRII8q|QEc=?w=;0ZrTCI?{b-O(>VPwstkjf=%-D`-&aqCMesA zHra%vq>BJ42{Y=iQ zJFB zB5l{m&N}_;_wPouWzuhO#NOp$>RgjB4@Uj1pvaAH)x9QBgV{|{9=cg_cH$VuF)f8m z*Xp)Cg-0Jrq1k8!OlsKJ*j%uD;{6eDi{P(UX$1vh-rwI`aCn)LiK(BDpLAWt&8;%E zjUfH1fW-_^Rsw+*7W}VBn6H#PQrdPhTa*>YFgS6`L0;!N%+>aiq5i5s z!SeEQ^7c2C0uOxk81LW1Or8AwAm13 zMK*PEuY*xIm)7kmJoUb!xwSQW1S8q`{;lsBt)F+e^wa}36zrScb43u;dI7UrPV!zh zMm}CI#jD#@9C`!}vaz;L^L+XFd2c@}fG|cSC8fxS?I?x=d3EjCTE(~-@1*wbwGd_$ zR0`1h^Bf39l~1ArpSZEBz}OuK|DEZFCxqzs+%+#144->cBV`5@Gg%O5i}78H*L0is zuusD3*?Q1yElo|vyZ7#q-!{FPDaa~dj^U}_4A^wO0nOR*eig8M3{=A>-mVsp-99Rn z*#NzNd-qUWe|~m01KI;K2Z!dsv-52CCPqdsfwkLWnDP7qQgPbrKg$JJ)O2~1uH2Ze zk_WyrP!<#VunKtN@@to$^AlCs=jVURx!T$WZ+kXNi@K}fR9Xmf>dakCpO4SC%;~~i zF<79b{2J|MdwT9qBa{s;e*Q}!r3W1*jIv`F^R22BL9wHNs+JGCCZ;J@3(24;Vle$Z zE*QqyFHbcIPR`6|=;^VW79NX95Y_^lBk#q;u|8w5+10uGwnlc}>a8Zq?<>68Gu4

Aq6~&sGfDSzL;VS>cB^LRtgb-^Gg;+bY+UoS6sjQFWDBdVlBeHZ-@)iU zCW`EdSM}gdJDRQ+bFG{^P|%xT%6chFH2-knh6Cw}`tBm>s*UG$aPM}sY*||6_jCim zr`rDgtiYX9AM2Q6`tsb*HaveGN!xn-OFjyiM*4C5hbL#_K8QU%#wjZ+ONhk$p9w9< z7DV#e>-UQ}Ha6B%|Ciss)U-6Qq3g#An|2NOhv3TB)zyXAs@b!*fbLnNckMd;q0Qe* zq3cJ_*vxRsW?7b=+lQi-yl?j=_T+$%eF9<9*vw{=w0!&xZLM@V4dz$y)2HV%=Mp#T z*WxxCfSZ$fi}uj;tv$PimD&)In7HavT;yu;PoP*u#l;t(B~b=z!`OyjVWJjn?91R4 zq(E5;!g)%V%{RIg=}`XNq(JsSOYCJ3M8J-ZA3v(J+H<+s+HM3^CW?~h_cw+Q4sP4F z?d31voE@L&DAUu^&5_fCHFPPUnD;imW}~Eqcs{s1exl^(8cbbCT{dkQDin6x`@TQF z&b0?kq~?rMT_qd z@&YNlP9G4i0?5+BSDqa07H6h5Z$)LNsGgF-A7;<|_mJ)lhFH_m!h(|!f)M~brk3WS zVMvfp|I>48BC~|{?p+T(q84=iiMgmX!Z%$u7$I#+N^BI19Hux%FMoQCp(aZ7WBr3P{O!?LAYuMy=R~{KEG>! z@_l^OzP)>k?3y@Y#FG{R0DH)la<)6k&s`hO{T(>u&C_#_9L_N@F{x{4B#b5Dl1oms zWzs#|g)tNS*3Qn3n~%?|xq}*74N@9sdhdvn#S_T0%xBJ=N!fCbA58(X9#?ed{;OLV z&?#d0%Fj-02o4Ep+3|X|v&cq`ndZW4=Rj9-tphp`XfovS6q$0%N}u93*<4Xps1E9X8h5+O)-9|fE^j&wpzhY`bTdE` z8VcSl*H_&1Zbq~e&KX07{yK53>CTRhNk~G8gYoxGOVnJW-I)}Hh1FKg`T?+B5kDMj zD{}Yl-NDbF!@#dP zFUu!TL|v{!_P-^(IC`g0+P0n21OO|sA{QPuH?ci_kg<1E# zflI0N75>+9blb+?KPeF2d*+KCJPmF_b9QeiF9dhMkE}oODphQF>!#3ZP^$kh`1M?b zKRVv5e1_AQf8^oTXLDcH$(`y?`mhQmvXTzGPzB)1@Ts4dy1IG>v|n(9p7q8RNjirm z=lXFLIB?+V?^`|*3T>-@tlb)R(h9hB-=lV7uqhV zQ42c*Nc9q0YGUiLa*xk$_zDC;s`~4#k{r?;*2?{JC@zvdfC^vuF$F~JUv|yI$GN;-8Jj?c+eA}5%t^- z`gd@=wZ1-7Mz7^n^phtFtgNi@SFX?l!TtLFb`>Q1dW@N3=kBee^{iAf!N7?~35-AT zvp)|+#+6kRb){eH(#lP31;$$-$zT3`JuU5?P97A?sM1n7a?OCy2&su-r(^uMPCx*p zhLq;$Xyl)XfyA%_?~l#wqC{`lw-)b~vxqKP1IG!-09fiyW7xsmm8Mo_#Y|mKo{UdV zj{|sga&Cu*uyNO}!$Ujs6rcIs!~nn)OSpM67UV15XW{Qy;S}0)d z%|l&>!UV9FsCun1*41*K)Ets8Xa9zRa>ZtUBozWZf8 zAYepDA$R(rb2qpsbk>B#L^>+_xb7z1YxLI5deEsPnW_1%Xl|YZf-rK>)F6;p*U*B| zC%xMeN@dhq0=(2j&oXCtwlJ*=L9Pb;TRx2g?Vd+Sk}k{A(z0dxxUDTM?A0S$6)#6- z{0L_&>@}`9<|IjoKq!~b1%;_Q1)50DgyXxPBdYZcmv$Dp|HXJH+`sb@ke{ zkw8;^;R;ZsTdh+e% z+-~0Z?RJesi5?Elxt~tM7aHoxAX{dVs&Mu|;>%IMZ$P%+%f9<4Auc@-o(&&3=%d@{ zJb2O3aaq%M6Xrn^w1hoc^yF|sL$KfQk$v~Z`zhhvAHimPe0`nd-H|NV zk{(bUzt+6s4zo=&?IuZeG*rGF&OE5$TYJJ6To>+@$>(d0pFe-T9sb%9); zV(eY#rPbxLjH)8nTTd^#@zX|b)oKic$gDx!Kl?Nq+tgDPgR*<_m6$1HbN;( zxN=4P>utpoGf#dGcpi%GO$a*}bNxnAQv8$DRBsOtRSS#t&#!MgIHlXDL3jMUAir7R zF{wb#wQ|g;%;62Uk2OK%4q;qT3m$~lU^TqG%k`@14RH1_Nud{29A!b$FHQup=LVn( zwjR12ia|k9&Wyc$#9!iQ*+1)runB=JF@hf@$~k#?#?ntJ!Wpea<-CWRxhdg)`DE&@rlkqo>xBDG5j8O>M#4ZuF=syN z-J&!4uH%~`>cYZ85fts1+1>uL(wE}!mTPV+N;X6s$Wwy@WB@gRHFXz9Gbbx+ElxcR z1!~Q3hj2p^1ml*DEK%0Y`YAbYpJ56R4dsPGWERT24g*}Xd$ooN+~_DC;f|{%h)QvSd=t;N_YD=SMmW6C6}uzU9)jEyVPyI@4# zJMvJ~)RedTuownQ%w;A9hD?kPB67kh5zsB{B|hTp_>&2@`QNVk@#k=#n`ML*dPaW( zfY#u~gAGT(-T>PTCgv|&0Hw~XrYW8>zU!^!=UBFZK(l< zqw#+sa;U&)6D7PfB`C9#EvTdYd5}}J^z_*G?|1nZ@8N18)O65GIcJtO2z}e-)eZ%P zb^jQmIZpAdS07V$xU`kI9z9|T;B?z40GyK`Ed!pg$Q{p>tw8h$iU69*R`z|np?Ywo z!>LoT7?DDFnYVA>Y9Bn$8i ziRXEA&JwJOYw!4=q@m#KAgjV=%F{d2;a7U*>w%a%Mu-NmFag^Vy$;^+@N=li5|WZ) zl2bJyObIt`5X?*5zzrKV;0(@Aw{9!=pn(VvIC5L2n$usLWa4;z`t&KIb3`=zGBnX~ zWGRRkhNcV?5H)x8%ey;l(_)dqxZi`&MQ8C@byC+xQ4b$J?0gVP1V9QM-00_zH!;Gcy;W#6tj{RgghROKTS zrO8*#A0BV+TPk=wz5G=?l)9v2$YXNXETMlEtRCLr@oJ-kN`AkzSMe-E6vJFJqSoMI zLL{W2V|08~h0AI7=#&8Q+uPaIRE!ort22HBmy1LHV1$G=(`?V zc(l}!rrN;%G_=>eF@O)nHaBY{D`g97B<0N2VhME45ZntSr=$ay{838b z65C0zA&7>~z`!7*d}okKH8f-<2;EnnmFAaLpglhJpOb-}q3>$i`PkhKosTH?P21 zmH6V@s`cxmxrH_3SF3oJAPo`$K5i6oVEk}w5*w8`D$F+dOdB;qqQwb*+I;Y4H0r{L z>j|PfuzF&_K6xS=xN5Dr4F$ykfxNxFNyrZJaQxM)ha5eREh)l5hrg-idKBIdCL6Ij z?BF1zf8X0WKs*AljBDGhV$tSNot@ebxx&r3-|o3yyAeY_L3pd+w-8OFD&W*`K9Nkc^$LIb? zvG72~tqfe=VAmL(>BITZ3mBdkxYU40!=T9~0D{^52_Gz3uh*!fD09L?t-m2X5|17IbN99(qOWE+TM^yoOm zSp}B}2??QF%}b};H21sjbj!?0df-wGX1DTb&O>8>DAo zaI(-L76j|+tzcVQOqm>b-NA6aFHJ{+J8re7h7Ex2>dZFOn^yky+e| z@DkC;`Sy5-ki3xO(Xux50Ex1bZ&hjLJ8Cme^})bve*YXgegBE!nQLkK<@!(|z_G~C z&;Et~+Y>!_&eyl5sYy%rc_A+XB=N7SP#zo`_M)kCBI@Gu8}ch1#5jy%c%2`?36X~p zanV8~&C7Pz$u|^S_q$eA8z53e;JV*ZHf5_RDv~Ze^?ofmPv_@%mXwr`@C|fVpz)g4 z)+pOEs6$3wU7VI+RF#Y7_thFTFqVk&64QM7(C}yG%De1RcM`^C{9f*$$(Rd(EQ_J8 zmZfFLvw%qlve57jl43~p) z527xbo125UXa~;!5lHKVDpYjpy}ZblEq0(eBD>Sl(jbu>xbJS{15KF);baKBM9bH` z^jALl#*M_jj~Jt2=!-w@ zvPm4N+8`BcBGcmwTV{Hjo15FN-WlG0JxIF?oF*iMVI zf$*d4Ly^-S^l!Vmc%amflmjMtTdw}PUlRj@(5!H-SqCtZ*`S<;K^l)0REkA%EmB^G zbHmod8rJCMNmz{yK&!r7*U$6!ct~M6jlG|^S;YV`=B62VD<`0OLL($s{@Wd z^uAX=rtHv&%LG3)f-6{p zvm;nY3Cu6|QoWKvNl2`MR&pzd~)gEb@?jtURKe-7a~hm~DTE-6El2Tm-2 z%Xnh>(mE(!C~X&KW^E1u`A{@G8!)stRe?(Q*BQY+|H(lWc931<(99}*wqw*CvcHxF zIV20&jC`}=jqp}29hGohh*U$I{c#KD|7O#W_RNAOLr?;~e$>fMz^kdwgG#H-Pa5WR z)*x92C5DzlLU=GZGBW0agK3}J=4cx75dGl`03#C_J6w#rc#sp*Z_LN#u||w9MZg0kQC$)Od--w&zz-SI2!X{X zCWau&@p5hjuo?Nx=K~<@AWBjL{hzEDK;G8hDXXQW1tTT{BI*^B8d$jRw#dfmK(v-K zts&|FMCix9KQ}{XiH1ImfL*ZW*d|E-+WPw4d+(?+`yGT8yt;r#)xcjZP2Q^w+{J(X z$0Jcz-))ew^N)q6z>XUt8qu+*@^CY0;VVPBL+ok@sHXJh)W2Sj6WtQIW#}mA5TY$B zZrho3l?9b8UG($C{cs0xRqbn*7xh>K)DsdDV*#jbL`;OhVTz#a3^c%h+Yr1G8S|IX z_vg>JyOW5d_3`5j5KbYpx644lMlOsN<=F@%2*H{L$sFuwlo@Q6v}-MZu#93{9(&=( z{5>rKLQScz9uClVOb=cHK-f34-g+N2T{4DZEEQ$g2vIeiyuAGB8r^l zlknSz?tH>G4vU?K-y$p_5&cP?BvgpDK0VyR%*LLJ=+*5lfzr8VZHy@o1NeG9=;^n|nCqab zT1a}hAp%P3p9DKWpg>j`T3gRIbKFj4;*;0~?G?dW#sF+Lzz^X^!ZO3qp32TZa$sSI zY1n`Xd;SnVbX%R)lsV*%i6H|W&RYy}*iQZ3!_$x&S^KY(JC3MF6VhK|LvM-y`%f8D z&b7bBKp)0+xz7rFmpvu~T4A}r`yuZqKqQ#3Fwf&}*wv{eNv>s0HuD$=*{&52oy zQ9$An)%=330JQ=N-t`0zUWzpsjx8^0s6XXu#C$l-?ISnwgj z_s{5c*#`o}jkV}9RJf}r4}7Q!Oz-jX^i)T<0)LSD0#F3ldfxMSbd(jo2|<-#?nH$@ zKMzHy8l_5r2FVH8BYc3^wsUanCRDH6*|WX%58w9nAy}InNqzq5M#)n|AF-=Kd@Z6| zxW0PvLLJ8qak+N*;P|asm{NQ)_RK6C5JCW$XpMMPUbME7q&`&F#$qB1{>v~I1uXhK zSKK5ibQwlJdg_Lhz%q(lZFQ3Vjh!_x(Qy=1;i$Te^+=K(6%smR$0o&<8o)clTl-hP zEF1mH3&hrhd&hc6_wZ49`!H`@RF4>Es1N|i;rPKJO#Ku;hQ^*Xm?~(jA3@Jk27eLW z4_vpkON_SEOX*d*QejJtmC1W@KzFuxN-*Vaf$O=WAmQbU z7X~ExjXkzAF~hPuV1P;-t4vZK3-q|<)O~w(_fG@id)6A}pZD(FOPD^kVtypfAF^|H zjzmaw2oBa-nor~7_uUVAo;|xIDq7w0jss*ejSa?yz1e_qp8L9JVRZytIdYV>3;DH} z<7$e_8eY0+#po5WB*!!BI2B*3>BX`3TLg3=hki9 z(_i7mhN0CE!o*1&;?d^4K9dxX)C>3S-ODwq;1MKQh@g|^8WlL*kYCaoU+#B!Yt(d{ zFo))rmQ2dr^=(#J__DJI6v%zbMp-*{<#Qv~L$VN9#E5^W)w4#LR}h3sWXiC@b3~P) zI%hV1#Iw8EWuroyhuj7V1J$bBBdjM;Kt{~S*3NFK?{rnd!Ac%Rh|afWX{q$YL*o`S zir23CLt9| zk`hrwSgPe-Yfa4s8P0p=56H_m04vD3uR#xnhp!0;G=%^VNw`78RI#=e0wJc=*q%do zZRC8Ibp07*6@+)~y56zS*wkc)kHO5!x-rl%`K9(^G6MJVUZeEYLZig2czP^ucGidW z%C)RqKp{S9Yl`Iriqcpj%9ExQJppe!I5?QWa`O+II%2`wHn5^MXJbc#?Chm0o_9<` z|9tc24QaGkGb3mht0!y^`GYk5o41a^-6IwS4%+7NGpe{TLj?UKW6PFi;?fi#l zu2#kLMny%DkSq84^?JN3sO;|hr}rLzc?EhX*^s<&;R27+O6*!-mGsg(@z9KOmMBqt zQiGnmC@C8hMT$uc`_I#%=_kYI^x~IiZ)ITrbSoiZ=#K6A{bU;trkcJxv{%`!`6s?R zc)?&){Rze#hZaYyZhS^{G)-c(xYtXxPh zBBmI5Ew0!>W5DuYo|F;Pff6GA5_cM7h!ITyjXU|Tj#=R*;j?V<+8i}1{R&D9kW}ugaZqlX)2FSzJtii( zxg(MR07#FNbLX$B^dJt9vJ>I8+r?0hk+DUXK1==5N{r}QT$mmF$;0ktrw2Hofbt|qhqYmbIyl5+`fi-u1idO*+&k4B~$X2II{45LRAz&md$yu@i~X$KK3MQFlc zAK6VBeZywM{Jao4zCIi`TYND%M0KD}+tEykvUXVK<9$cCdP&_Fx=Sn;NRDsv7*I z{1E3#Xe$Wm4FYl!T#pLU`C-hXwwMY@#11Ah+EF+{A$#}jD|YFXx*}>wi!9eW{K8tVwlB-8qAjzcEvqAKK?|?w zumv^ht8LUO+J$8{phfGM(QXR6`K`4(PhNym(^i2zeAC&1^IG?JG)h@fM1^|aY-;s6 z@`FQoaYQNXU6bX-xN%X`ITZ3sxrv?7-3j(##W^2tf|`IS{NLZG z@E8D>uZNsLl5)n3+lI~E5DnN|i5?Z-at&bZ`O0&KQ^{j@KsM zK41b4Ak&0>KNP(>@cv0e8{}Ny@c`xuxx6P!)!5*GCqTBBNUrwP&3#d@D~AxFAepL< z*mgnW-fszG%V{9p`3}UV&5A0vs1?Tm^znu|is*2~ra0mBI+YN{j|j$)a3UZgR=JTJ zrx)40xntl05l%kb%c@`-ty(i(ZaVbsTmIa+jiI7491N5O$7Y{+fO`T>AOyju0wD^? zoz1J{fDS~qL=d`x5JUF3JkC!4`lXH`3ziYC`W0GyB*-Dz{tAEe=+R~eZCC0T_FGYV z``rad79G>K4R{LMenW81h~tg@3a(d~C0!Yr2IS71brR`4;RzzX1$pRP3INl+%QGpGqvjal!{dn&2NBXgFk){K0=j| zxvc4ihF!@2LsC?faIqq^ER36Y#{^!^J;CA%UrA=e!y{`g4=%JLrc`G80Mw%RtGBV` zBDaiw#wYKIbw!gqD$F70+L1#m!pZzK{(c<>O+8+aY#x^+HIGrnGwuu>#_0^g>YGt& zD@LG=tE>J)Ib3{J=*Z|m9yCS}hs~Gs;<#v`B$4ezbUyV{b`YdfxW~x82tPl+w5+Te z{MGJKL>n$(A?Ww`d#pV-M4&U^gINO%`275`f-IPT-P*#LBwJab(p{-!;V~+TEtG5b z+41$}tG&njSs>OzC9Q)Jf`Bo}=e<|B#6^o0jv&Yg&b{|za3&1;y3^4;jDByOD}MN} z0gZphPn_5MEMHuIh|UNn#6dn?t;&ocPV~)8o@QLqE!Xo9Y-Fa2-bnJxvAP&1cJ{NM zDqjavFE}%Ll0El+P?#*ft#?|3FUy#=%r3R;js?TJ+^|VpJW9r2ADK_g5n@$89D+HR z^7SWRivf>X%nD*)ocqH8N|0;U{O1nFCTaJ8wbXhowxkNj+>=B2ypx}=TiJrvF|)DN zp#aKqBcOBw#43hZkjTotVuq11qOhOK^6ve-s~;AD~Qr3ZhG zGq(HQ9tUfWf##A4`BG!t*x|`#;Ue%_bpW-thc1V~Cq2Kl(SUEO5y2Uq6>F0D%|gEq znk!Pss&{s+TZyblXpPn}##`xFA0V@qix&7L3*RC^zla5NAu9EP(Kz~f9YQzYLnKh+rH!VzW5g+piwIa~2?v`v*x3yly7FdPJ2oLD5~PU7{~R*JcO zey|qL(m*}HjkFq)7foE1pYXa23kFE&kS9moZQ^a0K6eJ5_Is~c*F=$BJkvNGZ4UmG#U%%>6&PDbuD{rr>mtN+@ zgS-&y`CdtG8Sv58(!GG^te!MGLHOO)^S^0ssge^d_T3a3(^S_lx zspMO1()OE>X%s*=FvwI5Q}sGDoL})mb(2B|^llzG*VHgEoC0|dCRktof!< zQOMSPLKDGu{NfhN!mg?qg=q|^T68G_Phm;Hk&Cz;eWy2ZVDyEN#|MN5is9zGteCbq zF0hh9%vqer2oiUJ56;tt6o=lm-Q`w@2;g}Zw-tYU{;c7YbNt8`%vT%i>|Aj^^!@-P ztUkfw6kZ>;78VgQv72j>-Pq6&=Iqp*b-pp*G7|v1*+^7jZq!aEq3~ECKsryJ2x18A zJ@+l6@Z&b^SsHRyY27-xsSp1XM8}y79E67h?p2Gqsa>tRT{6K-erXkUxzfDeo?5eJ z4T4g5n#vK`B&y4C|K-I$v@fIcu1u%`Qc{Dcm8!W6_X)#O$Yz}zOp zHc3*3JTeCBqgaLyE17|^e` zw1%jgmR$P|LkOQ`ci_@quQG_YQD9BZ?>03=U^`v1wWUP`%Mu$D{k2D=Vj7xorwc48 zMewLpso$M%LS3D?eg_lK45h{8dM3{~RWG#gtsQ$+C4)H56{%~w- zU%ZIk8l}7ICLELJwK<}Zxl4_h$)p5$cz6c!4kq99q^@5rL?_fawO!`h;frx9gEH|J z0vY08VI`7>7=XxZQb08U017`!hi%~!D;Ao0q?C&mBwhmsDX4u_z|+Rh8hM?W$O?Yz zDp-Xm^LC#}Yi1i}&+CV74Gj(95fTx&fJC0g-4Qv0B=ZHpJEW&<@TeK$Rge|vq$EBP zuOxxXg~df8{jOwHW%igWE-4A-3cjCr#Gj&!v=Dj_utE%0GO+n`sRyowJPeH;r-DX* zUQfPG_6<%&%CsHLjo?+yz`eHb-U{Fr$|a%*HtE!dt0#;nY+xC9bNlG{IPP*BWNBm; z07Q_%3Y!2_CXqD2UX;uV3I%0FOeU|Jw~qEh`e}!*{&G+L14|Yv0jW>}aINusJ7llK z$5T+Stc@F>HllzW{}M?TlbEbi0KR+#d(GN9J-rrbuMT&N4|hCrhh0FP2iz2lhW26S z?zc6_0a8P2!VZ3D!n+%;_8tkJf$~Zo0)-p`KC~u!R(bnzdwUXh9iF}pQy5Yz$qHap zp)h_ewea18uICbZ0=;-@Y4P0b-@l(=^ z|2{2v>HYinB)NpK03pxt_a1ZylQH}lSZfbITvbB2!4!wc*uhc z*587()YapUHsc#cB2(ZP`GcRS@m*ivn%lGNe4!!VMFn@eDGRgOTV4cKrIXJ26cZbp z`x&?vE;Rx)DI47z$ADXFU%k5DY#U0JZ6Fho%OlT`aj&x&d%*fZUqRW)n(fK=6^HEJ z$CG^yI6O^+*gAKKV&lNbxcMa?l|JSeqRH|872N`gb0Z&~Dl{1WThyHRK=foZXUAVh ze}s!z?mz^au)@*Da;HD6$78d4153SetuC)S35j<1}xt`RKeD zy7sixRBI=vf`a0Wi6V#{2xB)546-ZrG=wFzd(E2W(M6r871l+s()OJ*O{_4hkScgI zMxL=HsHk`Kc@*pKYcL99Ki%q*Iy`#rxJ9u=)&Utj1a#d_bqLdB9OCIm7w&2{uI~IC z5V@6ow!r%Uo+*SZh?spjo$^e8y()8Z4n>+L+ybeRry)j3G=gKd(F&>&{)Gc-qOlLj zIdq6d2sB9IhC&b}XU7pM?WlaMYiqrB-2%RNjKwpkv!cltjumdXt_TPk(@^_nrD0bk z!g6F|Gskz7;*RHe7Jqv_=wS!t6^(WfJpjBs8x8F-v;+KutYnd_BplBs%t6w^ON0tm z(UPPE0{_FbUxtQ4zI}6)uy`E*@6inZ*3_#XXH&&*wCe3!Ir7}zB8i>CD`6G{<{dBS z{oklDHmME1Zh@>tR)V10xxrXm)^fe_HmXZ%H6GGN{gZ;5REOORh$G@%?U%Kee^AxJ zGw_iRI~WW+zW}*`gmc2^Bh>%)A%tFfdSC;f2i&K&yC7c4s{A*3G*r<1@KmMS{O#Q% zo^pOtO0a@R+xObPxVSilr!ydxN}?ph*TPqHTEr`Ug#7Orc;4Ti5NWc&iSz&ougSR@ zt`RkPeZ8~9_L0Trs`vf>h8~qWOF7l(fFHrLhEq}6J_O~&A;iSnMd$rw?if0`yuW8adh;y`M~Z* zbpbUJ%Y?P!Y%AY&|JwATnANkr;Bpz<`!{1T4SpZ?)>u~io{oN<2c|uXHi(76AUN8C zI9Dmrhn?5F$huF$HDuWFuJWopM2;`kxrxLJPC}M&%_Wq&H401$R-#U)hFex)>v#o* zT$jYzGiTILXM~!1M6j(`p#j4fv|MhZI){n&bqRAh!$|J(`-lPW8}qp7=cX=cGCYmP zpR}F7<~h>AfJvtUchb>7E3yQ(wH@LgBEN7zafpz(o$LM2A~;p+0SsCS1zAEGYrd-w z%gQz%-i&LHKD$40+$i_q`K5(Dh^OcX_hMs}FieyKac7Wq`iK?>5EP6}`!BN{=}(b4 z346B_x-ELC<3c&f9Adl-!BG`Ge1HAg9nR{f3>&4yDUQQ#Xgo;>X;nUbT42gzJ^6m; z_U$y3mD-4;(p&Sr1XteAVo+gLiMpk@s7ojl_trNSi z7l9d+nM0oR;)M#1)q}#Z!I1}IRbjpWi#rQGB$q1Y)B5?}KkAT;a{E-zvR=`j4GCyFmrpTEPj(XLw-M9-=2ghHLX}@rM3n2XzS&6q98-R%?;wPx(L{iA5Ve2Jit} zm)JXak2{+cu48`yIeO$9)Fz@WHm~BriA>1%MyA&L88)k6YxeAI!sQ|lVA?TbW-sqB zCxK(9EBuWAe~UEAiwExle3;B0Jfa*O@))Ec3K#p+jnmKCzvqFgnXpuf=C~Lml1_q1 z9#xn3%+RPsz9&ou^f+wvy!L2sVH`HMJDa`m7=vQ?IjZKNkf})~2&mK+Q5ca(dq&|F zELY0i-*p*vn2k5&SuVk~_MG@2fYd3$gywWh+oB%?v^y+(Nu<-3URafuH_P?851wMT5|zyO}=LV|Cz^YcWG^nqOmQ@G`juI)+7 z_@8(#qpp}eI$|8kg;>N`9QyZ6vA*!|@XJva(3H#%)i*wH*MTmo$wIT#7j1$VD;Z$$ z8zMo>#f=>-vrJJl09qHBeRN*X?^`VBPmd@*lvlw9N?d zHau)pwc5NTx)G{_|6GikWLet~+%$~h7A2}9L<8s$h|<&2(TV8Qywm<-#Au%d!xFF9 zg~jTx$G@Id%_1zj z2;C*nn0zV5t97@+%)B#)`yV}ewA1cHsf4>NXWtV+alf*>8N%z0s7+M&UIz{X7l%e_ zBgoctm)*{rBVb3Q*4ur3P(oOW_U`L3gB;-Y)efzk#{y-k@$W^FY{Y_sFYQ9R3&LF+ z9gUq_M70Tzgy|fM?|XK?hA9y>7vdN-vFQnQz#fMx5{Z%ZSJ%$Q$yM|0Svgn+T4=8W zvyTC0nu+6?dmuUyaeu7%Xz@WWm|V~(oY#qgpZg-JfseQDlrZs|pqu)M`poFwU7*rC zO2;5;r$j0kC%f^eXpewIxroRZ(&j+|QHhfmY#3lbdR?M}jWso;IERe$Y`219i^?*; zrQ4aBk|N=sY;Yl4ezkv+a6B%zCFt{Txj44xh~2&;&27i!1!W-zudp9EB{NG;aY758Px^2JE6iJM(lH@h5={bV51Efuc32uWk z;m&>wo3gN&a9oN;^_R>`V3KxubMN-^k@$^X_v|-n8`q3W&0D>N<>vdBua=>OcqfmX z!ficFE&?@&zl-qv1i?XN7-4WwfkVF~xN_G%72CPz0hBxUr;$?|0(@lt0qMb#N@oc` z=fT9jLbv&TIh0XYwToeA0J>%Sj)#{wZB4DiGwe{Dc-44MeIl-ienrVyyQ1gEk2_lz zq4^y>FfC?%WqR3vVIGN!-LdFipw}d_QAMy0_em(w)Q>P0FnEQ+aRa2{e@$4xRrpPpvXfTZ=%oc{razf9tnol8bP;zC`C+J_e|Y>yD7Llp`ZfKbOMd2_D?mgZ zoasqny-#1wO}-BLTtz2z;YC1w`U6l{TFbQ#|4;9LYJ@Bicq9JM22+?HAZksna|grR zkIgGLa#B-K*){KZw$RccnNEz(CH8fr^zmuN9ZR;dERj;Z_ptn`N3WRWv5TdEwUOLz9P}ea=S=6McWvYTM7| zQaVPe!&lSk-gbQ4g(yD+)gSkU1lT2VunW z8S9FBjC(CPm$@4Me%pDR*?rWV2L3Xc`U zDo)UGy+J4taeT_#Rs`r1`2zDi(tog50Omk=l-0PK2z~8W1Dt?zEEk}hf?Q1ZNt_d zKpTnkK~Ywz1VJ|PzH(T~Cs+RGAH(TPw+GLz`O^?TQIa!KLH2g1ih;uDv~hTY{=?78 zcAtg(UFhY0?HezsuBhn5;Ys{B*1K}dJQ(J#;P_;f50*w2dL$#9HV^DN|3Z>V9!k8% zIVJ5CirRvq`Fk}MJskunjF2xZr^vNm$mG}X@KdNpiG`y73kHCI6*Wy)*BZWXib&uQ z`S}L&cidbF>Ajy(`(3zvSr_Q9)AfPYEjSl8e_jMiMO!q0e`$;Daud%XyEiaR1XIn) zX~Cr#{R07-$y<2*c~hc4FSb$f=NHgJOdxCj1r)zLkYp6wect`>YR%chfHsm1+90Qe zhRF|*FSulZZF^R#<+}Mm5sB8(7=#gp{;polhY}n4>M4h8lgn@^8wL9f)QnF|EP;@> zMPz%K2VMqDT85}lgFlUfwk-2QCI|xdHV@yp72<9YA#?SHSD3jR9AL00fsP@&=DSUj zY@i2ta6{&u&Ae~?;SzQ~xlw+Oj%JLt6dL60L|9Njys!=FQ@Q!zZ^#qvncFCFN5D zB`IAZ}`>s(N0BpxhPFi|PcOc$07y5gw3<~nVB1M`|l8Ysi3(1Bp zKTs!Va%oOJLppljV_aiFKn5YRlteT9eJeUzrwMf4;a70b16S67y;>%w3!5$K+ZKy0 z-=R!^N1M>6#(U5nq4?Di(?g2@va$Tbhs3v^S{1}`Wy!Uc73Sxkg8<8}Jz{i^u>dHm z7e^3=ijM`6VK)}p6_tZRfUUT+6>PQVm4|r%A~nEHDn}9{Y(w)Jh9#7kGp+ca0M5XJ zOj{=wcVj!3rj(3oklZFb9BS>s^&&FT2qx_MYpRjf;jK^|r7qY@PQi&mNS63#;C{ej zmX31OaQ+%0-~%tI7MgUG9|;BOH6vgSoDzo?_IiZpt{$3-YI4SUgl4MdwF+?XhWogn z{r@R>9t@^2uSg%fyl|=%ZwD4E*}l1ZbjSV$lDw=lQ6bRoUvFT?^zgVBQueIPMso@w4z%9ORr%#262D-{28yy=lQ@3m>>PDqVB`8g|38& zMo-W^lH|OyO8}WLcx8@5@uN~S(bjQvDH^Xz00gf8DXgr){818lYf|*eJyj??sZC}) zw;gBEIP(RJ`dNJ38{ZM>muBqTX|vwCLtSL;4#1A_%fr8~ZE34%ZH;n1m8r%AlDoXl zf8os8KmyUA*Ela-=~34hmd%etm8M{X>^ewZNI4NQadBbs zBJh2rEC7#I4)PA4`&FJD4~A$2A(PsBq-_t)Kq$0Bk=bgx=DU2|2h8H29DH0(Sn}>! zz=0~ETvACe_V~O#)CbF3jLG${v(@(e3$dUB#D86jH<92Pg@dD+nWsNLdB5?NWFp!J zu{7a_nJ4xOl-#k*!HR=Pbzi<1{}ODtncLp*OJEay0b%P z8DGAZ2*i!a)$g)RG7SKml@^)wfvXH5F(?pX)^#`GgsBPn0-R1Nd`Xk$ zyP1s*4V93XfRLD(V4(Xg5AVXcziNg~<5y%?;%GewC1vx-G1$9+mr~-w-1}AK7@i?> z>jt}K)hT?waCW2Ol5D-4#%xaExmGnEkP-o-&Yq*+Vitoc374jj%nV9yL52)eU^qg; z)jAC~O|e1Ke=5*6>ESh@^3HU-!-x5>8}Rl+cGdk+pTA6fDGK44jtfB$EV*a?#r0x2?qh_qQQ$NP@trO@D3BS#Y?sU= zGH>m@f9Oq2m$q#nlRJgTYaB_qg9FhWz?pCvZ67RBtl)E)s8lHClVQelAr0DW25$fd z4$yt$4%dOFIi1H}!_k=Suvgwf2qeZa`b5B5BY^k&;)tgr;1qZ~Af73A|M z10>|+F1LKldQ%8<0x5RT-E;BrRe=vQw5p1Oyynm|(D;LhyX#L5EuFbaW+^#j zt5*Y=2jpW=GMpFbg%p+$Z5{hZ{lI}3=e!bO?gth@7E4iMA*R5yD>v?~1Gq|#6&Og9 z){6riUzs=WeTJbpgeS=3oP?wgIGLm?DMnt-fp`|?A0y9I9igB$s0(|5ISHl?t?gF0 z5-{n8vQkWCgKuw9)G2$VqI+l~6kXN<3`!&fj3Ih|Ee_EiI|SH){v$G-8ue!1vG75a z{%$jHl8~p`Y>h+dDrRU<*FS(ifitnjxOyKh2sB67VHDJc3RoLVCqN3A?FJz+N`Y7# zg=NnQDW#N~78Cybtxgo8f)2IzH6qC@94#c-Or=Q;jsw6=43;{Ui%L$rqTXPA8n3wv zrBgYA#VMLUc6E^jN>1pzDIwx)K5o8NpQ0nE|K>XEnB2Cq9%VDdfv=Z?VvS%A2j>18V&++H(U;V%&)K+ z^G{C>&py2!msKFhbs9T>=@u&qO+}Px56} zdOXh*wA%zSFiLT{>TLg9A9oJr|BG=lxe)gKJ{2}YLS|9&=fdsWe^(vaRV_WdZL$FA zi_Pw+b%ce3R9pJ5o5|M&zau}>sMn=qt3Y|D!xNA;Waot%aOwOaMA+aL36QWAV&N~8 z?G-g^C6E>LtD4xpl)+08j^2ZIx?5gGQ1+#Tm$oq_tb_b!CqEEy?QoZl)gH4hgD{RX zir#%0--+-**i!wVR{~K@jJJwGkI!r9EubTG{2nR5qlhSm9dn7*b&3H4t7qD5dlk06 zno|yN#i0j;z&{CB8X3S)Z$i|%U9j;hoSSg-hX1??WgxtdYykP^E_0j4r$*65ER@~* zh^C#!A}Kw64{;1siU|aqD&ar>U^W0!8u$Qd3hs3TjB5jXS2#Ij`|K3EuytPWyTwzJ zlRv>1Qp^n43fVB`g~dvCGS8}Ac0%J1m;~%;=7S8<*g)x5H*=&e(z>dlHcnaaE-^tW|+#!%Ib9w-E9Bb{Ltedvi&2nSN3P`f_?N? zaGjmv*XRo9@}Z(6NEQTdAXM+hT{B1iGLmrR6YS7{rK^eU+1Jtfb;Ny@oyF|9n|kos zc#jQ{KL{a4j`G#mF0f(7uYuLjd-UCL+amibzB~YOuQailns4`vq8UgilXk?2G3dl& zg!LI+nvLR1*5aW6eA(4lg50G>cL)w7aW+IT@*e#rjPrgYJw3L_WeTo9s!vh+M%;U) zKqPoB&hE5{K4v@O6Sd?oXvL0b!ETLxYLcPi%3mSZmI4Tdw zGBvT`UB`y(MA>7yKH|V|R|kgJ^LyzkJ~wyu4I8e>E`1VZJ8nQ5r`qOUg91G-wIm3Y#B;oQ500?`OoIfMtTfN z5M7%>`g-{FPPSkOBw>ZdR;WP5^Ajh;&Twtjvs5hTCyk<+?i?5hhK0#ZTv0(`GcYQ6 z12tkUfV6wQX2`sC)GYLbgS~x3K*h_K_rd}yUM*hiU#3qH+tke~{s9a`&JZ{i8Xu!6 zG+mLlFDf{Z46Y>7*}bMv1&(4GU4;Ibzsm;5*G2>#YQjR!Q#cIjl!SGrNv)q3BE4P6 z54gIsIhUYza*Eeb$3~z|gb+eSaB#-Bo%!YZ@5{Q=T2;)*M!?`SvjU5JP z5w;AOk+J<3;r^j9QM4FPX2VJFCC&@LvFTm}1&(iRekMQt6_Z10r9XEDpr(wL_?*rq zIr9fV)-jG1^sb5H1df))qbm>ELNyy}o=u{&uY?BV9he0H4M@V>z*|Tx&76pmal_Gs zg}W`)7lIzqcA2esG75-h#NmMXVb|FrwCvZtM)W*XPzr$YC`TUZJO*X)5xF0K2w45i zH5FJnHJfnUHXg+WMoB@QGeREk6=7F1&UMaV8i{d9vVqvsNxgx~F4@JLDePT>=WTFU zocayi{VaZY^J;(9(h@t(ZFN-P6k-lY{O}F?sFW-Y2|LGT^72+dB&ogRBTk8OeCaI< zpN*u|1^((j?&+D{OGB6-q{4yjG!0~IsjXe`W|S9BzR*PL0@GUr1vpAWB&}atZwldK z$_T>|$;Ap_-zf-&%j##{p2SH08IZX6^PeyMgv;-Ry6v_%kV6Q));T?8J8MoytxIzX zJBSUqU}!!Pa4>TJU_Mg8JJeB7Qx#{U=+pkb@BNXY12RT!;y|F4om$vXOrw#-^Et;a zZX*`ljt+vN8c;@jM0bGGogO*HPe1^k#CP}YZn5EHzMlEz`~|)!hVfg1auKT&vVu}e zMuWS3p;J(q5G?PEX@zCrltBQ0{JJE(ekA#@;<`)YH3TYTYZ}FTgN_*h*Agq52}ZtR z!LQ`&rhpul?MR{19sT4 zke%^A2Z^Bm9f$%*{e&H?0H_aUAn@|$^D@7}@&h_4WMvddzy+%30+mEv(23iIWRQZU zD}zY>NgyQgVtB8A+uIL96zGVTN>eSKRoGcsZC;Yb^mdON8Xav<8v)n`fk;r*@wb@k zf;c+LlqUihYeYcQV%PBpn^bPE4t9Xh>WdNb5EAl?M5=>oPS=h>OHGRr)e==J#987! zJf(zol#?KsIv@QgdGR3m<{Rt7hYdz{`pZ|uLQ)D?-KUg(2w*#=?IlA+g3>m9%_wql zshT&vV{5_N@ljDlg;q~XOA7_#wN+9Tb44@&{#1x>n{%Rqc?rjrKf@!~gD(4w6a0eVFi0E)x_0e~p2Gqof7`uYm) zeMY3v0`%UKXDX0qnI`+{52_a|LDB=FM7nX>%+!>`DHO$(_Rso()9FhQ4R5G?fu93C z+ThXhb;M-dPW!)@1GM*^i-^wUMYm0lAl(l0cmBs=a3KHx#f9~jnNvxP zc%iOI32GWjbV6~Cnn;`FUEcFVJ*zAsam?p=N|J2=y)5#fO$szp&4I{ z=LNZc7=HhAs}k?KEk^bt3ecUd8Zzp(ubc2+Q{lT~s0;DorjWu}aMLWm-o@^1(Jf3F zg!Io3&O>tTT1%oz*M$byU>Cr2P~l7F3rVt}qV)$X)v#q0P*ZKh5CYSHML#eSW0zzr zI?E;L*t)ZrCLy(kbMo$9R=DL0U<7c;ltbUqh-L)%Lq2xkjr0`uwa>BPI=yOm9!S=QpNGP`*XXK9u1+wr#&_EibgWOo`?L2x9 zF>+RD&rJfKB3#L5!M+VJ@%)=#Vw%&VTjJYtR&ogD0rQWe)-Va$dh)@LtOSL$TfSYSV3v9_uF!Mt@rtKRDwlh07sKyYLA|xDrB{L=EF`onG zN({|UsZiPe&lXyS1qVarCG;zfS#hlA3m$+&(8P~YbpStsIG&I;crb3UriuzSK#lQ? z7lzR61;O!5sUz_iI+(YzTa==Mp`k6@vK?s_iRn5RAc>)WfX-TieGVW88=+7vmi)Tt z5kz^;KeRdI;1GxqFA`=$8|nAy1net-`c|}XQ;d2<<|eS@uqF}uhbpGF3*J6MC#|*^ z-jSLot`lI^lwB9&PNFT2kPhC`=L*>Ht+jdg?wyBJ36BjkAH!l|bi@oi7%G-`&q9rw zG^|b&q zU|UWCbGXj?&t0iMra8+q01j^#ypKaP6g8I6mw4&>k?kK|Stpld(CpRykV{GL=Xoxr zi{@kT9x)pnc-uNUTWh|W0-=SgGBQ16RY;3sGqUWiVVD<=2)Vrdd`9bp-~4%?ZF;6> z!$?huXt#!Y(PPMn2mzUD$ile^#An;6Y0jQxCObuU;GY}J7*9il}E^o-!4h^cv!i11QN_aYyxavC!cFZb>d zy4yiOGWqNzT%^2E*4DUlj8cG@-RK{M0HcZfHWDk?!d18{oBDcE`?BqV4=X%eFk zSpcLr(YF6M2-7>RDwUWyDo_wS1x2PZoYrTAds{5eJ@eh&r6fc}!-TrfMi6PLc&$*W zytuGJ)!GaTdRy7bN;s1q=3us9C{*@wkjh$wlT6O^H7-Y z^thS`=HjoRT!1g!r? zpB$z)V6IS(8r?r^acz_Dbts@@e7#PcT8y+&s5;|&0j5?_zv&SXAitE&=+2M6zAXuV zQ$m;IAHLbHg!>+QM5pi8yT?81X}4?;I!EH}le-}$B1DjMB#{0seI1;C?B*LD;jV5x3 zMrfP>JHQ^9T*e3Jia>ySk-mnl!Z`u9Zrn+VGbYb29)2JeOgGXlwBTX^r>NCdw@Ox) zcy+KGJLesg+?jsr)G2FRm1Ip_btx!lg$WJP=SbKXK!w=XUpwv881Ep<$?wj5dlnLMdgG|zKbA` zTskOOxsBot)xBx_niZ_z0a`VvGjqNeJ!9A-!G2o!RWg|3oQ{^)q7eGf1{dy?g15t7*H$Vuy>eo*IEbVQO+u_vN@i%-57cH@8sat zTk)zL!QWYGlHGfIil~X;Sap)mxh{4?jDR62nK~W5e1gs+7;Rx$W1~T2j;a0b8N8{( zdBxybvmdPXFIy!oFCW&pPyL6sX&ofr)NDN3iEG#DKaE-NLe(8R?`s+J3Rc)vVGM5D zFf`0>mnVKj&D4A(u2jRAw^GE;%4m%DS-mZxy6{Eg@mgS>pzerKj{_nhqKx&qHWxaz z{usqgN8_aMch1F1*2TOPpz($solw>TOapSwi(fEA{VE8XzLViiOm}x>(xJnyO!|(k zHs+`IbCe}QA^)F~1YPEf>?N0cw$;@FO2V@SfvBah6R{O%|C>vVMT_c_((Oxic`i); zod!57o;AV3AX%(!fb-AkdqxmhgeYnqdiPKed{qEMnB*M>xOnjQP0u5lJ2Uv;B7*si zha@uSt5ftW{3OGEW4npoU*I&;6d48tqyav&LRM12WJaP-IY_TTn0Uy^r?|A1FCMT6 zu#my;@6_;OlORW<@ygKf1R;y16D_L=gO5@2r;7&)`yG`|9jfL9j&s^Z7`&B26tGWI zr$E%QCl#KcG(yl#Hw?rqg_q&#U(xo6h$$L`_dmrfC7R((s2SPHVODn8RYQWiTg(J=rjyVUn7PoE1<8&8?GR3K~EtAGmg( z|Fj?KCn~&%3e3x_ISm)|Ih`p?_SwP|kmovz{ycB;=9}ch3oQ8y@n>i5&)EuEsN0-e`(thqenN!{``?xwu~KH*48*Y<`P~2 ztRV~HNAF0(9wz;2f6L>O$3g;j71uX5hRTX1LV~Eigu26L&r*&zw72isIr(eno4OBq z24UA?zQ^`^1g{cQ{P`A)K9b0O1!pGiiW5bNCxR9ZdRCMB+GY=Ioevh3YzW1Tf{+#6 zGvDD-mB#RLtm5q43&Q)(pfjlWmkIFSxLkiSNfRcM^(_|)#=NDO#?$$Q7Sbr3@bCwucTOT_)^;$g>^c1EBM+b@X+_!5FQ5-ud=DmZ~ODtqi93R98FA8M0?QL~aH4N?Ahe6^>NsX^= zRGm=!A}(jM)OH{cRxN0=SaHu!f~|meKm(~dMO|Ej3cGc_*C8Ja=ktxE6RHm<;iJLK z$p!G8YoIdviHssiIl0+roJlUf(NOWb_UVo2uMS8bjN%vPQT@AoR7%7vxeCn|#K{CQ z{BOqAiVJ^kE{=CQFmyUHcdfSFxyaGeAVH(jRZ%G6>|8A{U($H~^v?TxxR-a>$s;n7 zR~Q55aoGsOvDYDcp4K*|zF?^ZVyV3tiOLn2>(Q=t;d1U0s7cWb;}c5Tela=XU~QdJ zyS^8tK3qm&TmD(Muc>OjQ33yhm$cYeT3Z9)n=8!mwsv-cGATV9PX1tMp1(&C4GIdm z&poxLFf|UeeVP`>&?_qH0M;nDc4-Mwl-WNx#~(OK#8neGY+#q)m8qnH{}+E54Us}t zi4~}j?Om)I$-l0kDAqyS59g1jSR)h`FNP>t$7|GJi8|xlKi59P4^9=#p$(0pv|rSP zn!xXx5n{{W_;B25Dr}foYg31`KSbus?s2wtEk}KhiAP|bt)sx%djY%fnV{Vjq zfn=!NBk&F2=1ZvHWXu((U?sdk#?X2>mmR zrIP+hM@-u*Q9vQ20X!1wZ3b^bE%H?(gZO78W$%J?8P#Ei9<%=npXmVz{QYGYDL#r9 zn1`Dh^gD_=eAhrkd!ZJ-X|IV7PpU*zz*!IWL!m%3fZ)g?$eZY803yxBkN?k6woPXJkyfKU`3BmgUA1t@?n>uYfu?EE;U!@%)4ug9tNM= z^VwyT4v0>G;Bc#ofo;;0$JH%|3@xOMJ*#>>VpPnb2{M8HvrYm(DBUcCGC6@z$!hq2?i|_Be9tkeC>zkeL!){96Ncj&WtcY*aRjEx?S9w?2F3cB zWSLxCe+tJeezmm8HA5~L#nn27-UKg<&F)9}QvFXu=| z4P636^qeSTDfozKP&`{TLe^iRT>|BysA^$Bfff)_=`DMF@HyeV=5#v(*@>9oN#*YY zHJ9s8EqhmOYk9|f^=)}Y%?SS2ub22Gr>^WqQhASc>ihktCsRs#f1txO7Paq|HE)Sn z;xBn+1IEg~NRBM~o@agU)RVm7%>Oj7uvR1DAe`Ggt37(usH#n%ZGYicVauZjN&8G` zwZpKh@h@0#OI3HoHO+1_O!s^Y3P!=$wwrU7b1tF1?gqBOsQ)>bO zsS^sc_!BA@-#VZRkooO^=9l_><*zs2Wid1#e#R>%^9tSNA`0*Cygs`kf3Karz4MF? zPR#BFQ@$9qjkO1`{u*Z_Q}_~CMS>=ti1l@G`|nWm35$<0_^P=4p8Mu|67&xO(jkTK zJ`GIv{zpmrK`Td5xG1~mT0zYSi%&4%I#vezHMFf1@6q=nV>Z#npu<@h7cT?2G3svv ztVYA7pbSQ=7Ka&p6*SuZ&%c2}`YCAVNZJKy2hBgD5Pw|S1rW53(+G;O;qCLY@#AyF z-FoIQupT)&Z7eQB3?04;B)bx2&5+k8KAy5~fVPO*73Ah7lTUE=Ad*{BaP)(}tHh~Z zvzU^0vv?6icCx?Iv%mG?z1XfLm>bOGqNq6{=c)X|K3lSJV}h(C1U;U@ESklF9>13& zL;UL1eOtV18b^@O;7C~#DqkV-_gkUsg$^S#?_rJx#$zMSmRFMZ@Xi)?9gDAJP{;NA zLjF;jc~}!AX66(bJHYYN9O==Dy=+>x+t4CilrRAo!o4oGNFU>RxTKUQxk#8ks= zks{l^8;VKu`DNW>O*-ohJ(iXh?xdi#3K)WV7vU}s4-CbfLP9gjFr6+Q?O$N2$*!#t)Z(RkYW7k|wk=GF=%s0~!xJ_o z2kn}!&3pXe^Tfnw*8J;`CON^$5f_>Na{t%U2bN!Hu;-iW^f)pyl7?LQ?2mdE#`-xc zYel2hPW!N`9mWDs1v{4bDxNN|I=@lRo=xJs$ZsxcNRj2os^Rg8)GqMpAr}B3|8)np zTUl9Dgp%nK&Z~lz&Fg*tKDL!1q1i(S#YP%KOlBwSW^cYP z9yN{oVI)@DPWn`UVT14?lgbsmD*UBM_Ak95P8m4y+&aHQd0^v^x*4JO{r%3Kt7S4H z5(Tgn{rvyKy~wWN7GYt!+7{xHki@o+P&*PpioHfQtr^ zODWI=#_uRZJg6lij~M9J!9@GVJ>7NBp3%Gm z{V?qhHj@5TwGbJ6R{3luD})(u!+YoM@dJ(;9nR8)EPEP3Zd6|>pXT|VON4GVFj%)VX;SG==0uLHu3zdgV zlrHubb0q!GN0rMzT_mNn?JlpCj)d!@fqKYTa+oCMTLX|)Lo){hof zNoZ7F!5PqbGvm7RT`}vY{m{Uj`#2&5aXA+7npYX%Xf45d$}ZB;>D9_pLB<{KC|Y0& zN8WGnJj(D`Ga9{TfAhMPe%l5g!B!f=%KECQ=c=9*T_z|v2(-Z$1C4J;QohTWVxbF` zGF_~w;6e=AIY>%ECKOR^P(#BfcIfm0>0`}M)_Jn^+AyL;R;*g3pWoE%0Eh!O&#h)P zSMbuXkW1yafliK!iE&mo`t|KgER=$Lvd9;}B5TP1#^kd1S^SN^*jNosnW)*m8Pju1 zy%C1{2c#wP7BXhaGC_DGCf<=b6!AvtsPPGo){txQL^&5h~Mtx6*%0X<+gms)8TLAFlD!c!!$F@{}0#B z-%lRMoI1L8YMWR439SMn?h zX@k=ZAO^-s&k#4Pt~~_ud+-<~Wze7nI8gxG&qHBd1%H#g*KZg1?{CKpt4rmd{*{Xh zXHlrXt6Pe5?%TZtY)FDIN+9xlufwp(!yU~i1@MN8@)CTm$i=wW5doN_I=>&)PBWYM zDTxP_)|)ZNNmhslmuw&uO7_x2BO`XNo6+_h{VC~ySV8BXc>Z05?fY37l*b$YySMY) zGj)_LNb_+9fH`ZPlwN0tqqgzSCf~pFP|>dQnOuhh^*m%Zk8zDtbfN+x14D*4VDT+f z^VG}#hVM>uMnFN9=4W6!F8o2}=@Ieq&TCUkKnMIyJB>b0Q$vFRdq3r?k^5WNKV*%KacOP5yH=Qd!{LU_FWd$ji?MFb5WgRik0j*lFG>x0^hxa6oq_`Q1wZlE>t# zI`O>z57ySY?YnmDm;v_jF>F2Y9WIfz{%mn= zvw2}RLl%u(@vn&V*=w$bC`sr2HtE|7C1E2OeyzHCI({EtMN$L$;i__fMAT1Q3oer-O47#0{aNuRjaG zLTc@^=hF>%D4V&;di{UB=a!_5252;odG_L@%f4iD#cK9d5fT2Uyk2a2KfD3RcLI#| zte)zX^DTpgHyCxQ4esnf1GOB*PGV8=W26Rhe`hkNMduY)Ux+`n*~&_i9Al_suIn#!C~$(3xs1-h!n z{3lT#sig;=sC;A9kVlOxOtV^^p7-n@d7IK?{!lRYlvmw! z5EVNF&30?Q-zokr`a}t!LcE*ysRO@GEgE{z?4#Qh?e`L5CQ@(2uI%^F`de>%de#n4 z&VD66-1%*K^887OnFTW+8@{B=012$Ax^ovTI;@+kKh5cA-ZPloNe7%@|A;9MR7O&_ zGjno2Wd~+|kQ6t32>}NfQ285$*P4@f<-`m-R*X$Tq*d-**oBvelO#Ago3#bw_uRj| zZmaPV{{8%a2UieeCgXtjnI~4omm7Nygo9d1z01GpW@>k{=nW4f5MUj8TXQQ&B6f`hGJ)Nu!n%)k{ zFr35efOwwHGu&_{D2N-QL!uLopqlfY`g(w$dAm|7W8dz(>hSAB`2xp_vP2si8ntTy z(Puv3L_noRzW|&D*_WYkzMHbn!V9O^6-7I49IqsPK(boM8{suBNEsu!5Ki2TGG{JH z#MrB-s7TAo7O3fXRvmf!z-sMRIe#-bUw~%l)t^fKG2;@HL#g45p=#)~xg_Ng!1DO- z_}90wxIt*B$~v##vd`-3R9^Ayxb2C%w$l0xb`IcSBdmnHJVgQ(5io%rMzIT~rXo!> z5C@mpDh|rJ?%o~qX;JUomF0>?RX1>`F1*&vEaPNXFDOn)nL!PrxA%r8ayYGMvjcpb zjZ^G;t`vpKvL?-}v&r3SWNKP#u<4__;|3^{LdwgTeCmDUc}G(o*W?dWR#x8KD@$!7 z9`h`Q=u7#rjg2^7a7wED5fyo*|M~fe9iBhre8+gyTA5sr|8!l7|BH+yet^>(I1rYb zrBj`O_ExV>Z*w2iVOI*bT{yMtXt^K%iA13lR%ugcq;>EW-i={JLP69buc>zxXwR|i6QKIQZQj`J(mK^O`F zq=}#65t1#%7FReu?oKi*KmjyO_$XL~4O72fQ1U$^^HtcM{U_N0s6K`B#2;!`aoW_7 zF`flyqsbLrQ~+&6=!Ht7 zC0NU2szQaEeJ=|uP`t|n&u{Tkj^PPm)r-7>9e#O6>dG5 zptbt;?v>ak-{d8uW@3G9#vRv`R`)?kl8tk@;IZWBW04 z0M>A~sfgSl_bNNPYv=NvgU?DQz*i1VbU{f7Di(3XojB?q7lU$~+_cRz5ry1I?*k`3 zvR|)H<~0PKqpHT1X`O{#B&xu~{nh&NrE>+(p*0I|VLI4q@Ot&tb7}l*8j8=|2LrQL zR%pXW>rQxit6^S0i_UFECTH^8pF&0nDzLJmY;f)1>D(*05wZXvQ=%DMnR0CuP=xUZ z6s@6fe+q~g4G(D;9N)oTY{SJsL|7Rx+|J(K%4HZ>wJ3i47an#p{w13664E~)tko;; zJU%;H{g1}Z&{WPFYuT8{-Id?@yb%c>Ab4NO&q0-yjP_4ZP!I~$eAZ1ECnJciS98}c zL7K{eQf;`Zi1PZ_R7Fa zYK4EP>}CJTzo;TjLyM^zSN#?Ao=lVrH^E6PZ{yiws41PBUo!(Bat( z3}5h{x7@(-tXOeLEu@J*QQE4RY>b__A-=@yeuvAfn7y@S7vA!H30Ahnaf=S@&*+os zL&x=ti^uzKVaQc(dc}pDj6E0IN3XsY?4Qq`&?+>N_!{LfYpiOA%Nn$s6s1L}lPLWU zbGl@VfUI4DR4Sw}t6;JOHAb%gQzr~gG$s7Ii;=5 zu2f<4lZG4M0$^oOt|`HXMgiPd+BCL-hMxncLyu#gwfXfT2*YSQo1ZrEOC3 z=;=>!4nI2_5?y__elg>UF872j;;}&@z#B#>_vZqOL?=mk6?SpBi};y0jz+7j`n8$Y z?zHXi93Y!cGccBL9e-N)mO%fidgI-JegnI2(eSWNH!%j`G2A>nMZvQ&mqW)i%k8Di zABnKz=si>nHn0?9KT?z|x<1x-aK$K zN;oK~TEi~xhA!kxn{)3$7E6_5alb_vzXv+b%|GBBK1#R~lgqOLhnAkyJgBcL1X{I( z$*RO9xi{9o>MYmH9*6n!=A}0K6d{8;w#B$g*K=s(8~>#RBGC!WYj8GEw}HST21DwG z#4rR=Q2jFs3u^512mSrLH!7Nb*7fm0t6ErCh`QiBVzW_}94vmWCVhWv+!jeT2w(hh zN`dE5`W^C1qfdQH?-U51;Nq<8>{kBS(0iBX*vhV7AHP-1->9S52dN@l$t~PZ#y(x8 ze*EAEF3BqbXEV=STx`Krwse8}4}TfO`n!P*!Hf#v9V`<+pUxip0l0y*knuZ~qh?0R zAd68)HX19HQX{ut$QC@Y52mGC{E2GE`EWhFu+*U^4GgXuI~)rkJbe?E{R#90EE=p; ztqW$(HMq7e{9ro|H#fs?`zG{(xEWO@r%fW|)P5J3n3#}q4yC52a~%6rUk0q&b>Ni(rJe&_&>NHS?~KR*_&+=X0Y&*{O{{G7sb4#*ViAYk z3A3awZW0Q62iXsSUsMw$Ae#UXshGyW_qX@4mQouQG-n7srD z2Hf|a7k7q++(L$+R()rs7G|* zFoWfxI(zM})dGTogGKiF9ia>TLZjLF;Xk#Bii+yQGN5dAfBx*Xn$ zlsqx_Y@^pu?eg%LnDjE^Ict_dnTyU)<~`h7GjXkJa6J zYB;7hiQ2!Kw)=lHZrpW0=GdaJ7w(*zp6!JXKG>`)KKKyo2t+D0dHl`8)F(Dn`rU>a zvlQ`eP8fVhYEMmacr1G5kGa*0o`T?;b*qis z|9s59V@|@uqI$12~akzkl`>$2|=iM%@Yr2cJgeteRj`zbrLO zLaAfnuv+NPPZ&AYcSY}7bmZj-7${0WuZlNEUxYwKBncbs+GUsVLGr-mmp5m1^qm^_ zQo7n8AE!4kdC$3R+s^V&ed|$h(p^rJFSw^fkzy|2#ob0IIaQ|ETkBKmPkj3L@k~q0 zj#>N`_s~S+%pg1BvSrIqTyzg9O15@*tjKq-DA$e`ur(={y1M@kYo=VnFPwjyH=jv= zY^-khoioW^9dRjp8PDInE8gr9{FcvZKZ>SfD9Gg4%cmquG# zo9`UWc;Z`+dsO7wsp-F`$q|Uaj6iT4K|w)OgpSYjaN)OUlnKr)(GsIDnaQyw5jEszg@$nDS ze~-hC{}K2QKhv%{rDI?J)qx~mY~&5|R>u~R(A>_>4w4234hhwLU7#nhz7M+V0xDPQ zMVn8n3~%X_m9D|4UOmU=yo_g(a|!w=-;t)%p-JU-2p`SceqqV8miL) z?|J|wNl}ym*BR)#-W1PA@zkCj8yh=3LK%crdd;Iv&3{DfUp=+d;rMq$M~eMBseqbF z%gB_W%NeX)p6Ce5*E5@=UX7-aW56m}WhPZZO2uexD$(naafiCfRl_!yH&!WygK!~F zE~wlxrJT@LIgCyeb*1@D>UH@S&=XP&TF481LO%v+Y}&^6L992%~~u4j3wql)D`+G=e<# zVP$o7{?G#-5d>bpD0}t>@Mw)`5>AfQ4J-v3CuUgiBp5uo5Vet*g5`&UmZObl(x~ik{N|oUVPt zFrkMkSpUj=aoKer`!=W^?5gQ2LBo9>MC7BA=09^@C5EO>q5lHV+BFrGAb~Nan~_qi z^N1!2Ir^5Dg*v;q}jLsJwpUCJ82!54@fL(y4 zXHcg-3J!C@2f)4Va9*+H&GQ9z;Wdcuzko3Xly6+po)>zNKdY-a+bd;LsG^Q-R1pM> z9FVb|gD`wpUE7#4;0Fce@u%BjrG5C%Lunchm$Qu z`vM6)@j2l*MMz?hJM{63B_*+aCV_#Ii#rI@Ow}{e({)n+d_&u%^aK2Xz3%s?vp4zk z&!CVB{0ZTjhY&pK>saCFj~6k3dds$kzv^_&srl%o?X-_cvcV^$QuZ9o+sWV2zvg8& z;KYRm#w^vL1?_`Un9u#|OjJ=2%*bVzS%qCY0kod{G@qE<6oy zSy!Yk3|V54$i<1^oemcrsKqICkJ`0)Mg%hFN;5MB8Ws2JEaKEy7A;{B0_q3+q@zIn z$aeISvD=%cW=aaLWY%G`syKgwa${=u&XzPed!8;Hpqlt#U9$6% zc6>wK#e&Tc|Cxn>bIew6AAyGL%X&{v&Ba0m0ww^RBD?@)__nG%?%)D|^^wCu@15A9 zMUA7X7&h7P$gu37o!v6v|8-@@z~i19|1z~lfE|9>%T=G-&riytymxf0Tb#sNX~bHvl)p2h??OktJ-I zXlFo-h>kn*JU8-3*_;w<5cP25%z{R>YKGg8&zSevk&|!mg+tL1VK+yKDXHcw;t_FF ze*W}nF+#%-Q!g4gg|bu=b_3}0IzN6aYZPHrnfFcHsHTBStXOS2`0!UEc3(Ip`2Z+6 z-ZBliMYapfrR`>(FO7c-xB20mhBB^xA6QLaR---1}c2 zE;O4nZ{9hmnO+Vjtr7QUzX+Q@hJ(KQ4Kzb@=DWLrEx{=B&Tq8-1iLkAiv9QZ%2MV; zes4_~4g3bp#)F8H{hY0eTqzvyAUsGf=8VIyqjrQ?cdR$)9`>S+gBr`}csB5Rh?y%J zFQ8dMcRP5;ck0r)*^>QVwi{ksmt1>-(J5Pvf(`s58s0NRQqbVw6lqbr!Q<~waspB& z;&rf$Q=GxDnFXC(t*pL)w^7(2z~4^zFRMuN0z9{Y*AaF(hF@LoiWMtWVcp6E6OU}LOhrx{Tr|d5_<16h6F2|XfU7H6hB|u_c1iY1Dl}Y%Ut^F}D zz>9u{_K5kBCm3)6vVSoo%t0w=B@-1Sfd2&HAws^JyZF)DYBX<8t#c*dOK;jR%=^T_ z&EhYHxX*%^^PkPen^6jnFl{CDOPE?YkGd?kJRDVcIc^ts;SvB+@QsZVF$2N{`bvGu z-G+ZSG&~l6d5fPx+?fsb0Nq&U8hX7JtUH=y{|<-}DoZjJh_8Ghc>LRILDUMFnVEhl zTv2SOwB{6qrR!;yW$N|H`aj#RJt6JIk$EM;;rhw*H%~r&`cx%*4+`5l5g2)>-?0Fa5}XxlH-GEA6;Br-L1s<~a~X!p&)0=w0t1sPeb$a@=#XO4NFxMIYp4tm*XmqL@~rK&0y$lh1ugG zI(S54mSV<=g7}fggBR16xp9;kce1EqM4qbc=xw&Mc3y=Yna z#l#v!La-f4Z~*@r;F&U<@Cr=KJq2QBVv4ZL9ssN52JzL#+rkgm|^x_4l z6MF|~EFNC^IwgV%5nm^ax2xgooMrKe$)$2`rt9CBy=95 zHHe7!Gh1_zXo*+$tFaD65 z#NroUa?A?3K{+%pTp4?Fo#N{|(phV?(YO&J{PR&;-967?uIMh*xJ==_+ zQqSODWsK;f>$rItM=$c*Y5cmBA}uQ<)nxgMQqyVrlKykc+`YVBv@eW@uOTAt`V7ZpWn`an9`F5dUC6U#W)4!x>9eO<|c4_IUnq)*@F zSo(0d2O)&03!p>*|8I$3=PyY9tas1`Y(@~s7s;9Pf!XQpL2g)<=V;luR^@DkZH z^=lq!579Ggr`Ej3XgF%fZrr%xX?O5fuvcZB*{2N3`jo!+(_>uu=w}LKRqHYJY1hI+~TTih`^%!1E6bZu;*a@{G zoQr2GT34PzAtv1S7j^|m4k(>xTDqqI zY-q6TLHidY1AB%}x;aIrmTIoHYffCUX}(!aZ8U2Dq`rhy$!U9UhbQj$<@s;iQw$1CUTu~k2kUKgx?!m^vL54Nd zS=1@w8U%Lr5%YyG0YL(S;o*0viZ|=)he*jshU77EjzIVcLwJ{>6t}Dshx-3f^&Rk7 zzx&^}JrY_%#GMjNJ0rU&5u&0+nUSn0vN9V&A=#@?NkWP!N=Dg4q7a1;$?6uK_f^0D z^ZcLldY#ufr}Hauf4|rD`Mf6{Vk~ye=E8$rWo0=3`X%CH0cU~$>ZulBT4@R5gXFDa zS|6RR>AtAo+OwyV`2wB>0+SSTH>On*}?Xfvsq5fn`jhnpS~tzYx@#G z1@JFBfmMdi3K~6odT|9;jk{5#N*lyKI0Fn>1WOB6*0>cHcNJC?XTIHVi=r4d9N;ry zBq`tBR4?bW!849S{|!PSbI){0d5xE|5GOls527ZAl*XyU9}qcFR=T!T{1aI{(W<^Y z{~c!?_C+uzJ>Ka5l>2<4Zz(WlTf29NrGM6?H8%1Rz}R2el!ZD71fsLq@%chPqVIu{ z4EMH2X3&KYRa~9B!Ysn1qmDbN-*2N)_HHX}X5o!0;!b-}nb0QKtC4e}^gf#k!9ouo zZZ{wA#n6=B!Ex=4KQ0yIQNtU=0QQY6A{cLV$@EA2xP{GE{jrtz zL6`R$x+2q`2|q@$;Ds`zy%?%e+x|t&=FFaJbQBAjtXh7zPpfEO~N7fT@pS5);6K zyAhR0?|Pq$<(mLWa8|;->@s@-LKG@G?BFsUNV$jQ0b(Q$o^TYH;M*xumDvv{%YFXV zY6Vu2i_;x89woqmO}oOSHcrg#(RHT<@s9e@4piebyqUb@`bGG2AI%K6hc^s;eDWYe zdEMcZD~$Y|SX3&L@l+|*vNlaOU!(^1ID4XZG?wn#!i@33gOZ2zeA=PuI4SwxDMWZL zs1!~lLyGq6?HGy0)Cc$OslzgbvaL5AM5;-#JIzKR zcP=P3em@CHr31&>o#cPqCWQ_!x$W2@xak2dCkPM~dc=XGdpndgo9AB8PB=NRG$cx*?*i!Mf@gP!oG1 z5*cygu6{u%gJB&`ZiN#Svy55N)jZdN5;rXmd61J+3AkrA&e**TtSd|q&|Trs2PsM5 zg~8^#*Ag`k85zB%xW~S~>em5@Yhx&m%xmEM!v>Nr?(Z{aX!KiHBWo83*)FAr&zx#3 ztip8E?|UvOuf#&I3kp22lq5=!(%2}tg!)YRx5qrjNvO9iGF7*23+ujzB>BYC<~3*J z*1U9t;k4@(aBM$X4;54eRCAU`j9u2_dcI&i4?vg{NFtZNu3nUh&BgbtN{fchr}0Pm`rhIA$t4OV*H>oyrF#8yxZQt@5PuwF`nS`6ph-;$;Ngv6cJ3k zIB_sa0#&P!>6#ole*Ac>+AEN17$Wh_0!0G5tOm_spuro?ufoeIi)IoS-D^UZZy`F# z&;q6YxcT6)bSZ&SfX3OFixwAVUOEF}$f#Z)$C# zGK35OqYYkt;sPDU+A?h0d|409mw>+OVB=tAHNWekEuA(gqW^Un?SOdEac@2z$vH|H zFkw@s4qTMD25V1!W&LGnWE6mzN0Rr%SbvkE;4lga-oDi#%mm;VA|N6K*h{Sg@&wOu zNu6c#Y@C0>XI?RSXAS2wFWYo! z2$8P05_>7@T`#spm%{pRJe`Y3Vgvq@#ExJk&ANB5skTuuFW8?;ZR;pWX+~1#H4uZ= z_t+6jM~ozutSeWqPA@1}382PHS%EjTM?^xxpv7nJUbcp6+z0r~zXYv5QHEF)(iKJN zIin8Yx>C5zwY>!~cqcCWC>nUSWuJbgc%aT{TEz-|HCEYls?x`a;*mz=`*Y#P^mPgY zHAY@~H|&d~jRQ(2kU19o9;Lzq1UO#t3?u9mi)m}ip zNFZYaGTWYg;w2UfoGy(!FY+JV)S`o^RfJIqGJhx8_b3~%q4ir3)I7O>x0}YPLY5XJWF0GPg~Ew4b$^f|qt=C|)rkTZbpT$}4`Id95Yhrp8acyY zR}+SBCRtnVqcMAUyMyuS)Dr^qKzzyW*6=o}>t`oGBXq2Gw zk;gEI3r;V<2p6Ip_CG%u6LP=&LJs~oN^k6Q^xM~`zQXoOeyepp9}%AssUw7-?1(ib z{4!24(=FjT&jPtG^aibdRtJUIt21)0HBnxPq^ll!?_LYsA-BID=2>#bHISRXlwF%I z5tnCbaWOxV_%}8oBx|r4;X|A~a!2c|RO~Zd zM1zJxQVA1)XyZ*-Fyi9)33+d^F~;a1;45)nhlZM(-rA#>(*+ARz~ET^b;3Qeh7mFt zz3ICcGD1C?l|G%nno0x0 zJU5E4e%OEY=lWe2khrW@bOaJJ+45tk`w34^5B&g&NYE!{Q1Bf3pn$`ZB@7HJ`KcsJ zpHvdq8fb>7ByPtwsy-#(VwX+VwX{TbZ~hCciRtt%8hC#am-Nu z&>6h5BkB{F5Z5MqO|VfQPGQEfxYR!o0@y2{T9r89fmegWCsu^tpYCMkv4jY9*{EC#H{ zRabXk(tsG6#Is`&1%jDUSa>Tu(}ILIffWgFN2==GWlncSaU_@rulG<&$rD_~!$ZhD z%t~OwPCD-@J{u{j_pH7fy9)Shf%S;XA$~q+(hHt*Mvo{IZ%e87iF6VW?^r?|DZqQJ zYq&Xid*Ic5MR-0PMQ&}Ke?qanop1TwO#ABK#BZE&x;q{^#@vqXcc`%2VDyOI+|K;k z8`3V5vEh~jGAL(Z)spQ~JF^e(JJA=JZ|fgpHvE0_GtuP=+~PAdn<}P;h3W0tlbil= zLuB%!GoFQ;n*~$nZbUajI*ollA}WfJ#Esguvik8~X!fq2=>&LntoYoOD$|C*43=W# znTQ}90&4bSoU=AsRnY#vwtJ%Z$LxcFQdm?AoL;D8UV=Rbida0d+w{^ch}KAy4ZDBH zo6y{kL=`4-wF}!ENn0rKh66aRuR4~wOn2W(FUtM29hdwX5)@u{hAdsfOyZX#MeXVR z4mKaoHYqQ!!gPCK5gsY>GDBEPB0{nD65)-xR4f*$+7SgjwZ|6H%|83gIk2Wm@d0p$ za|og^3BN>ZA`XVAm~6CO@c$8|sQcLGI_a1+(A_UGA<-8=)6T`jw z`U5GNoGQRm>otODvCdRR)r%kZ$8vix^Hc0Ax=JsuCN>Wn^)ZR-H2_d$85wDPNCNlp z8eAKgyIs%|gaU|zznu>~BjMcicodrj_SNc4xUpWu4OQ{>130F>yhJd^N>AL5Sc*#V zv&2!i!-WYf16V^*1#+YHVw+&IH`$@-bNGbsyxY=YB4B8kKi&20@i&uVAB@v74SN;^ zAP@PR$whDbXV53Bb~7LZ+$DbmH{s6$?+<{NsL)@S&ClqmsuUPpu?8;W(TNwg3F`@- zk#mEM>XFpDL17!w7$1PG9jX_Z*nF0)GR#qY(KE}Qn?lusRLVOMIdhnegh7ZA z$CGvK?MonYQ}Kxs#uW2n$2}q>AZiBHVhWm#)9JJkv}Px;r-SXT+ww~yK^RkMk2Uq( zKDq4We6n?Wlz6PFuL>fO*zx3J=5&t0)%b0E=aD_f&SEA7<#YGj5uhB_O}qGCgVN~( z#p@oHqmr(^-wYuiL`#P;Yu&%>g5BrN6;$#u7$@jLk7r}a+Ikiat;;_F%VBEA>bDeF z^0+(S&K#kQwC7NjW1z8Ox8{G2%ST>GN!s=~m=pqrU=o5=s(ywo>&XH}LwD(gf^LZY zaK|3I#grvHkBct<#pPqSn6sYovnPo&kdK2TUxqhb6N8dBI^XEd`_u-n##u;RNUQ?( ze~!XO4vGSVa{2T$DV2O-}AYgbKKl2W-~- z%awH@04kK?8{a?wDt{vfI`e}eZ%k=iT19r)Qp ziiVdl;H9hI+dtY?xt_}0M`FfgG2LL^kdqP)RzCNigPfPGOyN|@cM@nzWN#3DKuQ9V zs0=DQ(mntvA>+J5^`t|~h!^fw;sslGa$;odR*V-K`1c1>5NI~+X1|{i&KIO1SIZI<7!K){Om$_fO;Qv2|jkO^e$~L+6lZ$wt(}xRXe?fv&zz zRm~y!@-OMGSL@v zKa`egMt5z`0}(C#Is#lI2m#rd?a3DT9TF(!fgiKx?8}nRjngcGEzlGIk35B|^ESlj zh@GpQ4G6AxZdE}}y(;wUELcuS>?P*@0;Lz!!pudhLtRZD22JlnUS>wtKSWS~{T|8f z*LAE2+Yn!8-4a#WW?rIMnQJ*V5HjaS zC_4CJQRau;o?wR0`92WmpYt+&SDN5MAbV>UW*Q)fB2b!)r(O|=SQP=W2?+UX^L4@u zp!F%;6%kX{F)8&yQT+AAjslALKs-IaOUkO<ei;a5LpLd33^sP3!gP%zq z7T_BIC^{nRwEfO+y4Lbk+pfEy6iQ;$f;}<%-Z!c#y2zR6=tgidm@@xC1O*EssPPDc1iu=blHJS|1+(>&0PzXj2zN+*cuWk=Y zD!V929ooftA|N0DUV;qj3vfUID3ZWNDc?u+ZJw76oI?7EAAX69l7!wYa;^HYfLGrT z)G-eC=QTW=Z0=YLl{(9~<2!;yQ46ASA_;uz&A)*NsY#JUvew`38aHeSieU}&hiJm?Dj&X>Om1+fu&;DTOR-Pr;Gx5PddvOWZ z$Vq4}$s|JFstv3AQ*K!7fa(sf-<6r`pnD07RAYHcTf@4U4^)WJPkQs|O$yv8L294F zD*a`oC;7O&g^BwM`K4wejQ3PmQ-&j}y>dSZZZRS(T2uj4QZ#CeFwv4*&R(6?!6QCF?yfFAYXA&QHPa|DH%IXEX%LH;<)o?9rsH@k}2jvH+6tiz_jtMymgOudl4ayS4})QX~TONM;!G zG^}L^UVZWDCsdREl89RvBZ7;_74qM)rFlQTICJ*-D41RVEagDTC2@QKp?VEn%|FK} zod!O0?dm=ACAco*M(-35U1Q1obZ5!D`i8^!ST!NBA$Hu2P9}Jq2*?6dlwc!BxgiP) z%2e1HY($c<<}?3dr^?R~HILDKm^HCeSMvX>^gy~u#>l7KlMD|oMMip|3x@NrNS;sH zdVDJR;tvHN<4bUsp~RQyZkGf_$HepjmWOhc$T&m?HG1fdB& zaxIJtMULloPC4T!;#k+>E>*78tGOP*^? zz#%SQy{chs9F_4ck)~J95}PC{D=%-7uZjdg{GTQGLBocw6$3p{L^t$r2vn=lX~pnf z24HC!$%UJ02hk)70QRdflio% z5I~a~yX>Eo3~48a9z@f%LbcLT`(oK>AMWp0stcAf&-cgi=`e){HTFt5K4TY17}OfEm%eQ8vlTnAp`AFMMESf zj+{XztyFv?4)G-*q5w>ueGVXrz#JWCtfPM+(7>Om0!l#4>0FR+8W&f*h=hj4^)-_J zXA>Yuft={{k1LZZ_-E4j`tS>FNvEOe3g{U+w%iRxV zs`ki1z}rAiqN1Zo$Rgyn&6Dap%y%{bM1Up;=>z9@Q!q`F&I>3W?IbRWKHcpLoHRT@ zao#m$LhQ&1kFTpp!i85iBsu(TqU?TRyb-b|1g20TP7iIKjkz~+V2P~*CBCr3Ac$0P zXE^1H%S-ql-@LkYt`yJ`60H*%$ePY-xLrzTamVEz|Ib3o`;KCayuIO_b-we(ylvs! z8k(>5x~cfgUOglS*W0(Jf(xMdP0CG~G5nDX<-8!@B@m+=)=}orZ;P1w&UJPR#HB25<13NHL|uhYbm?6Oc#55%fWqgZt5%Qj8Ne zW*Y&o!7`KmS0RFMkXU2hURw6pq7KoUxCzgTt<~BG0t{SzW1iJM2zB@|eO6%$N7`x# z?ga}#yd3fnT*BjN<2KErU|5j&ApR&P0GSrPn|HBy6a)eYh5&|S1bp~f<2M!U^0c`{`I1hXbzdVdTu7aitJnC~52$zs^C5(q2DKP}_3XKMA z3C8d`n4-0c(pX(c>{WUX$=WZ`cb5~JC6->wpVQ;SWC&P-ps6^G&|e1TNZmAiXg z?&H&X+(N5d<7jK0)zZ>pc<^8lNTD+5FL7g9Nf?vK&;-biu-|jxVv$R7hTA~LW&*93rWPMz~>-I;59}IU4sR>WFgtR6fA1m zb~(c4-aV3ES_TO&OwB>KH(#thZBbnXXzP~;&Y*0UZQCb9`Hu`H1##;#ZJr6yxw+@p z9gyFSFMbo79wLaiFztXrMVO=B^Sm6!DMvizU1#BfB?>x-rX+&*o)BQFEl5gEZn*0_ z@r(G-WH}|6+VeCYCC6qWf8*)t;KX{irAyzS;TVI2KOnD;DQOv**0Y^JE8dM(t2t5` zW6GQ4)`_I348Bvtk>Z5y9!k-PLTHkCP@N6;4e?7X)1n3*A5RrhrNs4Us(ib$F{yF# z`T`0ucY~b|o{pVEy zV3r$1_oTp{l)|}>1M?&LELV(I}&s@Rsl2m*hlJ?2S z*q;2(Vw*1ncEqg)FF{}PZA__Y7)$;IcOm9Q4(tPwVWZ3iL|l}r(z!uD5HQXcE97=h zEfCiY{rZ>bcv5$E9zS^S&CC+N$1D2Bn-!u`5rNfrQ_bUL?F+4uqq}m1g$A%HqVLf; z5l%!kT{P&Mt;?x1@zyIfwikN1kCt z0^2z%wTU)-z}i2{%!=^I#FX^tETEgwr2y^j?uBB)Z#O3UWQf2VVcY0@V{B8(#yR#whhi;PQq? zg@rs|>=8(-26ecN+S(|#)bl3}9i9R7g#A1e!W@{~=!iUrUaM9+uZimqT(o?b#n<#w zFJG@&b}{rsCYe1+5E(XPfvI8uLaX|^b=t@iFVF61{0&swd7-gaB@bWq6cB38nc;AR??KA> z__d3;hpH*I?@_VDnxqR~*#5vD?e0+B?f?ZdF2--;ylP8T#GbVPAn7v~L0r_|y*(m0 zY@=(%)vtp|;@rZ^^t_c;)(($7Nd0M_Wd`}$QQvPg`H!j~?08(6zB8Dn^`0FpVR3N9 zt++?1f&d^NYX2i>1emzMw=nxL3-T#uf?|Snpm9WG;0KiFn;qe&CYmY5^FKH!am$72 z{Z{0<93VBHOMIT;w>g^5FOf*i@shsK6toVi~pg4@pn#Q~HQHWi8tFE$5^dE;>Oyn6qB zl}487eZ(Kwm$kUmaHS}&2cSRrxl{@B0bSOUVTw#SONwGZ#R(?ACybU=&r^FC{4}Q% z@wkCnIqo`wvv$@IKp@!9FR(-gar>KIf|N0rWJ`7*$~L(K(U0|QK+_hvFS>-hhtSE# z=r;p{03QxoCJKBW2VA?LNEcwv!mi_$0DSA|Ju!BGgzVsNy}f_0Fi6~w3mnOEr!b#( z`6|ZfJfTSRbO2d}kp(2QD=#A(Ri-=yzY`*`t5DxTLyz#GItA4~FLpY znRs~}KfB7H4+bmn@56Tnrw)jT>~7CJ@(?{gPZ+9|l$3rw*B`)-uAVX^?OQ+$UTs63 zE<*h?MyzhH%J8$I%nCXnX_2xjc_lyp{gc;MXl&S}R^_tg+T3#XmeF;pRs$DDE@%&?-d!d$-q#2C6|+@F-FQMoSk;k5CCr zg!Uh@;lJLc$KHbf!IH9&n*@6m)~hcQn@_2x)rO3)E^lc@uE;ZoBe%Aj)z1MA19o>6 z<2>nk!+{sLli*j^3KjR5H6ND;3aXd+PGnZvvuGtoDzcgMHoY$ zJ?=Pz-dDyxT|}N=#!1t8Jn|M^^zrUMO^UI$1cr9l{K_Dpf{6hW2Ks%>nOHj?W01l5 z5Mk*|^5?D>uZP@FXSaabt{+{S@pvo7_(|HCr5l7|Ui4sQlCJU-kw@ zeKz#`jGx!v67!=+udP+~bW*~(j~dMT%rEb7u@cxBTRV|OCL!)aN)Sn$DH?Q_7nC!` zvT-o^UGe`4+9;G?R;PsvbjW+;LpesG zqcB1CbT2OKXke!=gQIU5gj2?k!vxs1J1O#AD!vO|_^PBwi(BKv;{ytI4R9VoB|!q$ z;cvM@w!@JR%Md5%JUB_nj%Zt88-%p`!S$&HqC2~R5jeZd@rxvci)f^Y4Ifi5+{9Hx zfamec`uW8c3)R4^h6I%OYP(*C?719QCDHE)=sU#KS);vd9T)>t02Epjd17`RF}dI0 zKu}vW)e3rfep`7C=&BDOV@!(t#u8j;<-#frmnaFMg>+c5GVt>%d?1I~+h$f6n1(^n zTy!g$adC1!a?;O@q3_w8yW`vQZ--#;a=&|g*W-~ahwtG#7YdHAbu^{il5o{x zyG4Tk{!IlL=X;>S2qLH^5Rt*|moGp4oMe!6q2{B~^-9PDS4wyM};G3hxZnYz%ifF&x85p&(U{C7a~aLU^-NdXti! zMi$?#{~&Fm8;F&22b=r>WurLUFmfOYtIi^8EFW5P44?$O!zTXetkYgxtQuCGs`}G0CFVD0R-ZoVGQQsE zkKClsFavYi!G-*}v*@F1IrL8bE*PC7R`Uu=ud^`mp*pY}i}k34pe43I65-X=)s>5U zOk4qfV6iSc>R8Hz$5Pafv}pstLKfeDFcz_yEBSh&<46O89tp8{?(Oo;0%@!0K{E|# zM)m{b(94Os;8-IgNB5%!VZd6!O-q6o!%%ykm% ziUHbfFn0-6rg&|^cD$FMGKtg+M>FJB9))JC(X>e*Wk|RAb~6olmV9ksBggjGlI@tD zaQ0_296-oJjUV1>DXY_9!BO5;*$&$=a96SI+im__OTW0DS@xr~Ts#6+Wc)mu2UGYK zgZ<-)zB#kT5~XuOHe%raNYoo1(Z@zZ0MEbbsg;@}_-vubF6qY)1x^6Gv(CZ(TR?up zJT|g^SbAMvMnG3#ni#O_uLCo}BGX$+YxeK>WRIkyr^D574dKt^HP~jeV^V7rNsN?d zD>rZ4sNJ&GN@g;ar}P1J_ySRh;mCPcHF%*o$IBah1QrOrZ-@nw5|oRtqh7oPROp)u zIDYxQ1BVU;-krB+hOBG z)DqAQ+yun#`&WEl|4)2RKaFep6bKEpMJT$3K@1>h+h*TBnh4yO+o0&Jj;4BU(|p`T4IR<@?txcdj?^4o(JHZ|EirO;`1b zuqGtO$9I}DU$>jTm-HehXa9;Qg5_g{1J2wxkWqD!d*O=pov%L$NC6HX-Z-LJg_deu zg=)M-3n^s3FQzmUHeLJ;qPX*uBd85c7kXNJE=!=`x_New8;{rb7s%bYLZ3wwmb!}% zu<%>l#6=8=7=roy_Ls9Jo(Bwq?}-pv7#5i*KHDd6Ux%B0C$Nwbz0bZt`nDCV(CCS^ zNkZ^00HsSXUj%z9LWKP)3I4M!NswWiTUPq7Hx z6C`N%)6IvQI^p%aPLzE(FDjvSA-(5lNar9i5_p&(*$KrWE?GT?;on+oJ=M7I!PtvA zr1PWWF>qg0zcA@6*Dc@6$uFLyniaM01nYS<&eHdG?ay2a zzf%;H`Sk<&fUxyf$~5LD!xTnj@B>EQ{G|>TBu?(OOjj`oR$rKeB!XCkWU6l{r1i+g z*P$P)$@XA`aiKd>1x<~P0u0;f z;649o)&DW2>C0D?8a0FN8u|3RJvE@W`qF9JPFy!6Dh|mo!9(8Um4gOxd}_!<`)vbl zcjYN0>Kg3nQ|DBpxLMeOZpCP$Q^=RFLoTuHQH)2Zh#e-FiJ=e`0D+c9UUE%;(eX zW=3bVIX$Q3AdcQvbZ59)eLTQtdTQO}b0d(&XP490;Ep%h4r5g>i9aO{$GUm>W`^I= zf2xulTniF&5cl_xt+E=dy#%7ONoN)DwmHTGv2KLJ4(`BTnO=glJ8~!#MQIFc_R**H zIbAwjxh&tiR~IS&p1KUXq{{a=tqG_Wyi>AQ92Bt!r}88|LewQTA# z>9-L${zN1tmjT`n;kHv&HGI#^#3XAX6&VqcQgep%t3p6ZLfK{Jg1CtjI{$Q2?MY3( z2nZ_?thH2wm9%sc0~466H5@ykQzjWA#Pmm8kKPE>B-t56rdO#vC}=T1Rr7npt<4G- z_$16JWGjZH9v^N@fJ&TOcvzSgV9bn>t2ye)QqP+fztp}DpFu`6lozq(?XUQiMfyDV-mZr_{rlE!hl#VP7tSAO4ZrwNN!J#h|lnC0|qdC5?^^G;fzPgBrn_d2MMQdNUxl5 zHvS>zYXhbG?}f=Zx>YUH2x3Uf%e%SnrKRXL?S|_)`fmqllDDKqRt6d>NwFqYDqzN! z76uXZM+GBVeMya0=h&y`15N^yRWusSWsMf))Q8u$%ztxwmZjsqjX8N^&ta?@q-Xqd zx4|dLXd7S-lA^m<%gdaot)AZW1;dG7AP%OKfia|`;HTEYN)_F>F`xS*5y=h^$clt{ zsxJKGYGaBBAP&3723;`1S+DjoP`YENF+&{v&&|K!(jLfGNBjZvW)1JQ?skxaU@T!3 zxS(WZ3DM+rT&kRkmLu?Wh>|jM4DxaSa_(=MM%ZD8IVt#6UHcj(1-A8fd-~u^wW7!- z;)1ChFdV0&;O}75s|b*5HJY}_gTD|@slhal`#=C_2`nhYz7EO_%{JlR1e>}8xHlzT z3KR@7n9_qLgjYm!08Tk>g1*HZzA>A?J@s)-{~3fbR$hri1#;iQOhUi~%#7)*d8DQD zzo1h4k5P%6v}3P;>OF-WDX#Byn}Lw<;J(bs517D6{wrQgSQ^#>#Hd0%i-}}xJ)(Sf z!!`jE6G;ZmXcK^L4I@zqKHnrs=GKV>(glIsY><1H$?sR1rp=K7uL&&+NMDfl3C>xA zCT^%y5b@wIu6u8h4DVopke%}L)spQ!$ffFa7te<#r+kBy@2w%y_yctz6Y4soTVz^B zLC;{qm)yE7aRoRMj4-6Z_?@n0l_Mn25 zfZSCTJR5AbK?S(lul*^rhkJZ{WMi!bliZ4l(<+l(xX1j>_v;b9~;WS2Y3w7$qbzizQ>xTCmV z@S-|2@tpIKNj9 z3Z^(zWZZ4&ub%0Hq+pESY7hUb%Z=^|V(;S<-Z#cnzz;?Gw}Es39pXPzZKSV!mu)>U zEB+TieVL6qjvu*wn>fQwoU%Yw zPU^@k+Mr-`l5lihE>V7<3lhs*66o&j_GC!}L<0F5imMkMasWYh7?Sp;sRcYI`~ z>cJ?>k&@wKx`l^)^x!)7=;$;v3jO036Y5C8C45)FSn*RgSxS_oAMpDh7`^LzhOi_~ z9pW~BF$Tctis?N(2+os(hPZKlJ&D_4kZ2iGIP|1t%t|mzDyo=sCn! zusQIp)Hw*)^v2bt3&>a2*KhARqO)(G;o-xXhCADh*kAstS8~5}rS{OSQNyBrIDc%m z^qZR9_ugo;grbzv%6w7~z=%voB#?-cUC3$U~%e{{83Pj;Yc*G=8Eh#eVYZy*2R7QkCy_hjk3Qc zCUR|?*0CBkSl^0>FmGp})lbMPae=uJv#EH$N_OKNC@YhMo7m0ND3#>!AuATx4>J!CVa(4h9!FeDeD|_&D znAKwwqCECHPBs4C0G&s@JaiFuD>H+*XR%mH0Occ2GSZQb+xP~&N;qtgKq~^%I%bOZ z@-cbR&FR`)%&PC^NLedU*!taCb@i>TSo|&xJDU4ZGT;B`pM6_)4&*Y}{RSrY{qj_rRMfM^BBYSxc%ogSnR9jYqPmC`HZYuFpY$i zH-^>zi*p-Ce23-kf9^zzqobqssdX(DoBQX^om1s1*5@~qQqY_bj^s<9?EE_Xwu2(2 zoKa!e8AfBvTC5_TZ~Tyae1o|E>Hs0hDGyGc<}a>Kc8Hl%^N*)Fr*p8)Hxxl}gC|=8 zT0aJcQy(;K{wDZwyuP|>b1oVQiF@=v(eU?id3F_;L_H_P0u7|uU~_6`6oSpn505{N zW*atq$+8qTh=PDDs>VQ!9$UG1($e(Lq>kBFoz-nR@!7&@twZjER$Dob z>@m~i+(c@pq3{9;4v+Frz$@?gIP(C3w8^EuIN+BLi4kmIr%{BLH~j{BrikchCMqKV zfAQ61dA%AhG%G%V{NIriySSJYu4%aaE+mry2YL~@*1q#%vmikzt$aUl#je}1;Z2S& z+?>E`a0-2yPV$~MrSz!%L^F~EalqCSU!!%4}@Yd$GD3&Gt zmVPhPRYakqQ1opFU3)Q1H z{pHNLgwIDgH_n9QQS*i*SJD>VnJj6xphi|DhEWfm^%R1a2x()rGveLbuRl7Wp2!<3 zCYHWn*t2~&Xi%?#^IKMJ&9Z6#`bcS+T^wqP5-t&Q!qX3MbZ};iuh_lCFHB+`rm8A% zpBIuB6e644PREOQFZ+Fge=fMbJv^qw0jV{3$8$sK3H6TPu|iy~q}m-X4+{m|bOKoH z$mf%LZs-7IO8SWmgKqsS_AdEr&d^_AjoZ~HhUHanh1a0q(Ky-j1(-9`FMDRcCfSFW z(|*wa$;4i=!Pkki0HuI^ZZ}E0Lx&R>A$E-{`m5*6R+0u?`}S?Dsddaai%57vC)R|lSUxkPJz|6>9Q|nl zgHuc*p2nbSu@CNCnC;^q;H4;ydA`3`T#3IPxfy_ImLOQa)?g}m@ed2w-w^I2XR6Qy zgCoUeHxOWwV2{61Swlm3;>N#?k=HQXcQAz#TR+R5Jn8SK*4=GL6zciYF5*j>grqcA zr&#maT9^7RE`-iSj2$=AI%sHEKE_bD3%Lfas^Xfh(5lpN?0mA>K5_7|u*>+J#ckX_ zs+O&YRMA|wM%<#sna1I#l8${ysU$Emb%fm^a}MF(!mhinXNz6^wtz_USyS#+TbZIQ zku=_9zRR*DJSJOUKQEV}K4;_L#cjTeLxK@=_k|o|U&a0iFd6T+V0*UzZt%IV>;|f1 z3pWM@+<4=O%)qS{m%{{dS zY%s7JLityQPnT%wpi4w5)_8ac_x*|AJG#BnfBzH&h#Dg*u2bqV3pF6=>~yhSkHr9m zhRuV%kk(n*f%^YY?6#RC#$avJ8NhnBVpB--c48zm_)%F=Aq-dycX*>fI~IpKckUpT zh8@vg6%f=&ocSODJrJp+|C;ysua*2T;N%-gN%gw=rlzL6fB`U`z3EtH>KKru4YnCk zE9994O8*7Z*@*74wt%n=TY@=Q+ZqOqI*kvlb8KNz(_pQrtXwB6yNY}nyf!jb)g~)G z?w|RS;=b2lE0ucbUhF-_YyKhN+)+y8v~_26BJ#;0I=<}80l+*g=q+kQ0C^LE3Z@v! z2b(pvg-NxB8Ja(&%^#Hk=M_v^)}Bn(RP}_eTM$FQA0hi;*?iH&PnmfAQ*Mw)Ln{LY zcY$F_pda-uOYTBq^a0{6ls(5evU8- zw1qwuF>;EMbn0(?Hyl;!7RYe3Gv#N@!I~*8hw9Nr@4;^M*e9Vcp5mXy*!hZN`U2TX zk~XHzN2PMYMuQM~cwbI2Qi+r4ALc6Q;Vz48Y+{7PKn`{z7;EdXk_(2?zuF-BqnPug z3;E2_p_yhgVg%`M>En6e;Fr1l&K9G12q zUF62%9)E_h#bWQ@csbHZOnG0Pc|Zi(Wt2Eoh8lFd2R0`&F!=rzT@P%G0?*z+{ucTO zAP%FDMwZ*e1jkdK%J?J)uum71l=o3vby+`c=8Jx1I8F^ej5-?93c-QSrbd{2P_>i;9Y}d5#+xluZ%l9o28|dw59`E*9hoPv~OFB2dmkx!%_f zwDI|Yd%!}W#9T}IkLpK;lCN<4(@osTPvy#kghXb<98kh(fD-OskJ9{Xu*jBNw;o32 zyCh~`Vjsu2m~%|;#N~tbi1+c*MLGzJX5jf$*Hycl%oVGP+PY)X#Nx(S*c9l^m6u{h zB#~AaF^a0ij99Y*8Bvbdz+f!5*;5V>J+yXlb6dp4Po7k_3z1ryr*~VT{G_iRt;}EM z`k;!S@@h4!6PS+H#>qL@?a+b}rBB~ZTTUTzSKmepOIsyD?T^}%XTQ&2zFvc=e;2kS zNG178v|n?~y*%@Ga9m5*(m;DtQIUck%YDm;z{d-oQHmdO5Sd}Qs4do1&4yxH1By6s z+Cc#E-|nx_O=e3C^2tudKTSeHG_qcKV0#Zj&*t;<+eu0j(0rhfOSv`PTqL=W%F4$B zeiQi)xWO{NN#s5t$eQE~LvbA1loHCRaO2Kn>dZB}!KwXM(A^?!%>hzfo;8&tl$#!& zJ?mray=x~q1-*lVgY6~}KTi^}yH}V(I!&5F>zrwY&?|*P7qGN2AeW68$EG)xw&>>5 z5aWgc(awFRO0_f*UwXK z3x5mZ36;7iR&~VtjW2YQ81Kj@kn?vWu2cC!Nl1)$UkPY_oA=Ci6bm z>PgeJZ$sr34>@whHL;XQkN7EW)>rQisK2q;Hhi}6mu;5*(oxqt+>aWX6*j$33yl!5 z-?O6P#=i8!m(Ld*Zxi}CK5kys4fj-W`YIc0jwO2D4bb%p{~9yPUpqA5gd(rt9}Q4q^=)ZKupm^O0}H@=<7OaJGBlFXG8G2y zgFAf6luoL6*65a~0Zc5|a;>qr`H`e)&!$L|D z=}*kd_S*whB;o6!?vNXMP7bDe_)eAcGco2xejn@@9y?g+U#%|6w7uZEe>i-bPSblb z=rV%pW1*$_=4JYHw|??ybyoPDiXVf=&z*CmS%~T09-7P?Sv>dTW$^>U``5y!u&d{ffyK)eZYt zWtX(#{DOl|Pdx7bXX8SzUVTOKRgI0wP4pT(1co}y<+~AKs`X|L^k6yg+M3vY#TJ2b?G^WJ^ZvMXd^r@|89{EJ91Ne6JRdb;qqkZ^UM31C`m9QEus zUhB6+;G}#dGG4+?2SVWb265aW8FS@ieJ12=uWW>01vDWgvW2wi53zRa3eSwHx ze|^avPbhW;-Ul#rO7@=#-udfM!_A$8pTFMR7;6dxh=TEUq||`twQka*27dY#+1Y5O zUFi=}02(J^W82oUPH5wBto*A7^0YMu``RquIuINDe}UGP2xWhjXY~$hcV>dgu0q(& zoEgto`qtQD1aLrbe^;^qn;UU(Q~a=UNT2P6zl}4|vD8T0lON2{PNgkIzusO`nH_9@ zB(#|y<>x(ssXddiYtATb0;K1(npX}cs6 z5>SH$>?r(9_NZ1~LRs~kj)H-r$JEQTI^xH{w%tue(eA@-c=WH^%+lHJVkI3*Qr>T+ z3xR2GV<9diQ~|Jn<{q{74Kf%C-+rRxGN1;;t=)e237HbuP4?fZJiVLnz%^{FJK6;y#JYn6_$@cZ%Q3%%OGsU&c__}7S(A_A|2Xyk}*yyG%?OgTKfxlor<_AC_{S=HH^Ib$Zb(q4PP$U5}^pefCqk*X@^Lb&x3BMzxi6g=M?FpAVw#;paF`(|=l%n_wun;Q;hvheMmo$`W zUP1&diB$qjO^O`0Zy#{Q$z&=Y=FeU#viCNZ+5@fqo*c&YOs$(!j&9*0U0rA7L_|e{ z8QMUtqLagp*3BJ{8itO+jssT&>+Unym65pjrM$-utvTPSLa9%ynbG++HWm&;iC#SN zJO}xd6cm159KxN77%`+LjKAHm!=>EzuL(S3i(Du?A@3Vuv z3Tt+&=l@9>9_z$qjdYWp^)RN8oDE3NW|D313AH*(;jp4KM!DT+yX;j`d*`lWK3uZQ zOQ6&HucC%g7tot|fAtO6IW{)%yuIH|WH(D0rD%@@@pQLuTO3ebJefkn{D1q~G`0hE z41s-|P}O0|3*;st1WL20SR2{3lvkoG=f?@dm85az4fY8_os*&%zaxsS!);qhi1$kz z_+g~2K=&~>Jq|OXOv7^ddOzjKCy*6Dn8$S$QmkG3ha)4UfK)T*cEk6ABfH-s>+By; zhhN=^ZBlRiCv39PovRJTj(H&_a^#NNP%{&3D;jueuy3!qS!Zo8G0Uw{cM8j2ZX_24 zd6QL;2O8q?!>2=l6S;Uj=@nZ7ul^)U3A7NS{^2BH0Q<|d!wWN)Q&%_-Z)1V2>q~9O zg9n?zDj6IXTMn zP}a|T@Ysh*3Rp^Qc;dCS=y+_$^}-+w%>GMgKcMPp+j?`ceXyf<@DpfFv__*YqIVi~ zo<#|*O*4i*;X{hJy!kn zKuU)n9XaxdjYzg$1E9j~n=y7Xf(i!%9Z2~y;i5Vb#_kb(lr{R(cc?pUA#`WqbKzTZ zT^pt)g7qOB_16?h*C}T9{OUsEU}x6=&(iQ`X0&rPNu!>|AWO0V640^mKhy{O&ww(u z1Nc(qth~cW1`mc-FJ1V$A`%lFGI!8Zh%^bn2_FMB@U5I13|X8jEvMX-Zy^Qp)+;RFAWs1G^ zU-S5GB-!+>x8)f0&Oh=HX}!(0w(t1(Qr1?HEC3?z6TewwsR8vHI5AQLN!AyAO|Gs8 zh9j6kFv!F37uSa)_6f;|l6BJygFkv++MepXz33R-LOp(6-0=UCAzPNv5t zz=E8#)ZHzws64^J^9)-m>=Rc3ET-Xb2F%Wsa%&50o#fKVZq&P$oi5BG1Os?MWf8GR z2XfEi^tm!M-2TfsDwd=qDnWmE=8)yYnQ{1i%)wB^#*!k_$N4a4l8RatBEC=1quzB= zd#`Qs!b`!ZA3`CsJ`>_=gLW@~n;v6kFgHC(-eSlGPbUCD0QqJJcF}|20d#O-4CaFz zh9%{oeCBe6Osg+C4Z?!QLtG;WViuIWEU_4ySers^2DGP>b(Z4(F1B5#K z(Sn^T=D>{=`_Guc*#@~F>0UwHjCFlNO%?Jvh8AjKGi@?hYDkL#%24}s-|;fgzh#(` zsmV(pt?nPd>{ebZ95$`Q9yi&LP-)D`L3yG(j4cp+R6v>-46|SJ)Rp}bDve&8MZn>r zFS3tGd5{->VtRXX>h5|cS-aNiZLdMV)>)qh6X)~kw`aBFA`px{_MF~8T9!I zqMbCMe1+zfcqv8^g922L^gZDFuJzntvwN~Wv@2&pyX(!H-75lNidc2pJ{S1ECMwyv(f{eSXE^oxY%#caR2pcUS3w5!kh)HYt;Jo zsxm;zhZGV`+kQ)C?)U`N{W;S2ue$b6t~B zLicIT-M;MIfLbsqDk-rM_4z1gr?{V^bzwT(f1svkQc{SQN+;VU{_X9r*Ls6>H`Uur z6=|veN?xt!JfPOW3WE;}erYi09vDwl&T~2WK;(S{b)UVE zGv*<@$IGBM`QOxWMB0SwGmy3;hKVqbjwCl66c!ZZz&Q!e2XV79DaoWaBalH=y}czI z2u0zY4`_h$(rro*(WubC}v^Yixe^Yd9k2T|o1P8vhVxH0$v zK=%uZU_kF)B&sj=lunD>)^1aU_8u{B^=a&(jnA_2Ghdjs1HWTr|H<;!7+Lx$dy)uV zdqFRrkPUO4IMA_3crCT|^M1h|@%#vgCrnx*1HfQVh5>6Y4B>_{7Y5*m5ep~(a597S z4Ck~suO4s<8_}6p%fMvK&-u199}& zdvcv&3~&)uULGFXH64(O!=Tk{3;ms+_W1`EEi<~ojv!Hb`9GiAfxyq9WYHmMvOzKc z${<0Bh;h-UKT4oXM}3v-tNEb{OaL;n1C@^4zy6ts>|bIJz}N6}VwM$encb9{=zg+d`Q8Au>C zs#Q2%|J6?CC97E&@WRP4PQ!9~52ye%Jx0@WCPi zDeu@U2<5y{TrYF1;L#z7G~h+3y{O6Hci+tZl&y)47$YeZ0HT{ofP4kPo8`l$cxMLr zUNde!xc*YKzXwr0zQVZ%`PoPeBfMlx9mj8p2?=6{PzFJH0IE1gw0T7$pi8J67qtbD z!-Lg{XYBS2jMm6QQn?@QIGC|@5HCaBBk|=ewSs^qEy@9xweF$n7^Zdc1nR4><#LFb zxT4$yG0Rt4Z|M}dSIT3;#y!56w8r4#2ad>tP=N9;=g;Frt0p7!(8FGI0IzVy$a0+@08(Pvp+A-q>k9M@N+Pq zLv|F>DZxBQ9$0I`0MXia;I9}FB`mPzh&6`8?<*War+cp-c*z#9C>(&eYc-j*Je-W@ zg`jTauo#&63dJbnCqJr#@(n`e5XBUd{<{oI%I37YxinqWs`Mb# zx#Z0o@X2qP78K$LjXsk7FOwLQBue1MH4_elOz@crIeRlMdpu6U7XR`BMdqRvM<6`9wGyq_p_n)2wU^TnzbaI{d;>eGa^SBOyGe*6Z z^;N(}45mS?Kla%l7Y`5E2cccoCIE^7y+mSx6kbOgLGy!@3ee1m&ttxRI}cto?eO5= z2U>k276&Ua85oQPISCk~AU;V_FSXG0QodxtE9DN3j&>lRK`gT1$052k{9GQX+cT*! zV*Kki^!@GdK#?p`ehI3b!xB*lEJ9D9Q5Vaw0CUE=Q z)#wI1YZh=HD2V_Tr36Yj1RR*Y@dgN}u2T8O-FzjLj#bd!uJC6At`N|)bhhD1+d+Ma zM6o=+h$5a(mw|5!vq%!!UD34{9R7?oQH@YWN)_9Zke-a^1! z5F-YY0@*w91BL;0%*+U(PLOoFZlhvPglD7qI$PwK(C<(Im9Jw6KyyyI5@Knl9LTQ} zsfF&|$YbT;rmKW~NgjOEqTW@281~RE!Tgp;8VjaBAqx%jE!0B^6@J4NK8%rOcq=zv$6wG?dLFIr19kYN^0pt%fLiYIY9?aYi$8m-z zdJrw+$&G$xwz+=lotK}FXbl32!>TSr1}nl)O}@L=H|t{xn9g_`9QO_k(JLPa&`X5}FOUSwIqx4yYcI&Xgqs|&L$F2>rMpO6 zr)~)I+JKcdmD8om2+M6;{Bl2W#|m6XOmP3cx(mdq-IaR9{#r{)r7jng=^C#1N0E@q zYlKNAS@`oqamxe6=>e)VFF^V2(NJ{e`%(9N=?B+5i0(cO43J+}Zf?#@PeeCD9_oB~ z;!^JCKVYAQWo2dcN-WnCv6Tcy?!{0j0wT8HM5jd=D{6E+6LbR-}Sn&O;9sqpb#TK z(7sOM;0oQ<2GSaFY=HL71{{EpiC!vY8@e&Lrg!+*1-tQc``55mK8qn9q#$4SO<~lb z9)JW6(YIv^__)0@fG0zXhz2Ve(IYGoO$~Mj;MWkj^j%4~+FdfmDHvG8pVJ$=XfpE3 zb>8;}Yz^rQz|Jlb^k>L_l7tP@<4_{)BW!}Us8I+^L7|W;BUmF*>+J~=Lz#U5iLUNV zz~uq_9et1kK6=S7I3ae8He$)&@A_W=o1)DIuYukx7^hJcLHxqu_>RO36eJEtc@I~hd4tn3T6qO}i_Z>kyM|Ir?C*TQvf;qLtt@;B z(@5H~s$Et~Es`)|2gd?~+mZIL`|gjCMxv@S7(No67Y3&t=%vC@zP(l5^U7;xNd|+r zXa*gjZUuF#xUgm$1`dmddK#HaO4!k5$Vicop!XjXhu1rQrpKW~;sEF3;dJh2d<)gC zfI-iMYE3Fv9opgP+PSJAz0=M`ev2{HC!K0$y}ye%H!qxrR&DvR`3T-3T9OSOP~*h z<%8VvKz>7drx0vAFz$d88@)bQE-i?Ov;}~mB8b($Vgg`FfUFQLHwf*E+mqz-{28$q zm5J{WjlY8!qk>qk%a`nJVsy3}mD=Ep*M}b^$%5>)*I;%EfF(sRH6Y~bT_iW76rTcT zicp9TvT#reBw08Fjclg;D)F8He%;B7O$_}eM#vV0<%kuGn|LL3kgIg$`bQzj^?W{M zMLUk51iktn69@bI&%(p;1}Ly*P234C+Aor!B#UCa1mVb9OM4&uXiXj70hkl{y_<@! zXWKVs=-f1FgeaxR3gZi+3%R>D^^Khye#pX*J_%X&Gvbg1b! zewn)g_ z8BjJLgag8*aeOuzv~)C+AFM0umTrf;h*O%X6IFP8X9gub>VW5^n+OOPs?vBLAMor3Kao z6b=xRM2JYTp&ccWIfFQ!yF!!@GRNr1fSQGJ+JY66Wg1Y?O` z(uxZUbCGY6u0`(;kQk%@j^iK$*pN;d`RS}XsTYMbNJ!$;tKXK-pFc->(k8x}00Dyj zS>){(A;M&H8O6o;x6{C81Dlsn$&JU3%?DnIqOuPkRS0pJ;ZkAAfxOn&qARQW$pxE2 zdj?8rhHIc!U8Rj(a8zz}@Tgh#@700I>*llIrx)9&;Zy-u;ptQn5sQr+J!hciG^6B@ zwyC`2rn2r@1Tl;V|8J%xc!<;-T5}i<5rlC7#+-;}rslPe9aQy4dW1)9-eAXN z;$X^^I0PpIziLA4DwKzSx1<;HiixGUGlTjidK8gR&b$QW5sW<{@>j420;@^!uLp5i z7N=sp#{g#$L&*e&zf2jYa?+lOXY}pS-RJ{Ex<5I=nT>KfI~U`(*0Wh3JZ_S?Jb-EzE1~-+n9NMK>;=pwX-!^pieOVU+Vy}l4oGPCK(|z z^$M_ixbazFqXQ1#0qI=9uxD(B3)I&H{I@zU{Uhku-bUuB-nMTcE~YH{C)hS3YI7$b zSwP+vGDraNXl{S0#TkOXm4F1DbKq5P%vGH05GP{8@D#qK4&{e#*>U8y4?Gcr>-HbC z*C@%4IeY~!kxo^nfDSo#lOV+na0@d*T$#R{LB!1DXwC>H4$~vKYMDb+Fu+WkGa~cg zC94UqB~v!&NA3uJnOEW_^E$|n6ku74vNNG*FH6wEnFFd=k(3!+f3Z*@-j_^LF!Z^*Z_qL{Avb%4v<+nxF(2Ec$i)l z;OOC)-Tx3;2w}}a3#85;=9Nfs7~sBp5k}}FSfKhDt1YPq)g&hBKs9h+uT2%wWpMp( z-Xj5#S8xXHG90l`4vA=AgUE@E?E&s#+}=s)OM=U< zUJ>K+om@~z&Hee)k3spu5AdHNtKABaZ*6*F>aT^#5MsWjje#MXFEZf8a3tV$3TT5f zW)oB#NY*aE!1PN?69d7IhHf6ovflmNZKs|ML}127D4)L5Kgi&}-Z0R8Uk5u;*jYmP7e~URY3ri`3+dMK7rs6pJjG+wa@!I;u``f zTOebasLnO0tjUzb#hS|JDQmzTz?Swml6!o)^0D5^$M;^T8m)FH^cV`RY^c>S0jET>cI7Xv`RX|lHDSw^}NUKkl438&)&`@{`dA)y3@&j3ZaykfAS{_yc5;yRpETWduk z78xujBg@Tw0W|NjlGKYq}!t#p!Ks8`+)C`khJ*hR#zo9j8VV<99V z(G1?&PzTghSV{L7IJR$GQ>%hp+rlXbIn5l(L!`lHzkQBNz`y*s>{=GMlQkm>k%QY- zMJj9fK`HbEz8w!?b-&zz(Ypo37g%i}-!XLKy%P6^n-g8&e< z&bI=W2$39hR(|vCSwYCQL-AO)efp%BKLX!}Uzv_zJ68HCEiEk~aR3;bGq)LIuUs^A zjyZ9nI#g#g`q$19@FrxSQF9>r4Ac2rV5o+PLdpeD_iTIS$_&w<22sJwi8P-=$@q#? zo;6oi;ex6Gtv{ThBHWSRNV%hPq9A zr$!Os;5;7FNBoqB6Bs$t8J#}HHAcK&N77R8~WCA3rjTnR^nitga$?o*y>b z2?dJeG8yUU+M&<5y0~+i8_pQzD;1-|6^e+d_)`hLeEHICF9fE`U68x+yy;J4Uk$+wpifV<5ffhwlX(|K^E$mGWFo@5_m<^@WZncY*=4d$HKW!wq zsKeYF2=xd8*h)xB7E&bPUf(y898=`=+5j85locJ+;McFAYHU|@3qTKxK?#O*ibClf zFdjVk{d;tOzqYP|fizZWlFZ2C$iOe$9>mz)7!K9~SDD$ zDSg?Rdm!9b%2H2KQSx0Y z+d;@_?y^Ts;d=$2{6yDVcWXR#)-FaWDRHu<_FXWpvP#t!9qEMCkK^Ss@;#!hO&+o5 zPS+x^Qn`j2e=%Cty*K!X0bAOqbI(mI8h*L&?cjIHrV2dyO9S)1VG1me2&!(t`{Ob2 zFzgCHcmBp{7b899H{}Oq>^7*mcy5prDZFPo>Fr3IZv!J#PiOjP3Q`ypo2`;Kx&7%+~D&PCs}hi=-%2>2D0@gjB+X z8(zRy_wJo7AZ28;U%!8M#mL3JvA4IE5<$$XKl<%kZ<=tUEgn-e6=*YH(yezBTd#|T zJC;5c9k0c4^=GXAeIFfSs-*9>=R50A6ywRl@k)dnzjM2 z-s$`GIQ}|rHzf$ z)fcg`n!R+3yy%GD^&>xt-{Teb|1F~V*hzR&gsFXD#jhUq)@&nn?tF^beGHVj&%~WW zc>B+~~iHV7qR#y`> z48Qlg+=PNa`N4y02|aAhC%>*CR_tj)cL-oe_2ca<(qkrp3wjwZUn-wwP2H0DYv0pZ z^z#APN}x?gXWCm$VA)lSXIVlwXf3TYHlkM8*u zmfo3Q71-aWz0k9`;~}##u($)Ch}z(b21g>WYRP_;$)Eq8s~m)njTltoyS?VBD7!PX zHr3KiO@JGcqNc}@NZ!_|N5{mM1RfHRUJ@^quCO$!tnGAkgciDF#bi2J#GCVn185;Z zPKNdo_q@U!BmVAknqb)LPGm+4i8e%G`_p@W)qVdf#mo(?;Ng6L9X_O>QaI8|_qU5i z_dZ9xJyffR$)-6kJ;`&dgO@WWD&76?0#aRW0k(m3K7psJR+_S)ba0RYXW*1kJ5>7} zIAJKm*DIR-w1dss^KiyT_%P#okss|nW)VIgBT3Q%?>(%k?HG^Xg!(=*c~$Xo-_%!% z5tMeb!B`|@o4H0~;o;$1Z?jK{Pz$3ZGRUuEFnLkTKt$fjZn11#FzLrA?b8eQT#=HX z^YHM!*gI^dN1_LCkX1?98NL`kttopsg|h+&CPgGROyb086jlMl;q1zKKZ=@RHR}n<%Dq9vssxS($UiY|MT50C-#Vq2P9K(Bd_TU9o2|IEma<(^ z@J>R138QSztkjA}!+xo*T$|rFa*S9sD_dZWPDVpvAr5%3rM?q=@MOcTI;}9^OV;e$ zijtD}zXiOC7>SM7qpjUvK?y;wSF0)lCuQ?);WhQ+N9Gp%mrep(3Ldzxq2RQ05X$Rp zCwxTjoiRE-o_44Cw}<##j(};`V{5a(7ON;dEiEl(DuN66HWK0#9BzawA-dk~(3HOO9lf2TWIYq5NEh7MAJ@kWu3^U9n0AMKz*e4dGZ z;6HyExnZ8azjQ>Mwm=&C$>w?Dl1GEv55L8^TR}&Oh0_o+MG5aI`?Uwyaz8%{pDwvx zq;nyHJSirI1cE!~o)Yr60$G2}S!I2FUGLNh)?!Dk^T;M}UO(d$Nyy_msc%cFhK}*< znL=7oQ4t5CrDbPjkz>hbD6PoW!odD8(n;$&rKAX_QFfuS;p@@ulccDI9 zfrX9Y0tRY75RPeEH#ahZpXqsr_49-)-5HRsbO>Pgy?1l!Gx8#A1Y^=5f z>&1mjn{?6s<82yZ^eOw4k)fRv^>-hm62Hd^_lse{!($3ochl3To@PuRBrgs6<(nGi zs&s`sIy%}Kn%{!^QD=cL7y{B|4f_y(k__!3-oniOZb*@*R#8=TZ^OAX3*xrA%*x{; zi>oUA!I-l@z5R%vQcQvoLneNNT~ns`;3QH!jA*Zb^m_}g+D~aupB{i3&e>fBF8S$= zz;JLFMZw}T#QpoQ?`nPP;!fM*j!kD2wLno0&!4?VTOmv*auv%S4V&;UsA^*(5GxgS z<~rQry|`>;fLDzk>Tq z@_-!@>T|>%eL`0SOTaiRu340P)aCYABT0uXnv6<*Y@gmpk zdz%g4AFaNji2V66V6V)o?)&Ab!IGfpZw=G;HPi10e6WjIkNT}7NAKx+qY4r)Y~gVD zNBIhZ?vvj)TH27B!H(z^xHkL4mnVzl_VG6n-I(^GL`*=$#JHj+Q_Vdt+=t8Jy*dBd z$Acfn@7-7|1OMJ3r?r=P+xM&8^6`3D5Rl$AlN#uAFq%h0OBN2@#n*)2IL5aVp(PWL zk%u_ggIZ}+CKXM3?$`ct^&GgHz#!&cjvG%3JvX(~c-c{5Oph z_){mm$~8JRGc_F?89~m(21b<0y1nef{YEh09`XV&@Mo_hgtu+9_ng>~oHD9#MXOs@nnveWRI!8O#$akVY~;?y z-4geOGiw2^_-4JRML7&}IE5NyM!IU9*iduk?o9#ZyZfNXm;ATrKt;fc!F>m!EiE9y z9!`5->K9+kOeh=ZqVi?@IxbKYN6PTLGZb}Xb-riyexOO|lDagS~a{ z>AnoWo_ep{>YUcGu1f-nRj znj$hG9w$pe5v9zz1)C9#;1AkO7K?LGM_i0oM2Y>3VBLh?5))-=YHCmYuDDpW-_AZv z`zks?mo_JdPew@SsgnrL3GGv|08-eNOUuho`hF=0&~(7ZWj~EZ3sQXIvFu7qXk>FW z3t>g&P1wj*KmgC2o3RoGYkUOzivgK;QVG+d0=rAO%7&3*GI?W`_5FtS{x4r{3l!>Ev zDjKg@G*ZD_lEf?jZFL$uHz3FQT&e|e`my|&TaHF63z+mJ@q(eqV`{Z;-o<0`4|Wnk ztf5QLm46B9r+r@HXaSl8T8!C-#aD^fq13R3?=t%Yk6_DOGHuJ3{)rrF%;I6zQuugQ z&?GFAFqoO0%eY(d1U=sdW;(p$-(j>y)yU`)Xsc~X7&~}OB;MQw2?pdAHku>nZsuPg zZ>h!I{ez$y>OKbS2U{juFpo?|K!ADKB|GS>VaZj;tdk}LnMA!Sk}~Z z85S8Hy_+0)xZ0|%P9Z~ZVXY8q$Rt;Xp4WrcPNSlCDjTb(7n@3oBTJh#u_xkbtzhTm z4Yus5p0*>?BYMLI7l`#-xm@{X<1&}!T(MhrbGQx{o3oE)<=9WJM4AeNcC06Ix^;Xp zmn!9hi>|)>Vhaq;MS(AiUydi>#P2L&0z|PudHaFmveWnMFAnTKi!U*XoohVdw0IG9 z4+j&Y0nepB&iIvf@zwRE?GG7tR4-Ht$8o4v07$rV0!0DXyX%X@G}d}T&uE?7#<_8x zW)~B`(EyL`)#E>raN?uleqOY!thfL59x4*rYj((3F`Zmp zYhQ;m`Hb86LK^vVksEMpu=mP2dVDK4p^$cOFn4lsX$K^J3rKCk13SoSe5+qRT%rfo zj&BQyFcM{Fp;>>&PNC3`e>;7=-?=#J86Z@bV7%etX`GuF-?H3EzJp51Dn=ZY&|~aS z^wu77$AS^9W}nX+9#X2zY*?;8p)9R-!$g`jUn&0Y8DPt2O4WiC0^ogs8T?(CS8^*2 z&K8!1N`NQdBXizqHy$uHHZ@(9BFv)elsbTGCAX}M5T+k~z8;D!gYL}nUdux}4qn6f z9eLiugZ3S->=G-RO5-~%j8nB6iy9vVJ0CHfxZWBX<`(lA?7sJ*SdX0K;|rygEa#_g zF_WyEDEmK?Q;vQnMUTk^)<0Lzq_Zcg6S|wP1||)!TAPx~>s^U&1;gkVG~?sX9+`c= z63#Y-H!2~K4?Y7t)Fids7$t!(<@zfL&BbbLPhY)aB#hvqo|p{~64KYG&f#pmLm!YBG#m1L{~R7m|V089v-5A65y?2_}C;PG&rCR48P;5*3T*XJd@_n z{3>^G=2LKJ%{zAcTz%g6UkxWr{r2^hh6kXFY~0oa}ig;`)a)=r}rV zhNxL22m!2h0PS=KX%ZHfen}L1UhH?8owb(yNCyq;tYUG~<|FW9prNHj072E8MwJ(^ zF@F-B{k+r?w-W@NL@OYO(2b-v5B-4NA7!Y#3$#o33I{3^kiP97zdTHoaF&g4r3)#` zF$vIer%OM+j{L^j`g$xbF?n#J3DouG!xYZ)*n^a?9AFBDlhP0b||ftT&EXA>k-=NLY)bcl{221aLl#;q^Z7!?Q*~V^6erm@-HM7 z@{zpL=5vduPSWH}&dhEL@v0Zahg}to25gW03-__6@+c%XE(DWB_x1Jt5_{|w3YbBa zw)odabP`wcSux7bl=2#J6QJ@CdEk5RDKT-YFP~1@%oDlR_B$-bZVl!NY>TC`Ur1iV z1hWPBj^Ku1@|lR)!W;bbymTAdT&GH>tM?yCue8!L6yz6zo|X;Na+jvTfN21%*-Y_-jTqVHD7{Gk(d!p zKb8DBG8i`eeQV&=DNe<92NkzKM3CRo!UVchpnOBwF>Rye?d(|Zx19Q9y0u>rTg>!C zprh^;@vQde{Ehg)bk(@YB3(#k!2U$aQh4C4dr`a&_)tx56@C#TsM1#^PZ}jlpyX(7qdy$G`-@bA9A zf6+AcxVCj!^{B<;n38QtO&R<~Bj|YDwmL=hj&&dT=G8cCI;I0G)!p6-_ z=g5TTiv5i@U;De0WAp@s+M^7k&8yoDnaNs#SaMU;Fj%ZnRepbysaO^(F*6SuZ%O+9 z0|y$z=0*qw8oSMdmU9a)VbHvX17ti8pX}lnXyq!Jk6cPZf&hTH^v3smGhDXgUi~zb zhSz7|fJcM8!TpFp>{=)nMq*|AS@aW-^Qu-H~j-oD|#G`TLh37#(^xHz+Z}#6 zKTn~RBqs^4hJ*8YjiflSR5rW)UfL8|hp7dcQhOoVk`DXNY@Fdobl6R6sT`rJ_>nPO z@mLC--gePq6OE*hKkjkSKX}v>731=$RQ;-3P%5T6r9AvSr51` z;^VmqPWPrfRu(`3yob4%YOwHpPSTDLN0EU}e?rbaN8JR=PBP+XxO|C7NP0z~I)I+I zBNvxI_5%X@gE|2TIQm2)skJM$HnzI7)Cw!E8oU7gVJ3U~Q~&DJy&)$m=)<}YB=KIt zOZf^sZ!)0lwnnng^h4>^sjGd>L0JUekvh7%EZh$mWAWf90uURB3EtsV=sqq|h@<1Q z^Eng&Fm&&Y=2psw&+3wn1$A0Gn)zX<_*%wc8Txbo06sRR-t=>O&x}A^iM`^alxH&= zs(cqefdB(>o9Xk%R z*CiO~G67_T;}L4K>ZSL3@wEFd>0^-B%--n5Zw=N!8h2v{y$Zta!CI+kH7u2W$}SlT zNw5Skv8cSbc?byiw{M03eBm3v+fjo85s<8*>FKM~?-2}OWMp2y&BHZSgcI{0IvL`U zgD6#3pIyG+3b5OAn0lr{cI@e(B59Z)E*$tmmO-c9A0Us=7q;Xb98*;ce2%4PL%?4d zW9^_%Xa}-}*p6UX(Wm-akqV#rdot*6@xwm}bRnrohqEE10wN-z$OR!#q_dLTb@#zU z4NezTsU$Qr^q!vuQNb{0#Y~3Zz2eAsF)`TzdVM%Fbqr4`8&`N5@P6BjR$rF53|2$!lfX2R&{ z6ZNn~D9L9-4klpjsXw$;<}Nj+3>Ikj0A$Cs6xC@f_#CU=eO_g5%L%xNNIX2eYKUEo z@VtY^C?YAI`O_~Wu3vC$zz^I3JYu0;P^EVdU9*ccj75il>*5^T-aKSdj72JExL=n~NR@Y+?c}y^JX%NslgjXrR(I|UaJHyV zm+`Ur*v(1B7swhOPQraU0(N<_|Xq*_(w-$)#>QX`WJ0R%q^!07c{DYw7&|KC$&aQahIqLuFAp&ay$yi?t z&h?R?Ww!1~U`GaJfRjT)Tl|lfi&MHGv;aJ5t-(|lv4n{h6q^gBEa;8(@pzH0ufS;~ zUq-$&o|1AA3H$Qkk6rwT3ZoLf`WcTZbID8MszC_U$I4BgWcVA8s4GXlf9JdNc+XM# zTXZ>c{lb_Jg`ZGS+^Zen5dKM?Y6%SJ8GeJP-zgPx$>8|liH`^M0z&EQzuVi|Kk#`X z_Silo`|SiDf{2QU)Q))ZHP=(-Jn1RDef0LXYZdYDZBH|aER}$OkDa=FI4L5kZI(Te zXwMcwTtq914EaFY932yLaoG^w{~Tr9ja>1|KKzmIvMj8oIy$gn3nR;3*LDoO+-oDE zLXV#!8q8yGe zeJQX1<}a36FD(v7nXCKd)k&;Yn6z8?II#E$tSx@+fIA$CUHK#xurF-~%?HvSfOD$R z81QI5q60l2wWC?_sMOI?J^~_Y?C${`Tgtn#9&YU=x6yxz3F*SgZK;H)Az z9-y*I>+8v1`Rt7qseU~h*@0S2fjpog# z#dItY-t@#k;X8inZ<^hi_1ydG?_r@LQv1t8yaB9Wq#wNCj&Lj*afT(;Q@Sg;@qsly z0aoO4BCa}+(`v;dm;xp*q7Ebk1)o4oUF^jEYHZZ*mVkWK?3QnV5OtZFJD*;OzWUV* zMUvzw3N`=)<^rk-4205^1Zb!>bN3{K(usftx*B>8dIQZ)ku!w$4`n06p9Kj)*wlI# zFpRrq2Lm)A0LWhsY+;&ky$EzmRrKZ9v5uftp5PwG??9QG^E+Y~VdMJ0!e(0bqFeSd zeSi5wjK#6yK|Nq#aGS#Q3r@*gyu69?;;9n@N>^;s%Y3N4i?T4ncudw>{x5X^4SPEc z>m5c2MT~?$DROrl0(}q(0McaMPOhj_n#!z4gMeKZ5f~o*)9usmfLRX%7{LmIYc+qv z571d1t{AmMou6%-r!cL zlpH%yhajj>1pd3aIvW@eHm-Fd2lWAQ&a6_^By}IYr=*T~TTj|k2RsH3#xXcrK+xkJ z)pKj;-UKT{n>8796@bn@9tu461N{epp**Gfg#-Dx1jkUs#sFukHd+PsIvlO;-QL_r zC}mwHI2nQK1gx`~tKGoG6@`E4iEaUKL~xHs$Hrd5cq7M*YMF_J^BE{S*l3ZST@h)` zs;AA+(trqtLJ&$08jx1IATTVAr{2DY0+}RYuPm7w3Dzo0n4%tDmtg+dGc=I2IO64{z8bZe;Zs41{?m#@ z+5s4^{`QfSs;B<$!>1B_`&;iq=71gWjG#{dW!nFzJs59o4KD0DY4l#*1dRw5*Vhim zz#1REzpVG85eSbEW!w_@(M@V{a#HDII&}_=$;ALg2Fhz2e}8(oZuy`QkF;GOMj@lx zH@>@JMGGomaxQ*KQ_!x{J{AWh(;Xax%Bnv=#`hqG7>oTzqb z+RK+i(DUTsHgFxl?*PeUWLSvYru<8Q4YZ~A^febk>W-G#D3+Wx*(k2K?7w*6ulTOM zu5Jz{rs_}$XJ>)#8M}2C2>PXVw3PC7=MaQ2b{3F^Qds>^6`?qIg>=&`Jy}WMrUqgV ze5lnc+6;^t69*PMd(2@>Fq%8b?)S=7h6>mp7}yZ^(P`VB&tj8JV$8iMA3(}j$?zWj z(4RE$r61$l;shp+I?`B(Tz)2#ci{MAOsRziR4;R>So&g$9_O^FXu*;8sKU<=<0 zjCqvCzRDGH&Fo+WrRZhTOr}RT+3OU{Y>Db%yltKr>h*Q~A`xz(M)Jp*ljg0**M)?x z7_9m7Yr@4hgot&-XYNC%RiaM-iyVN*dMd}kdHhxiTIPeWXX~ z0)=l)=>$j#%PJZP5ZO|&!VwH?V zmbM)%S?C)QI115m2Wl4B3BKzGkLJQI{X}SZv3qhh$rG!4SLj}tzKXPHvU8jYewwJE zQ*f|MU}_#vuPRoXJXuUVQNnjKK_+(R_!PE&$iO1yn+w(+ol}4OCo9c>#cOVs(R6ur zy=W~d%4=y}v>ODSKbQbV`ZrUh`#5g)@Z~E-jfCc4uZzl0uV73vb#>U69M3v-)-2X2BtNM^pd0>#Z zEFn-6MK(>eqJc`33y!~F7rn_D&0T;^Yv%?rrSl+PqX@surLT?2jDidS3gpd33%*~i zXd;VL+Wag$phleo&?wXClIlgyrEuAk+$4uA*`Q{z((@hJb5`D`SXfx^7ZNm4RP^C+ zlIr@N18}(Y-0DPue#lMCRXuf1BX64J;IBUo6VoMaG`w06owAM1YVc;=AeI54vvZ_Z zhpiw~BP_AtNUnH=k(JX#6T=ET5zu zwk+RwGD$eGb*W{|1Psk zo`#jTiw1Es1}F`#226S{wlHT&?DCAhxh-EXfV+pmL^VF|Eqdb$@MMOcY1?w%%0NZz zXE?dLV_y)mGqAd63#oz11Cl^lKy%sqZSuJo!pA41I9+q5heJXrW7?}g@p3*xqiyQ9 z`!+#)!nu~J5?Rpd))&MA^rqPLoSyOB1**~q@nZKE_sJqsF$bV5fP!UdX(?C`7j|6t zFAoaQnoBoxs#wr^satjR8=y{(5(j3Oeu`MD_=YoQg2s|6qaPbGI*nMbcTo-B;A$ekGkL!}6m_sjEffgLQ24uHkjDB4FK*%5q?MZDy}xE0(J z>o;+r!C#;bUZ~lCDU66I9Y(7aHALDd%sZ|?7{4-IY%A{qg$ z5(6#oahY^!`+-l4I>>Z?{dnlZ_+mkjC zfS@2t2D1oHxhHkXz!P@Whu^HM6PC)n=??wZ6+%Vq2y7F<);|Fy;qqq5&gWuD)n)G+ zug7wa50o{Qq1Kw8pBG|U8=H04%KcgDS3*%&tIxw;w&D{LA+6W$^h_q(RAe6?XQ2LW zrvH$Sv@}1_b#KfYC?I2@vCt*0 z`3F?oTX0fQ*Qqo6LJR!!2_9or1kH9(26{eTJZBNN;YzyJKm7<`KRDR~j^eTRt{PXh z0($(eFP#ztdcNt7H4+wqlF&f73JAm7XhIHtJie7FQL5guKY14)H>90&^Z1lvYu2xe zX3h^gMBabryBzM#6FKbKP06G z2ncR)0B=6!$I3*(%?#Zvz1N`p;i}cS3kDOmbSoP=K+QX#`0DWUgGZntA-emjP&*L7zwrf{qjJgzr`nRJsX;LU${PZ4-V$xr#zk6 zKr3Ie2!$Ue0>fb9-%cdgYeyEOHzMt10l|Q;Cmgh0!81Ap?c#I%U#8uIGDb8=7f!)2 zI2%&d)?i*?ub=2oh=ow)Re=b zq6ZlhyXVVz>VPbJp-9jf!GDe>jaLZb%xNUF)Pa--6a`7CsjOu9YNpA7mi{UkeSnqx zqSZw|%?X=TQ#A|5e2)ORWJI{^dhL&YZUI39dn0XY81JX=N1`U$FwZY- zR(|+k{znp;%MQw0wFLtht*}hgf0 z(gqL;AuAMUhKt;|$$^dh2G&D3^>OYDTM~8=>0{)QG&_D|t6#lOq5DtaW z)zYSNrx)ldKN&F^nVIn}yB-}yGbTkPEg4CFJ9;blEy_wtmABaQF%T*UYaJP}RDIvs zS>DJfH98BIdjAYobfmW69v3}{8NGrXfwOwNnv&tD`sTN;yQFXvF@-tXXsz0XDi4qv z*5(Z|VbEFsGT#(mcCm%pUl{O(n)0aBs1+SSe@8mH#+zTRsQJ_QU%-tNm>GW6*-VodW{z_tMx0TF>CC5 z60Xr-Lgc0Yk;#yX;KJqH&i0+ z$f5J+&;93tc&O)rDD^eC&=NF8r>34)2A!^YXgfLI*cbRG>hKIs#kzJCM2n@zgcQWbl0uN5f?!mPEPpgUhDXvbbTw6^jJyVj(}#7gn# zzTZd7&)Kp}ZPtSn0oF{14X|4PyyYDxHiEJVh4WN|l1{vJQ~{;x|g20QdkERg;(A#u#eDc!N0C1{5We+`a~hCwGv1<(_P zs!3(aPIygaB`E;0FibDR_-Kj6-QLv9tQtb%zr$)$?&m)jSqw?80fYxCH|Ps$O67An9TChB0U3CH*}vdkSXBkNZA{D`5dtIv@E`Q4@&3b(;+roKI_&>3 z^&a3{wr||{mnc+5MiPlAN?9eVl5A2oMHGd$nXF_~Mnp1-$PQVNP(s;dE2EGRLXi;( z|IgL$dH?Tw9MAFe9M6&Bdtdi?o#$tr6rn+A0o&p7u=naYZ7qq|>P^jhX zB3_EGy!I2thX+g1-R2r={dlVH=6>w*nnD93qmwb$m$)Z>7R5bC!2pb(=dAjp(^f$l zRdN}(48EVL>2B&a9=e@Trl0$XgYovzV@I~ip2xA~1%&JXA?d0JG5xo zv7*2-eQ`@jwct@JF{No1Gcy;{vnTAVb<+Ec8$j^` z<}g%(Kvt#$GxwDAfOOh59bUsO{A5)bp6VLQbz~m^Am--{vTLi@9A#f7JljSEH+&`F zyO6>j*R`OU#k{=VUI*6%$@kZ|mA^Z(UiLF^>ZPU8vT--S5%hHB*xK7~qhRnw+t+hr z#lueE?bjUBkX=3F(}aG62##7|G~iej=c~WJ>Zy-oN7@u76Jx`NMGWtyR^6n)-WJHe zDQIV`LxW77ncOVCAc59B27pS$5{Tp&cgTz5Mq|D^I|;@U9de%e+C-;o*G?Ly7xd5> zJ)~E0(YkP9S5UP6z{iI0?gR3D4GSr6b@~cMYOKFxtU%!4ouaEb1981k2vS= zp$pk|^=N8q#5CvGK8Jywdmoh6RyMivodEPKnN)ihG~#-PoJ zzQBf$o!dhOUIkU2N$4(zzm5rKm|rw#nbVS{My&3gOuuspt^3tI_CF+a--^fJqMw`; z1A;2};C|!dY2V(%bBAyk3kuq))fOJNSEn6)E8g^q$=fo;(auyddL5c4YCX40) z^p9qh+!R7T`M0ChhVK9yoo^=XM**=jT%PD%UJv}o=P#Y4nIGc|j@5%w@%K0iz#@_< zLbP-3|V|KO@71 zEw}5j3R$V8)(%V=@8!fBl7qc~fg?Qa<@Aa!BJF*`ZO$#~J4|9m2s+EVji-ujr(y2; zi9QU2HO?u$-194%esVCdh{TV<*7jVQ$1y1dq9kPag|uV=$A*sC$|QN>`}Z)nLe;I# z91~5!A5r@*$@DP39M@S}A|7_!++0-f_Ut~Y8u>G))w=h8nnq)BQP>@)EUBNoiU~ohL;Z!`G z7Y!5k$ps z=WY|{F&C{&b8mowEj770p*hX#dWzeWsmd8g9U87|OVLmD;k$aQ#W?eo^o?T!b@o^W zDd|E4>9;YtRg->?@zJ}Mw)-1yLQV$n#dX;17!k%`pDmmI3vW4?^3!;x5kCd8S7}9b zoTc5Ta;vt$vg<4kqq0m7kRf)5|4E0dc-1U}!tW;`zk5>^=V!;(qa0_ZF*XlQ1Lm3F zUlZOi-{?6f`3lU{eOx^jjo+w z#C@b?K^qsUH;-Y*|2fWYa@z#HzaW@U{|@dr6gn`fWmlO8mDX(@XHG)r6!oKod3_pt z;JCUZiaRz5ZuJUIpWD2i9sN1Fy*|~y3Q|+6?MG`p@0R363zF1sV>4p86E&OyDcMwzuXVX86M$Jy{&z9=YtJ9 z9_^abXSIF=jvNXDjvxNww7Xejv+irBHg|T~3Y%zF_cBV&8tdkV^hs?A37mfRb0dY6 zU;Uka7}XdGmEsz&BR2CQpKWTZEud1I0!`(8783R;!#fVMQmwwLyi@VXJ+F$ogEM&Tb5D8CQN=&=nE##LaGNM zc3Ko4b6h>z_m9-xYMDU0j@ag|U%B=<-?@%xjDMlMy9$jo99x7AN9_Vfj#(YdUqSwp zSe?rQ8QWvV+?q4y70Ov2@4H~w95u%7kiu%Ksw+3oB~3+pdlWIJBGJVut*50fe-_I4 z2#FcDgzRs?S1>LxTOX*%CFvLIYTM!YfZ@2e8`$3Sx zF!-CKnKVocRD$noL>9jN-b19mUgOLwYeQjd+6_%spw<-XX75;VrxPm>SNxTo{!j)@hk=9I2ysDn z9A3lYi*yEFas!+ij5-Bp!aqR^h*NwD!aql+T%eQNR5aq^-?c?y7ew@@r3VX6@~H8->dSA?VGWW;T6&)#tr- z5}N)}HO+F*QgPZ12Qt(soS8u$aC%zdi)`1lJ9iaRai-e{&D4Vw5IZ zZ0_xSuDetimt8ofTLR&FlKrBa=VL(V5Zx5&9b8`@tPLMwWc`!p&-Gf0)VjVgy!`!ASSI_n{(`?Hctqq;n z0_(T=@NJVux%Bgq-zAUVdD$#HTrf`a2fx`eSY|aKyO`v8MTuxOdm?z60-DAL?@>Xf zXdWqyD8z;L-V97hgPGEhZRx!DF)wzB_)bNLixdwk-ORL_h5$4KWF#6;;jTzQ^gl%pC_75;I zbzS!VwJi)Vlz2FWj?#d-A;>ZG@dlr->`bI`*7FNN`S&f$Tg3O!J(k=LX z|7b^|j2BDUdO})OHUL27nSrXi#1k!U-M~#YiD2-6f->w^@~n!Nx89eYo;?|9e=RTn zZQx!ec0RyN_~-ko1!(GVrOZD*$t7 zKCI`sGJ%|7^NP?@Q|+r(!%ZV|Gqd^6+5DC6H4XPHn zj$qRRAXx>V3ev^zpTJngLFHNh~)#rqI4t| z@1k3lad&l7*7WH@-MVqSpU{V=B_Ip>nUZh5hHQ3fhN8~hSX-lFrs5XX*a@(p!ms5H z9^6wAwpT?9Q`LAXi?nj0&fdLyx1w5z%b&Vn21P%9+yOlMFQYG;T;ypLoc8VN5ygj8 z8*qN^{7LzrD;Xy%(v47+(X;8AWP}&XFP2X=J;<7I8h#&bDI8F?ayVImx0}j>Y9CkT z_wy#jM;SQ3do$8B$R_kta3$<>(9I#{G;!IJ#C1mAs6f4G-01(Jxi)WpD;Q}CI~afm z8BV?9+lj+WS^l`39Dx)llxv^uZs)&24M1GOSG>o}sSYrwhdoqMHBa3=X-WY$6n04@ z_mS3iA}Y&Q8~FiRkoLG5RSC`xN_Ud)g}q#r`%9~_BV zzKOxlSBywLD+Z?eE|@QJf%8!mKWtc3jC)HUxsvlj04s$>E5FxAJlXG46oaIc)Hy0% zXVpgr5ov$FfT;*>0zbZUVg65JDZkdesx(ZE8;ShT?pDiazL$VpOqNXi|x^% zr&$<>5c5RztpvRc^q^Bl+qV^&v3Bn3XtPAN1&w=0#uYoyaE&iiTe-G$Ir<5I zP$%kIc|DNT5ZNa{4Du~+Py1%Z+k&xmj9xLSww+}_*nt3!z{;fW?87&+!jj|GFV>7)(9{eZx)J)A6*cna*`87TgO?!Gizq5z zL|R#7C558Qz5h}hBc$RIsq^kD2AeDhvUL7Gn}TK5L8H&${!p9wb^NylOH&hrNu>-l zM^c3)CEW@7+OL^Xe!Y*CGm>6UtWLOB#1TF`j`aR{VMs!N|G zBvj<3zk;psG~`XBCgdR3L;YmGjQ-_Mmi1~i_?&Zb#MXwIsqP#1I30ewITW)wAoale zm)A&`E*0(ypO%KWU~0?!;gM`m5YpjerXkDf%81U{moQho{w*-e1$1JigV&NME1slP}GUE!l}X_NokE*8gWUvmF1Sw3ka6 z5RkH^k7*#-a2B~XmRLj%soIZj^my?I*!S#Kp#?UY#8HJHT7>TlR%DX7) zV_%_4<_QL{-`N9lU1r>wN^!SR2UPKR(okQV!7JY))%q+*yx+IPbdT_L^4|997#ZEp z$S&pTU1+#=mGHPIVwHV;@utIQ<^{=hp&7v8P`fbDkk3b%ui9UFvlr$G`vVg(T`~fn zPZ=7Xdph}UY0gM??(g{Yg6|!zt8d%?paU`27`9)8UnUpNw4UKABz6uC&nf(}A5vrN z8G78OkLu`(7RZ)J-qY8ZFzvm9Fqngj(9)8j{f#mCbE9$U2cj)Ms*ycMEMcqnE~O@F*grE+An|Uk*B$BRr=fQ6;1a<3VyK0 zqCn%q%Vs(cgu@bSpBz_v33!lsPh`fOF$srnS__v6iTaXHi*G=Z=mc^ssGzdB3sne1 zq3`diF*I6Vy~Gq1FI>4&jeevVw`8x}!zJV33&NhE2U?U9yN?{*cs?S$tMU5Nn>1bt zGrLoo#oGe_By4^Ow8P4EWTKKjGe1qdQaM=imfeM z^wp#yeo(UE7gNJ(v1pA}a0O7`nmox@-IPy>{Sx)@@S0<*q$Bj(g9U%Hx>N22Sh|{e zsrUE>2VZsaM2C)!Q?3*qg%giATO1w8QPp-V*d?1##i{uq1A$3>J_>1U#?8T-cUd%# z4)0UDm)c1`z4zA!JRv1>l;u;`huAd_<#lEX$__Y};F6+o7fbfjVU)e)c=7rhkZ;7A zkM7=IdOZYGm^IM!hpD7gw;+z>(T5W*n`OUKv+24!rmtVv6=XjScOIReqVj__&d$D~ z!2pa=Yc1LvNsbEY1Gr42(nhybbzuxDl=9{M`}Uc%l_MF=l2V2R ze}1eR0xPZlN`KEw?m_}^q$uwql50fp0(3jI&Zy(P zFR^TWZg03J^8QW>pf*)3UYL67B|pjUj|e>HCM!#hOu+x#tgLRsvDYku%8Aw)93e~Z z@?0SyB;iOQX$v?z9XY30{Yx2W%e_)EA(eURH3^dZryEIsg-I)HfS88sXP1|HS9q{~ z>m}rE;4aXT-9@moCCH5`G#7Lh5`~{jbyslS`d>{qtU4}~{itzx3To&C#3%u0uLvYm z=FavDBQANdm@J8nwF))p#GutFm#=;;MwS8l#MSse3{RsksUSuM_0ahK*)4A4r|o`> z(B-a=xckLqNaVw9yyT>+DA5vQGo=h_?A?%$UTFLMG}efEhperm;$3mU+VMZ769Oh~ zz0(WwnB&*R8^<>dOwmzjw*K85Ffn@m7$rdM3)~CotSmI6o+GBq=%$4;6;w?kA0%&E z%NEmy#+^cYJ2x})y{>0F<92L;3r&}`Fj#&OqO<*eE=4G1xVhsu4t~94{PRa@0%E8T z>e~{J$V*62gN;Sd0u6k$Re$kAzVO;-Xssczp+B>08}%z*xkkTg$@XKsjn)mh&(hLz zY|?VvMOSLpH*dY8G2mPw@Biu5i^k#SNkdeAGB8pA?@ZEv4us}s%F-!1&a{!N;p6b^ zgOGFAPHolwW}@o)a)hLcJ$b_SZeS{5L(!`nJ=v=><`y$DGohIDg#{l?E+q%t5-f{Z&SA03kj^Y0j)>cz=X;rhAx8GNX5yBFN zR~(Iv0;va)JOF`$go-os6QjhNifumB!KRhS8?ZkHdC6A9ep!;P2D^KJ?jVMaWiqMS z@W!`{*wl*QO2@r!y+OhwB^XfGMCdciCYi7N`DE99sgO!Y`eLScr6sw<;pz8RyK1Vo zocs)O_JEC7<@RkMzAyFbXJo$U+L~szSY>-wh6_F8&ZaMgrVNVGdwcg=68Tn^oJ=94)Q z?{!b?NuOk+qiMq8knjZk?_6b80b!5zGT&Pra)#sz>SHvD6}+cq1+%Ls6h`%hFE!if z>yAtRj@CW5Kcz{WdgHp)&7VHq2HCXsUO|cETw5kwr>46XbH85YB=(3Q@91-tX`2$A zK%nR@F+^#-&HF8QzqZDdulMNv?IiOQbcco;^qIF(%7`O@@KI>0*c#rV3lNu;-D!G{ zkg14hDuY_ZivXo)?lEieKn<(_6rcxLbaetO+Wbgs8@p$b7nb zf0@7?%ky#D%MWlsd3B3D3RRvFJ=y~a3L7M|ZxTThQb#ITTTytD=90~JqJnwab$5!Yp zMht3afMX&o%qoWXwFv`6_;O+*cXH5TfnN5k^78&rme+A{`zD3Y$~l~^s;&;~+Gce7 z(wGDN>T&k`@lV1|2RODgQAf!KPb+RNU+=dLF^~z(LU1zye0c9Ks2O2_{#5SuH8L97 zFs}KRRIy7`v>LHhcF=o00V~>8d$X3^Q`Y-^WTBi7Nv6S*wIfo*&As!t{zj1`bz(Xc z9Cmqq%0yg%-fgJvL0WcpHChld5Ez+z7*MouAc~8N1#Wo2QxMj}%6NMUy6AY^gZ$@I zZxaT*72TEK zf@{MQ4*og?oFeJ$lnHB1SX@QXDuwiKc0EvhJ`MXe>k8$Zz7NIxCoo%kipsxX91xOs@5=ITXs;^yZgYFXxpXqPxtOhrGseD7*aZZQ7{G+)D`Mw# znS3YC4@bm77|VQDh0GECB*{Rh4c7O%5sJWY@4!^JmJxpBBbJtl+}YioK$HpA%*V~R zk;NL?j1=wI03v`VJtL{q+*Fq9P@)9-o4dNstE;v$3`5!tq2m$RdJmv)`Z?@7(?%z<$zm{yDM{Cx=9D zY%2V0zZU5$#8(MAq)xJlq2F2imiT>nM%i(#+0%o==E?{2IabHf>bav>B8x}lsiR{8 zXupfr=UFKJ@l72!UOZjY)!>O;oOrkmK45dIR=IIO9n=(78lDqe)YZ%zwnIA~)2K`%R& zWFhl^Wr_c+%6L^*QQTJ+d)H71A!u{u%58Dl!-p-`;X~vX)*uN~yDz~f_vP0DE=FJR z)oANC6gW_@skKPykWkwig&2E^t}SXKg96_Jdaul$;xCs2C0utts35hTq<=w9xzY6M z&GCV{9UXazPq1|WiKufs0Z&sKO3H>M z{e;eEqq24y)#1d_TUjrX4YDV6RtNj@C~a_uM7E}`^h?o>n%q=RmO&~%wN>TS$Tn|w zZ?X7r@SDODhRCId@<_EY?s}s4?5CN}`d`~@rd&*`(8rlJv|gg36*JS(p|T7L+Gfy$ z*Zq#@FPGBcnErX2t(**g0ulz;8yKEtz2+^A1EdbP0g^yYcqxh`u2`e0T3M2l5^8S?tFhGNk%O zpWeh?kWPrqr4Z!!yG*{ur4MPA)uF;`j*jt2`hk*DquH}$x&U5nOmT7*W?vbeh^xH& zJKpfn^Vo1m`Y?@35JyMR7J}#eOHXRQc;UiZ^ngu>NqR+V$#En6BZu=TO zm$7GsO$cYJxQO&vk4zp(IVK+iJ2lr&mXP=gAl{)8BlxZ|9XTz5mQ_Ohk6Ukk?4A*y zb9njZ%Ept=$8qQEbS)b314sZ(~>M-JxR3OA%FvkEu# z5vBUV!S8_YDf!~EMe!0mLs&4(e`~YT*s6VGxd(5B2V5PQm=g+mUEB^!BCcirP&$N+ zT|Gz6^ix&+upPp&OY2tq02iX(JMPim=<~^PcBh-tyDd*TO_F>XG1Ri(bvhU)TCB=& zml*fE;46DOIhk{|RF^6?baTXb`^qp<+4_)0*FE%qtm<7wk03e$NFIPFIE#iDnLO_= z5QZUD;&YyN;raITgz1~!3LS_EUlZTghvdm8&=Oh4Iz@1|B7K?!c~b~qgh0#Qoc!nb zFJer?>xya-vD^|$@2B{ZaB&C{>B}%*+KG>=nd$XvUc%1K+&?tq`1$Fc9vJ0d%)mOJ zw+TvGi~q7I&h^h@vkWhj?Al|_9gA%Vw%mR{ zv{61dZg%$Ru`~PSb||SHg~;Q*CG{(auC%ibA6FQDm@&x(JyQSe{);o0dH!O+=?K+nrs&yj5_5#gNkOz&vtYN|tt zd!OppQ6KcGy<-MOV0<%BT{`=xVIS}w?}}RWFNRQT=Oe(3tKdAOFi^Z23rpE$4~9;{;R~WC&n(E-gI2R=|il=pQt6tSRJ_6uS%Qht3(8EQhz0ozbZ%nS&OX^ z1zuo*38D|x^>`%DUf6vXdSc_chZ4nM^+SWjb9Y!N?r=Ycxwe1$gu~k;argG`h!5vN zKb)gw!0H&3Kb?eI(E`QrQhq&Vp*;|+RS2i@_T+_)&Z;_Z z3Vl=%SSuJcO8LdGmuiHMKeRef^o6$wkxKY-$e{g+8sKV&~& z&gAKZ8+HIJT_wq+di~Rb$r_)~otuR-|NB65`X4N?c}{?5!{bxB~W`~h+F**N$FVubWWpY0RXwoJ z{&d0rBD}Ws*8x!`*lRa1h?#t?XJp3OlR*#Y20u&QJc3`CKh8RtMAx1=6_Ml^{4_SP z{O6&Yd&}7u<6EDB+CskfkPAh6Jm)P%L0ZH-=^5Z$F~rvhn}F$=o1 z!jMqYx-*YUxYmtrwj{-u;+1R)iIO73VrikQQIni<`!R(uU|>+>??}7JQM9u-MBmBU zNu&#R4pZ;<@&juYvuz@rTcOX9J}_?=Ag_h<#v3p^d|&i?_R+cCO*V7=t=+m%_8}f!im& zLKuNr-J>ng07SH3qTcGkJ13FsEPk-0OX4RsK$*OlK-J<TO; zCqT={e!NU5!1<$bKwIgs7zi*>FW25*bCJHQBy{hd14yRcS*o!!M_#dW!< z8CYx?VNS7cnC)p(FY*vY6+bX@4fABUcgCK&1i9Y^b;}zF;y4?O{EW5t*3nRdanhZ8 zK=BWyTYTOu&+uYD7+Q*ksvn&%{11EsM5{-1PZ3hWvgy^gFCpGm?cFLbr8^W03kwC^ zPq=R=p>S^b2ciFMqw-Q}7h*`;ricdjS9Ie|%PT)01&2*azA?g-n6H6;hWe(Lyblhp zrqxaaJagkMWUX|dgl)&D$1}lH?QHv>9LN~&DhM64Z^SJ~jqBd$|B0g2?LPEC;{hAn zKOGLy`j*`}Ab2=3<^XOIbZU1FEsY)v>nS@VH#$#(tns*SZod4I3hOrRW@et}cUD!s zo%s=X4zcSIK0BFcB|s(gz39ID7J22y`TaGE#XpM2u7Y?tL-JBco?7)^Vkt%D;#>Z+ zmt;HV+W*EXZznP6WNInAm;n?5v$6D9M5!p->bp122<|>%bInw^&KMG5U-Q`N?8?;+ zMcjBTt={Y@$f)0WOiK%>h+(#ahvKX!_x|2-MS- zQO>d>j<}Zt!N`hOLzvdTfQdv+3}?{n-4b7oev%_7`u(G21RB_~d~V5@MPfzh1gu5# z=p1%u43#BWCYIkWl{$V$j6~An8bq2slD5Dh+lfK(dG7{ROc>j$I&i4bWTmCutML(E zO~wqJbVkh?-3~1|fA}OnjEIa1t%^OJ;>|oGe)N)+m6Bof&5G}%7%DAmd*dXnwVP+> zPx)}{7K&;dCMnhMrmmfD8ni!!JvZzI{4E(1u76D`1135uzZ-XP3Q4Cy+7vO?Lh@db zz1zUeQ$<3WvXV6g#tvR}@4OK&c8xe>ztVK-xzKUayJ9T$OHF8O{$%w>7pvY-)Dd{4 z6y?HAwKb6i-I%iMmLRRkF0Qi8lC@u3+B?RjP$y^(Qq-IZM5(DSB67GU{UFa74(Gmp znpR5m_Ntx7i?jbMJFgzRsqUr4!)_>I$>NL0Pq^EMa^;d4p;M&=S*wpnT|)%1jvh*vK52l=ZC5coy$J8x9H0-atNoa`dn1!kNfmu%O7B7%v{Z>NGQtvvLaPo7ZWEhA|q z4n()~Ree(S#13yA`Xwz?Y}(^c0o60C5Yvsn)+S2?^;F-}@gb{QFk6u`O+|sKQaIhd z;5pr4sFOesQpLx!p2_YwVzF03Lib%c=H|RJ^+wNp%5n#!FZ6|Lr($7A@_P}AN%Lwi zY^_t}c)iQsVG;q4H=A19CFU`!`#C%tjk3JVT@OAy z%{bhuG=XA`7!2e!l=pi%97<$zZ=d#+XYAQA{N_M=qqgHWX+uLp4&~Mvb7)Rwtk+P_ zH9~I;X!u`%Kvv}vM%jp&oxh%ID9^Zv$|f8UE0>{FVC%L;+z4n0V2F^&B1sDWuK3!Q zOJgR`ovJI09;f#AhsQnWkDEI>=)f+*)83g?PP3Suagkg_Z|+ndH;El8G#T{j-R*XM zt6rC5yBti(>>sGel8i^3H<)>^53+nc4#66E*8#nS^L7M^mU3dYltuGGriXfl9Y*Yh z2QwJPb`}7Z7UnsAU;D5*R4bD}Sf`E*_ATW%yqPh)IDwf-0!Ays4=- zTZ9FbAZ!EL72_6`5yztlvCJxRojp&XO^`E6?~KUWaQ0e9{=Il{6{QVS#_6nfnQ8QA zvR##RmykDf#g2ckk*`PY=JGEH@u#reS+3%&-)Rs0BdM%;^lS-0k(Wq-9OQx{F-%)A z(V6OQ!w(`Ln}8Y{JtA>)DU^@4#<224_q`VTQg`a%OMuq)?g$@(0121n?odGwUWKon zH(-~cY>!n=IeuM!OTCj#5Qoj}tH9O?>)_YY9E{9l*L4bfXH^|mdDXVB04Ny##XSf6 z7elrBZ-3v5mJBSx(3@)X=F>e{T81!KM=%4ovasfuebKw-5S2vC_DDUySHro2IRkr< z&PF$uS*f2%xiz{_PR;aZOUEfhP9)Wh9#WyDuhBfv2y;J`zBBXDZJiz$FJ`Jq`dFfu zA{rSE%BP5IRaaNC){3yF5d&y2CSG)-F%+$tLjW7$+7f6T zqptr23Ma60BA>+I!dB{ns6}OU9xwg4uL~5BhXNQ~AF%7VrTbsLLDFqz+w}a_Y43Rd zgXXG1MEZnCbS=AM`s-fgXHd4r6-YYbQJqYX+^Ztj<2tsR(^4>#7wCbq`?@8KHDST5 z*+$5XSePCT^ifk#%%Y=zZrc87U@xi~*v{uZ2n>)AP@vl4sL_qgxP<5;Nmd%@4zrMno zPsJc$J(CM$Re$nQ=wyU%IK^(}TuYo8#7#HcFdDHq}J#MD=6<_$IzrI#Y)Oj~x2c_;t zgZcjF%^o{yUu9-$rK2U;v!Kny#02VHbJLj*Jb9{9UbSsyKxU}61m%2RdKK~Q2f9nP zQ7^H5!73EBgjYXS`RyPp!|dSwmp{Zq-)?(?poP)@Lm6^GM%L85_89s_gqsw$WHpjO z1F|0@;=&v#b z5V!RO41(XMu}UOwdSIf0Gm0IA5X8Y?YP0DQkd3K|T-;pgO%#E;a!;g1$xoK8BK~PO z3QjtTtmG?fngE3xKJYYCJJBL$*$sFF?aQ2t&z!Ry<6M@&PO+C+-K4nf+*p6p{*aR{ zUM}SWF^ay$L81;F)m2qAjxFU$F8y)KGdo~cY-arGZhQIitxsdhL@Hcm+CJ<08n=!z zaH7SgU2R>Rc|Jn2H;Wb7#pv~;Zlzq`D4US1SIRjXKSzE1YL4(uRWTh6i#@p4?HsW; zFtC!gW*~lO`Z`=FL3i((cmW9XmA8~li&+#|tXpia7C#VBrN-D=D3n({5GiWD=XZr0 z6F{Y6bgW|X@*=?!1!B@YgPF`%C*33KX>og3+@4!O#^{149NZe|c@A1nH%rLNo4(_f zsG_Tx1|N>0TVR7Q^`}23=swlW3lz3@w^E=A)c@81v{F6}ij}V)Cr@Zq23DStIEXA? za(hcHDlms>r;1}a#gYiJt43`3LxmnT#zZ>vX*Fz%&k-Xgw1@wVj*?YGsV=&yVSiO0 zNHtlY8KmsMKd42^<~gX&bp_b&KQj&qFs4v+W==u%hQ=to0br-D^DHdxYs8AuUBKsP zwE3QuT=G1G3t61v$NmXR9!>v@{9Xh+aM9^1ox~Poa=rcOe%u$jiX0Tg44m_KX#%zR z&i^-p1mUQrsEvNnbl9WOCAP4;5;HjcGu$|dcbR}`0MN; z>E%BzvSkP38Wrci{pGvYk#}}2j=tahBh@m@baw{cOU=BV;&v<67Y;ofTdbnHuQi^e z#6DEox`2<#63;DPR9e&}E@*sInVR|uZkf<^!O_8O*n|LX!T52YHdEUT*{+&_iHCcU z+fyi_Fp^hNrKOV9VdN$#-Xpp30PZl_t+@xj$6Ft< z5CHtyaVBOJgH9x{vZv95A$IaF>5{7NO_1tfVAXoop9jJpaYzeEEHXrQvpi%e8~#4k z>(^yGF^i$Z1WM7AE5c4!`3pjQgna45*n)J3gM(~CC#a=r*o!i^pm!_6G7b*@9mlW6 z8!Ovl{d?pxF4VU6c9UPJPJ)RDYskvl^VwzKE(aqWkCcl3GA2N}HNLyv$pu=$$PQN7 zcrMPl%Qi2>Aj46+#G~>ze65k~M`aC9GyhD7xB-S{4 z9@zt_vCa?g&`{GV{K8kQ^=itfyz*x0MNGBxCYC2h!atnLML4he%GW8af-|M1&NFa+ ze4TX_{SoAXhch4N)Za@}%2O$C&Qf z5gLX6hj|0Htv~amv7s_LF_?D2WB`sV=94 z*pS5&u~+=ye*1E-ifgx%V}N(EgjA!S=yNW?e-J^dr9LKITa)CSk+2_SSp@%Y!-9%x zh{C%GXG^$7`p0dlMPgTnHOz(_s>HHifL%nMggKKoyY$aakgWIzp5k1{tzZE;@ouG~ zib!?Hl1>G_*{?Zo83|TlcI}H`!$t5`fTIP^mm5v^!uLl~G>CjmWD)L8fxI(Xd=pmC zv10SvK{BQUazN_FdQ)NU&JpSr zAGz0ueQ?^0gkmorkBfjrlmA5%qEhe7sF(7pNSBJ+;UrPx@FZ>X;fU2o1*m?mo19#B z?27o}a-gI+l!ueE3|Sw!=RR;za?M8C(e)9S?Lw zf?Xv+LYQXYu*p#>1O+H)pc%O{MTlQ-^~BjAnI7|`PgB`1I)7IV*r9=@@)4ONyHRf5 zyqU0fCwik|fm*8#3+_EF96xnbi}xOWjiAV8D!Yr3lK&;?+FH9nwnOBj#I`}9O?B?d zx6S)gI{*PQB6!d4>y7eubq2C&ZN0-21|bdhGyv*4i^+fTKK=v+RLshAIco!4tY3(j ztFp)z3WQD8F*(2wT%?j{dPQv z4R{oZ2A!;Xl3nHcyKswJ(z7KvB27Mgk?(ZW3ARjzsxe!yD_S?1wTFw4ia;EI3Moab zKoAi3^ny}?K>_dH(~DIG^`|z}h{`>2}KlVSk?UY|VWKZPp8y5O_ zr%RD)F)yk7mhpsl`BtpOFsJWL;up}9+F?G^n#NreuJyBvU2DN*-KdO2warf z6qk#Rj?PUKd&G_as-QpK8I9(o_OIx!UC)JrR^6B7aXwz|h#Zg|!YZ;|6*lXuEv)PM zoC}k{$Ra8Ic!%iaZA?DrC(%%nm^Frx5s;a%*5K5c@U#AT2yPKtK4KvNhve;kkO{;c zRw%RR@kI0NGZRg!jEfkmI7!k2 zAP$Z!+65NWd}{kN!6zAfqE87kNJ^mL8_URd8W&~Loj=P^@GD+#c&-0h5I2kLB5@&Bo*^NkNpw2uG1Yn# z9$+E5PQO#>-9c6KW2L5s4i#5mHpJtj@Jak;U8i7fZp$Efw=bour3Fr3_$6O{JFBH~ z0a|krfhn|?0~(L%m4!EbA-+{O?3cL`H^#aD@(-fgsD-`UIH*)OJpjd{jc`aL3^UWK zJaezm?D}gS1`Adu=S^_q2`m*w0~kmW{!7ujh%6)IKK}WF?^2Kft_AQ3ckt;O()^jf zhEz=K2>ptp@FM)@C0pB2-S9n;^%W;E9TO2FgoPI$Veu~bKIkt6&d0Y}&R@gKIRG^( zCOXBr9=9t5pe8nFvLtgORpQ5w9}^C4wnVV;MPhS>q`M42=tY@qObIm$iPN8_TVi?6=8csbohc%zwV-`D_#6579dd#}2<7#ofhbS^$M z=p8kMGwiGN@$gU`UN?W!iOJ#V>oScEubbSNEH=>CwpKGUcl{PWQ|>9IkwDdoKmq%r z7{N11iNY%Z8RbHEmZa@%XPhs$4v#H|M(V>U2wY;5s;9Y=fN$?pXvY@*B{cg&0&*+afOq2fzAK7tMh+r*AQC!f_5t@7Eq^o zwnC0VLJ5#LhkyjqDH3}+$m_q@i5Jd$ZoS6%jf>`~XGSLk#K#<>RNf7cm6)GztStSC zPMZGy{XFXCSE!y>A2)9PII8h4P04fqG^b^b%hZ{F_JGBL-;&&n_1{MTE@owE=O6q( z`eWT>TNf8jQs9Zu4Io2mgd;ygN~ixYHp_=GWs|k6@1fivl$f89qF91n;kb73vXMDr zt+)gP8qfFKSuIK<;67CM%(-jf;>;|XB#e*ALBdpYlzInL@cRQ!u3Y{@i1zbWN>Jl@ zm>pze%WRl_%z0bX^GGJC6lP_H+i$7wsBueGscEprT6)MIzxuc+q`ldAL5MJV@gnWV zWItE2G4KG)c{1WMA%8y*>k);^1ULe5S)emaNKRI!S>*gW&u>XXAEg!dYr&oWukEq& zmfpsdXPsuuOEL&pf*nAcF(b0sczfhPLs&K2pXY_2@E#Z}VB;|G52`BAk-EQu3}XT2 z9paktdB332z}WcV+;e1O!JI>AIGOq>{w5sXw{XHyv6XH{nwXH;_WNYJ84f=W<==jH zh6s{{=S+61Es|7uOqw%$FBgw)C(APkeOdXW>D|#~3&_1E`i++nU_xYl`O&ZyRUcYr zLAg}4_{NlOro|j3Qc?oN8gWemh$^6~YYu(FO5PEKW_H^jtnGFTk3M)Ns9crTL*QqD z9v9@Huu88I%IwkItQ(%}5equhn&i@masHjcw8g*h z&$Z7~H_*`te8;x|6qg>P0p>oUmaxn;XfV0_h;bg;dTjX#J<>2`LGA`!d&A!*jjRo} z%cJR+F5aI$&iZR1Xx2J7nu9S(Ji!oc2ci#Rr(%&&Sj)or zd=T_`fV*i?_5qjfm6Fm}umHNC7h{cn1K6kTyqBetMCk#+wL&AKP5HOzC;;tybt&cl zZ*($t@HAs{i z4GppMN7LEofy-vtiu)2U!R}qXa}O@2$lINCb1P|UVY>Ym7KO0C9?zx}hH=Mp;syrh z4-g~5#z)+*EfdZ;8D38cBL;V;~jFm0UZ3R){0@{->SFb=xU9k#7Xo zB!}B2AApjb+t{qjpz!TaLxk;)TbrDExH|Rk`_jmObg?0gjR*0@M)I$p8=x=lym89s z#q<1~mt5ntea^_7`>>WzY;JK0KxAkQ?P2Of0a@=TtO0;|5V6H@HUq+P4~w1kdw1iR zIyKvJ>O@EG5#86EQ%$4h3QtD7AxtG6v;S>fDag;K^Y7r?_GATP_*aA-JyY6`E`f^8 z%S*hfe=Aw9`%k-u>TW{w9IptLUf4A&Z}hj}Gg2G-{{3BT3Frp=&p>{DM++?e&1AsP z-($Z_qPCF;Dtshbt4f!@ozv%^IY9%_4Z-LgS+%99fWEZd8MXOU-HQWNScud{hjmDk zK@g{m${&PHOagQ!YuOMr6W-ni=A9Fxq*CZ~V>nmIk6TLz%r%8?SwAcZ7RM{F_S~;cn$&Wv z+x+u1h$f(88`N7^B+TApSBW_j!lO%lbgeFROIX^S}+rlE+Fhz=CY7^7IV2ba-!2__^Q9j<(XB`OGNZ~^2< zQhli?B)(qe2T8wzjSU3^wqW0CDlJ(P@}>qw53*2l?gfl~J_xo6Ml%;!&XU)E=WDWS zXL*DQl9Cq2y1?62)Re+wi@^{TI3B(if5$;L5m08L1QT)?vcC(gj`}unj4_hZmrQyZQ z)Ta={V|>1sW6XZFU1ANO`Ls?zg%1Lz;vYQsAKIB2cWt!~bOCgEewl2vN-9k@CDIXY+6J9Y$z zg1Z7LPcYD8i;W$v+-olZN78NA_F|LxcXN+KbgSf5u^ z9Ke3Om?8YsLhB8(RO>ss-$1efaX?7g7%c=6DPy2LNn$XA*f4#I1Mi06Wly zKJCS#=bqhn8{Fa8`v0hU4{$8|{tx)FB`YChRf??ajEo9NvUi9?h_X}33{ggd5GhhN znF%2x$zDlGgfc?1DtSL=_w#>`_dSmLd7k5V?)$D>*LnVa-|uH;$~^Iv1M8v+I(s9| zqkH!=pvLgH=d+2> zJKAC;wEz*eoE9lNSzGsb}#daLMTen*;_|thCB0UzV^gj;~@rh7j9aGgB5uCIKtOMkU=x9LX zs;}k2>yI#2b8WtR=t z`u}T9OzfY3z=Poz0ad~;mh#VAl&eB$qcJWUOumJML}YsJ2;VMkF&-{L;JuJn7KVXUMe@%oLBk>XfVy?=NQD}_-lnIi`-B4$ga zZ0|Fnz6wI2;K^(D=vd@UP9v>PL|P9v{=aJ-ftIFsEHu5(fHh8Qc{2?~s@N6D$8ov| z=y0$-T${H4{QVEDAZ<0W_Wd*XH6RY4D`)FK=F&?s}ul z@D??z@xzIy#HFfYq7?2+QZrDxETDdus^5VB2bp)Kh~hg%@-l>8k@~&U;ajj0d|`^< zThm`<3sq2pqI=ursC`~e4p!M)zV8qbA>QZ`(~6HtR4sO|ALTyR3s4;jK-C;dBy8hl zo9UQb{EE$-z|Pt)7^UL#UcupBKwht#2>}Tu+128RZsRJ3#qMQ+D zhksm%m4(qT+vzcuF}nY&xB$vQMEMgE4-#w28p_MCJY)2C9V#cp-SVh{aEGSwHJz7Y zYQg~WLy5yNz_@m~tZ>u5FT~&sYb~zYM*`|rGIK2Z4SA*CKe4ks1Lq@*2LcO92`N%}!TEart@8Fi#5jF+`Ly5^$ ztcF8`ZO7TmmwWfqU49O`_fGl08q__4zXb*(nBnB4iA=TvP4oM;!3_+hpG2a0F3!8ROxcMAnGozY`mJ;!ev|h zi|r)j?7MxY0?&FYj4z7+J58yxcgn$2Jk zM~TG^kq?+Dvhwp^5nU^bBak=_~zeVx`T;ykG31$L19? z0GW6+5r^EwYFLHCBls5oky>hUGAL>A#NtumCEz#F-_AFz<}a~opnnFt=*xo(eg_hz z9HN5ug)k>biTBY&F}WZhXpJt>_{JWN(#`BY;$wJ%1yZ#VwEKj=`7%4|Wt@7?#^b5@ z&UL|v>I9#_h$>n~A>=QJ*d4BSVCoob&d)@!We;`)K5!QLPE*^h{ll>MKAR1OxOV^j5j z-lB*gL!`;?^X$!zJQ!zSYPx6d7Q%T%1cad{!{nL3f1MlKCf7txuF-w3UerywFnp0H z*dW3Zf~8`@cRrdptI9 zDOpPZ9QW>D%#0C;#&IRv@qg~duBV+Vb-fbg49kH?;{fLyGmy;j>leZ2!y8_~{ASkP zY2)o*ciEI|NAGZpb>#Zf!4D2bgiIp)jxOUJ;IO1^&_Wk^Ce~PPkyS;wbZam@G}dwP}%uNkmvcU#DeLEl9)rfg^8T5?>GF4 zAjN*H9l+zJUU+qVI|m1|%~9kB$%wZt6|HO&3KAxt!!X(OQ4H4;@M@ykE-ilF-FcIH;0a%)7wF5s%(Dq?C_p+}^6LppFcS$r|GYzIz zRI5Mu0bn__W60f7ss8(Gn!psi^SA09*n)J`B6&IF)El;&DNVkO)!lRZ(4SeN$wk1n z`}6)L+ol_wP&{nKDM&SIdMzZ&^~H3#Ti zF$IjF*&sKreaS_2m$2)n;RH!)WFvzy-Nt_^_KpM>SzYm`zk|9VKvN~jFIQ2z>zL&f z7FM_i2DRJ~Qfyjg-BW%z=w(?{CK7gf^hmQjIS6A2Z$SdRXBlAwzYHe?urIYvIr81fVBG^xPPnBWtcBmBisI` zd1q4pp=ke@Otma!O9F-_%r=D22GPgrT8WC9Bv}aw5`2(gTx=!K#ARzsf~*cB7v-qTkjP?$SNPuZ0D0fcy)V>zXnfpMkh!aG2#*hu zhm5Lq0|r@rX5E$JjygO<2om&V1RlA~CIYewZBHO(b!0MKU62x5# z!PF%12B@-_^A(6ZkWiI=eCGC*u#-=;2g!Jk5+GlKR=9H5F^XivzUL6}-)tS%H!~Bg zOP*yBotDFnO`w!;*Cm~#+_Ow);JXB|TMW{(Kif(4#r$E8sYpGXo<@;&eO&>4 zvXd0)oMI^@O|%#9IVt|aFZYO+G}OP>;+GAEP}uhIdx-6I0op~hAstILqWYH^gUQRB4 zCbWhmEsZ^j{&*t~No8haZ0gEOXy7}emhQ{)&A%&R#YZM|B?z{ogCqq-VQ#nJiA z_R=7rp-ja=xIhWa*yF}CsTU>Q!loT|Kd-J$0Yh5_CqVWIS6?(#`znhsHGl;Wda>{55)Y zYDH~FY}#H|#Y|VwZ7E$4#6gHhJ0O&#V=3%4C=z#EN9S7T3y7h}7{83g{d~Jv+H;J7 zvD{53_z?4|fbTNl)SDD*S?)KACgk~(@ zeY46fV|BfixIqBL)=w{`12KhNPHh`$bJ~rYy||Ge9P224 zP)BF;MZ8N$otE{lo=ex!|F1*@%D^Y`WM)<#R7m2-5Fc8*u|9W8vvGZ6`TRyWii?h_ z;1on&={6#hU%u*j_p!aH%=R}jWp?uz{1u`I`{OR0P0^1ea4pG0b#ja&6ODFtt}hL9aL&E#XA#tL-(tUU{7oF2kG|&!CC;_wIopJr(CE=ti9h;}V=r?nE>)(o3$(cYD7^%-$nSG3)27m?Q3gI0HP6Fr{PH zIi6C8#jY#IFeKW4JZc_0Wv10$S*4#(b;`Qal49apP7`hC=Ax#oZY_=_sJ0!QpK^-$ zPV;H{K)^6fsw(d>UTU-aY=eegsTwZcXiWrGg$>nDX0*PGiH=4dA>B40WY<-~_e@dT zzIn?@h7Qd(H+f$eI$wscjmR8i>&(7Dx*B*mC<(+8neCY-6((Fn!p1O4LQk0aQ!HA* zJ>W^@OTCgK;?JI+kA2|26zJ}*FE4aMjx3Cp@z|zm-@$e;`9%{PwQ2B1&$5pLwO$&J%M1x^L)Aci+mC)j~20p~gwl`|_)(Q9QQfUh19p zr{~U|C8|#T-!^0kMP^8XkVeEEinDqvWWNV+ZV(AVQY6-Z)n`!zXo^OAqHLH--#l{$ z4QH5kfP|3S>lnmTONL2Ra`~fwY?I^?GT{EJ+AEGar}J<|EOyd2gz15P=mul}q@bN{ zw3g_&Oc1vgR{bIRa1*QPwr>JDNm5i1+@7=~TVxp}loM<%+2-Y+n-%YTxiWu%L1h`N z%x2q%IFc=d9lI2$qxxPTSm|FPAnDv|lgoyjK)3OET2e+;f9Dt}x9kpU9Pr}xEkq4>@u68Yzza z84qIiiJ6M_1}08*MuLm_7Fr-A2Z|OG7hw`9EG(qumwcm<&*7FFD+OmNu(;NB{Sj|G zE{6A?#gSOkV?~wUE^}P`T!sf=yeeKh69r9Tr>+w}cTfxK;d5wZWq#=orb17T340zc zK!N1fGR3%D2m$k7J-l!1(H{SVq~)}Q3B~BrYJQhnK}bbU|3?Be5^^A<V$ zk&~VwDL`!fGDIF^I*(;Fc5Z>Bbd*S&xgnXrw**vzRP@DDn&~e<2PI@1=&G)JZ?wY!&-acvQ;jijPS9L^D~FGkN7ax4MGZn^l}g_(z+!9`1E;ENAT zafd0dfCH{5EsVC;Gc59*uS^qOSiY=)<05R=%dA6tZXd*F19ilshHnc!r+q&rNNU!R z@1n;|zJ32WQA1@xwd{g|aA2#*t;x&k5$ZL^T4fu0lkxEhNtZ+R0>`~h(UO-fwpsaW zDhItx*~QC}06hZg&V5v#G1-28mDPWydI3?w8t7niajFI%0%o+w)O`V~aM7&m4f z_W;rnT(&R0$mtdXFk}J=z=_LVa9!7J)T%HmOAW0a#(zfr3-a?dP{eJgdY?k={+5k7 z{jagk&Rk!-9txn(P@X8_h3ZXeat@6v3kbeexg>-^vf?F;Ef3S{!ZNae^1Q0+I3$-!*xi7@R>u!?~VcSwd8;9 zbrRj1;VXY1;R{Ns}{8}Kd&{aUa_|3=VNz^BCX z2mR08b<7$7AWGGZR_#?G0rE#{K?4B7JPyp2>+E4J58{OF_wBpXYjETtB24i*6Y?gj zW1-v6m2AC|B(6UCTz3gvukI6aeoL|s71G74Ah?_>3#cg-rPMMwc8H~QoG@Kb&?I21 zab63~C?el$rxP|OOmU-H(|-vi=+}n$i$BA@ptl6}cvg^J`k6IC6^rWNU*+nn7$>z7wvT}UerFFe|jQH7c z0waY--7ag#A*q^}`yS&e$gG#{{-){A3zg@sa#p<9BENqPes?uV9pJ_XM@SBjLJuB; z=|KzPtfQq8X&ppA7m3-35%vrs6v*o+1@{AqKpbw5{K(^8)9a_4a~Lq~x+0DQ9Q5Dq z7L$Rr6EvzjN|M_CD@h9GG~(}50R~OVyBS*b{dw*e1>jFiV2id8?U5P&{T644tQ#1X z68LOuUb^1c>-h%?|FAFT_Rl)ifcn>1T4z$4k+@ik{p0{>zaL{dJE!sR%=2w5@DZxn z(lFddJn%1##z}Rf+@AQ828}{0R{V&TtPb#prDxc!78>N$26O#66`Yid%nYxAZIryi?fme#b8J&c@R(0AWGX zpHWj|%{>5@$4z#)6Obbb9Y@j=N}f_Retug?6i}t7AKJP7#-2E@&f(#6+0R+&!X%(f zq~e_ExclJRUVp$(o*hs+5TXf^>{Ot80S;^1)2ADyF8n+R(AgnRCT^4i%PL{qc*c(t zfV?}4zddV9ssqEQ`SXOa#@_?!{)iGq9}y(X31f&!d?eI0Bd`WuXJ--7kQ}qo*{Q!} z1nPvVdx>WtTtjH)`tCm`?FzGAMOoE7lAj`W*}b#=Hy|1S!9HM2Fawg(B;a%BZgk2E1tqed6*!w&& zFYP|Eroc*P(H)k>=7F04p9eWC&PY@g_zCeLv?_Stud96^I@5qo9eMp(r&3{cBoxsc zdRnz0^AG%i!iQX6%V3a@M*8A#GXHgOzOHW_r>OeL<*)$?{zuDfBBmp=6`wTund=C& zk%)68SNZv-1PU~wNi$&m(zjhaG)fXl5FipWLi1qrE8X6K=B^oZn%rts^l1u+(OpW1 zf92?4S4>KPDPOD*e22KsYvUc(0e3{oW+;NQ)@_20mU-7idqo~#u6kdoTig9W z0MFU$P1K z5yF_@P<>Ifa+l8FPO_Rim#Wx{zMK(zmaLtL_lw97jJS$({mlPb4XU93by#`^jNMfG zn^uwW1YtXWP;~1O93JFC@~p_)09U^{^yzOn1^{lj2O+VLM@GffL#sH_I)`f1jOt<5==9SuUH)*wbeeHOxR^$k~_3#4#ipmEWt1< zLUFu_RP33RIy2sS_k`&Tx+sx_-OFRPmq8g4ouE&Y*{?^IG>_40&cvhNW-FqFYGN31 zTfhPEMup`iT#j%KId2jBBGT96(?}uYuI8N+M{O*?Kv6Vr!7yp~;*?%p@aD0Nm#AmJ?cZ;LJ{Tq_8Yeu6hr6 zg!A3BG6U7CL3dW(jFPdoh)`%N&npA6qpgHum^=Z-5R+cj}wnEYSmD#c~Qh5qA^dTW+) zs)q|S=w_SS(o|2rSpWz0FMJN!(O(vcLJ1^Je?Zy2JiO|}12TrViv}=CjP~^KMg(K# zrzJvZi7s6ngY|P8frFRlB%0fQxc*TQ&XAf)?s9VUb@V-Rd6B(vz{kyg>|7nSj!%F* zGr63YZSryY${AUk-?j^i217#Hy9m**$DLmXp| zk0*kWrGQt3t{2o!Nkgu31b;xd{7XmTrrYYJLSL+{UBrRx6ttvEH$A0rvt_drl`5#q ze3PC7OfKX*nQ2SQybR_Gd;CjSG)1?FJcSW^*MJ1yT{m;e&nP(E^1`@<)@j_6Q}>C{ z*AR=T8%EHv%}5 zhV%it6_VwZA%vfBM7`SiPbcnM#e?|nhU@xX`=`cV*f4aj`x5R&;ZR2;!|}MOJnUu@7Z4@~Mb64;9&k zmR5FNJfoRZA0a%_v}XTGt#3j9q)2>}X5^H&B7|!rE5DInuzS_q%F5NqatwGkmI|JA##f0hEW~)z>0n{UO%jGD@?|j?&C{zj6%XOG5_R%FKOS z(JsAJQ?l8C*wyf~xI4PeqQ&JFWznFkA(}QYKt)haDG_0F1^%<^!T7 zGwKq#-Wy_qmB7sccU2wb6DqclTB)DL_ z4_|urHT^nC#`T_Y}4;*18XOW+)pY_!{20NKx3J& z$QVI^>j19&SEmeO9u`Y*YUavj*4FZ&(Tp2i*P?f-xUY|GWi2q(Qlzq*a4o64bgJNa z7MVq*Rd~baxV-%Wnd=aKW$lC9-ByfpN6UhTk|lXTLxLx0>g~NFnnBCoN%v^RwL6q3>JW2_7+xh0@-HROxp`v{kbp=>(jU_KKl-jG4)-96L(jcI_ zy*PQf?`>=En+TVsOV+tsz0*FG=kB%bVcWIn+o;v+B3oY_rGb(Epol$VKay~6K%-G4 z!Jm-o?r5ThuUw!AF$e33$;r3)UK5GQzdaD;6#w^%q7o62Pa^TH*3mqQy@Ejm&+e;x zpZ+*LWXk-AMS*IF}5%zjuBTN#P5bdrKLPQ!C#=hwe*Muw4;#J_rIkf2rr*`vM+w zc4m7`8=qtoxh%q`2e=P%FQP6i1hGD3P$dNA;-o^7phiZQTDCDuIe$FEb+5W|36rnU zjN3n@0mD6{^k)3PeiLbXJG+CI)3h@cLt0E8?)SN0DOS=ge)rg&npxi}cAYH7HN4Wq zlOq;%z|3Yp1wC)tdlqhydu>Q9X&Co(P9z7Piyw?AN6frBdm$(6(2h+iT8$E_s{1T{ z!C}rISa&8t)AgFRW8Dg|AssvB`FFxTH*u#8pT=zO8*K>fAqGsR3JC9dc^A@-{P)F0 zMM0R(Ien@5{>*HR)3fh;VD$U;8=FjOcG!2tR?W@L>k`CHlse>??d ze`f@>U#$~<2X}eyo!?)(2ozbR?v2;x8e;FHo5&7*DWE1Oj8j!KP1Q8|oi3Fc%T5=K z#<>+_8b=B%m1#^&On@RDS9~r1q+O&VT24-mn&jEhtM*>1Rad=4m_Nb7mxZI`jJGQ- zy-6zPA|H=|qOA*6sQx3n6CJg}Elsq`{cSIel$*on&aJ19q`*)^=)%kh%YJUfyd9$2eA> zJRd7+ad~;B9ydi+{t1vKII-#Rb6Z#W)}N~&dg|iWzJ33mK!*wA@cC~QhS7Et*V(hB{qKTZS9J5u(Qf!;4u;b{?Eig4T z-XXV`h%;@pD9NJq?W|6oT-IAXwbswZJZELM>$%dDEE5lbOPzJGvq7$UpEH?vxX6^c zcaG~kYOaoM9vU4@&q|f%PtQ@vRhrO_jN-Advg*Cc?p$3CqX8ZZ1NjSzsUiCej=yp= z&NTs4lK-;HRc_oSJI|<8XHQ1kj^>%EDVF&Pm&L`!^!)l2*W2mYkLdoW*!rkDP-RG7 z%k6V%er2AeW1jOg8M&ek_rW-YrE#7*gLOPb_lf2QTe&Aak(HGS_3os6{;>CQsX6A{ zB5=Q$(yN|FkgIvS-?~dkFx}$BqTTLCwY8seg3NtiCYg{ueOr9Hk_%eEWNbmh=b(@0wgp@+Fgme?Wc|th+&M;wT9tsNkdcTj!gU_6x?p?l zl)Fv9+PFNrF+Y1Nma*?{a9V2Iz)c^w%wlpSG3oq!7^9y!p8>i0Zr1l(=$WRW8Dk=A zUjmmA(}To?KoKcN%_KLWwhqw5;T0L*`zuIc@8-qj+9n^2?aXB(H& z#Q~2*ty=~{iyQ$_wW7O4)NlH-2Azp$S^i+ z5#hpNbmB?7QiW0I*3p1fYgSrH8Grf{d48!beFt|(d_CRx&7Hfv@#eb+Hih;90X7_~ zxIB$_gx2#T$)s~$jtl+MWSgwA6>ei{At~EPoMx{seOtV><^RUQ=L0p2Mn*;esdt$c zSY?;lW~(;&*CN>J+mQ(=>BNiRYA}@b^z@+kr19v*f_DlZw&e5~45c(Lv5qc^8Y28qZRTUXh* z(UbDr{aGfdx=%{~rl5mI-hY*-AfNbpACM>!Ydg*sWUJCk7rdrteeQ1>2NU(CsTv{$ zP-w>m&&0QVw*g!@`Gt@1D6wDP)SwRmuWf|euBjJ}{t=42oKMVK9(cXIeEh?Wsjr^g z#V$`->Yu)y{IF@I>l>SS0Y52UE##2L)MXQy4(`>QCi=1|+T@Cd_63Hb(!X_onF-(a zD#+*(VrOR`S-LbN%-&}=@ zD1b%5?)u|Hg7$7vF)`GOC zu}pq1Ir$}gT2FCh;GW*Dw)v;lks#Inx7|Dnp6XJSISot86}8rXRefg?8Z7s6spP-kGO%9E9Mm%SlFHRUV}`S3~YNa)kE_Q;}}pzU>yHQg~dp`$O@W z;cwn9^)&gG`B8@(`NM{?T!w_VvZqer>}>gw(=K{4`*oY?9@BmDfSpK7iV ziKX1*b;9Qp;KSfJyV9IsR{1Z_j8`0AeU1-_y;Kupn0*zDR_o>V-E$0`Ik)CHPnkRd zYyv}o+RmGuA$Rfb#WO7Z;t*O`8@VL=V3JQH1SzV02@8KLc<@3Rj_VI)VOLVuXA0`~ zq!Ja$J&DVNWOFu-o9VR<(I%T}1=I4^VsYQv)!)*WnA~ue)9NI#Qk1_E&OT5Ij zW@&P#HPXxe@}r^IeoC36B5fmJ5Lx4MbE;BshbhOqmGJn{A`7j?=9m~$KHw4W%P3cISja=KFL zOnv1Q@w;31NXveexRKzyiHgiwSc%m`SlH&3zZmN)<7R%}I)Cvz6}(vw^@V7!S;(Gi zeG})%aJyU^%gam;)lh`N4Mh3w(PoB_>dsYjo`#8(>VOirW7 z+FoJ!&!6g_7P-2!_*Mm4e-Wq<`s|yC0QG``0=Y6rMQkQK@!A@9dV)>)QCG&)QknJG z(TLMOYn6Jb@2$B#qe^0pEIq~VmAPi5v3DhC&{$ghb}WgksF`(p_T|>G(y`oubl%_C z##rMg5u?EOcRs?+gb`XUShwpwyKB87uA|L{;+0=m7+7^5$Q=9zcQ_f*a&d6`y`(Y3 zg_4}kUbJJk@M-FMaHo&LlW*TRqBmk-XbdvyZK{d zAAb9*C=}`jYd_TQ6b={eydVFN@Wyv^98)0i`gD(Rb%{-z3TpBoq`PiS3z-@2<5kCnL9f5Z+`oDcG%i|A;qU z{h_{EjZFkPp56DR%1Cn@@J>HEJ`I+L4r6uA9Z6#=4`T!~6V#CkW=mHx4G+SsHI9S2caa zJNv$D^zB05xeBNUwVck?*c6&t19CD?qi=A^_%W{NHD6!XDPie$4mq(vIJm9*IQ%<< zAB{DW-uR!7;a1qmMk%|B9($d*M`T*cQ%S$>2ww9;`nP-Q=0 z->~v2cGZm(!FCD?CpzUH0bJobIGDb7Oa`;o0t?-YiXl3DPa+45_wW~9AXY$vib%k3lQYJjLMWcdGlhn5!-1z78vH;rn3&Yv4>`O*%v5!|FW0k z2s$ljV$Ha?ro_z%FAIg9%G#r9LB`MF+t1Z)t6kNYJgW2uiBy_65e!()M6Axs9h#_gHHb)3~!#DUQ&d^ zB>d)x|I=GdQO&sOgq~73HN+)S%Z-k0t@_S~Fni_8 zNlle!)kh{Bc8OdD<)2EiU!ano+&jGAmbQ`zMNzW-riVaNY(ElwP- zg5f628A@s<)!&01BsGfhiN%X>Fnw9P2t!6o|0f_?Qp1QN@A9QrJVCD#G(PMBVmnpo z0y*jL0^f##R{=KV@sz~+FH8rdyBui{@$?yv5?3#6#IIP^i*;rl`ta!!liJ+%IaX%< zPobK#^w{I~1uR@I{*!6pS^yp(7_$aUZ|)g>kd%z4h0=^Q&M>?PC$ zkGZ|7gwWc-wa;ARU8UC(Qv?0bSeG*5Tz4spf>Vc1$l9c6LQi1Mq66Hb?I@@3&fFj7GNn3>b?#6Cm?J$o0^-9_~H!MJRHLGD)XYh2OTQmAF`)L zwv1mPZSLifef}xJ{3@4dYcGS7V?xQIkt`F4@JP zR{V+Sz%L5~MZu0Q;VB8f#tQb|b(NL{YWCI@)8)z@GVfX6_do6+rs7XO2G zv1m72*I0YDW3pvDUI78nnNq}SQ{lfcZGV>(k*;QZU=sORdFQn%!)&bw>84kWrRfjv zQpWT5!}_uCi`?0VQ|q6rbJI4Po>X9rpE8M>c=bxddEk`oYZ8KGoapk%G`p3~sqlVMg+>FI zdz{YsCySX44Z|*d@QB`harORvUjNapX?1`PB{)(ZCM2Bq#Fx6$I}n%Eq%aMm(wTgS!uPMK_c{s z35UL$f~P$9Erb1Mz7j+L*{mnEKnUIRA#gh>S|a`gNi1>%YAA0nuV#7T3xXD)5-*J8~ak%lY!O*8xiJ+;j6UZVxBX%>#>PjL3*) z6X#|;7IcWD*qQTVLD~T{SF#1!e|%|K;S~lo5&MnSlYd#TPQQBp%)@@D+?XXTDQU}- zan-+Lszu1TYcPVGLIre1Txp>?q*9>Y-xLdWuExYswn|jyDN)2Rk z2ZoPww@V~EUJqoKbC2DOIdJ{t7JUWkPrc9oj;bzf~3C>s_jPkrVZWQw@M@7 zm~BaRb|LF=8Se2y7Gfl~%vjx3*! zdcmmroNXlwd8x{F$7Jj8O8fUmcvjxPXrC5|RfW`AU%>TXd3pJihlj!U49ndQ7gRuh zZK8iGcDmZ8SSR~nn)Z6D3rCs%Te=^tRHuU;Q6H{8pQ0DA0r7F=zHmFOz8Ld6$P4@w zJ+&fS*!Io4{M$?Of2axRAEWv7y`;2sDA=GQ+~Hs)uEkKy4%GgY{rjtMgNr1f^d_gC zIOMg}#G-Wvf56&#)OX>HNFgQ3;Jyt`-oL+VHwdXY1QuEHvc#f%a=q{5rZ|Npol)tu zHG0D7uOKLWo1rLA-Bm80aT%K1__O2*!Ji6^$0b&Z?$wvie0owMrK}u{-*w4ysON~v zk1Zr-?zZPX)2FS$!B!0~iR0*YHmMqo66ti3p==<-j=+6e)bezb;_w@=~>J^d?sxdYQ!phj&ahjeE-WrfnI!{2wO zpQBJCRr*$07tc1@LDDBiQCnJi73346q6e=_RZOJ=W6)#4h8w;J&yC^|;q{h>nIJ1= zd-x9TGMooS+b6kE8wV9y{>y`&WHH#cV?pI~u&UB+(~^svjxiM4TR@tk7|xn(?W?I$K=l>kf!Y6p46 z!(`=QY42+FLp3&VfZqzLC@j>Q&|nC9yMnM6m+>5PQSErz&d_7=vk2rxcF6Hsh`2H%yP z-9{Q=IPXWn6r02b)_W!!@=E9)(Ik{4vrgd}mPdlf>6$38Rfmgb5qTGvaC?liWB^ZeZlWE>D%xN7 zGG6QX)=2CL3?o+vN}U6ZPedIG8Hpg}!SfLo_BzLsO(TK0JxD)wYB0csIh3~~rpnW` zC_^DwoTo1|5~K9J^L_FCz`qyCw0jN;QW$0%A3`eMSKT;jPV;sdzK)}hwpg|5_`-+{ zk{sG@m9+B|-*bL8yJQBnr$((~N5o@Ao+!wl+%BCK0S%<9TyA7zS7J-YX{jvJTxe>z zRDuS~g;?K{C)i5RRt$Zl)|~M5K2Y`iIRcNUyfR}NZXCFKvn3lyuMI$1*F=#>%zg9w zV2Q?)dG?558mH_#+mSaN<-ZO{IKhb$5{hue?t+O0ztSo8j#O7udTmZ-_kcbxy*;;K zPQ^RR{o&k&W?|M&x}tY1k5Ad_DvaNWVQuN%EOpQF71__J#DVEP;iNoI#5X`tT7HJi ziKc(k3n4P;t;;s4bA!@K*@%z-3o%1qj=*%i79{0xS zw=sEZ%AqQb%O+E{mV)&vqufEJ@g;D1&vW4d7nT-Y7xOT6G@|+&WLo#yHbhtMsSF-{ z%!D;HHI_xgPBO4pl)41OD4@@tW@4dT?j_P6pUf!9GGfDQ|EC&VR z0&pLN*sI)gG`Vy=e4u!1+kZfwfbUrJ{?XSrcgcHxp}p@nx(UZI6+}5)w|g-p(vhCQ z=e3YLP_0SXi@6c8trtmk6)%>=i0=&FdV(mb&$?3v2#VuF?lKf|rhh-xT;5OEN`PM5 zIXiD6(q&~21P^{?&WYJGixD14{}I)N1Jlp2n0mmIToG;0LA^&%uuL+M^LVD=YKvaL z&URAZPfmY*r*;YYKda5E=eF;MQ}u4`AJBonT{);xfAh(^THK5AtDmkg&zToz*sd<7 zO9_zWFH0!SPK;2Px3`n$&*WB4S~IBhjwN+OuDs=*m%ROUDiOMv=4vBZls;a&O`~?U zd6WZR5CtbbFX(;f)(fK|N4(%7nsXUlmq-IVv* z2gGEK?eU_8%-8&4$bq?*Po}|eo+?)8KXDtMpG%}CePfbNp#x_XRxT3>Lzaq(B zY}Gc1UX9{B)_?HF^^thpv%l&a-wI9v9MthF>LFQzh(4Yuw?4-9n>n2JOJk#eTDYLMh4rEe&tqa}X*fM@L6W3Kn|mrzYJ)hB_!1WB{zam4;HG>2BaorJDVK@jUc!*)56`C~3Q zrTVuy-4;YR*g`@@wMKt#Oo@dnvw6RvK`!TqiK*Pe1P3q;Q6dFj-`Uum%QPIacRH;) zb?4yZGNkLx4#nwD`Fv=!A@Jj}DiQRug%~pLl;(`H}8-s1-hF0b~S9 zMY$g~46atT)SuXMt;7c&=jhUyP`PdWeUOIT>GS?~_9wHAA)}L}mSlUEFJ<4g#aLDg z-?G0-+JWu(9)*?5ERTP`D8t28lZYiHX@3kKJ@vSywfNmqvuv_^cBR)?uYI;1L9g_q zAGfqK37ZdDEHnvjEDh$41*1#ZWK?blvI6=b^~pP>FQb6@*?d- zMM`p;fmdM>Vazbi-E_=ShARxe#F>-%LO(dFFI3YsJ_p*n>E|_Rg9;`}a+aB=F0a*w z{tWI=OP%f&=Bj|k)RX6re^$rgGRwaAD$ajWibm^m*iV{t(@5V-tR#2D%@AcFW=?2k z-Vsfqs*ea+V*o-Q#_(Y`!MuN#2u0pkJx`7VEd5}^U8ms^A=2gv*tX@*%E}6)6$Nc% z=v^3nki&RyO3iZ(w8uO&Q$*IOzR&|J@v~#u`I^RBpGwX!@j@r7aDQrsJZ={9j}gZH z!jrKCS|8Jzz)ia4=6J6adcLRUKdH?6o8&WmKdkW+NY}^(YVDK^=#LR+!+vso%4~?7 zMK~arfP$xFkv+Eg)nF#$HM>%gwA{i!C*5*K6Y+KR zj62D2SuW+8<|2!=r9|z?jX|g4%h_ZULnVH1@yt+>K^X`1$R<*cUq0CjPSH#sk-M$V zCam8ZHZIPA0<;p}2C&p-DaY)gvG8XYpL@u+K8TN8Uh_|9Z5LG@x_9mDNUmaR>crNd zQig0xrR&$P3qi3f&kY5d2GY5;Ke@}_fQT?UC*JFQ3$8XSA?S4&FS~6ZbF&bA8eGJI z46xt8&+J5b?-(2^9?2aFliYt0-*dNr4_n1Sdwz$2{-ANW-RBzmpN}Oe1|HbAD7lTD z9U*<0!DAN#jh8$YeUc{c%vAPizElZ9`s34SV ztR!tc&-L)76F%oSQj$_dNcQwQuyY*JZYoI2AO9erp$|NWVB~X^BD7Z2POi$`At~Rv1t-73)Z0 z?Q#^?;P*NB)UMR>&cE>k>4UEKw(-_(EeJihEt-QSN-XEC<5c2=+96rTV$Q2Mx~_uF z$vLw*CC)Dj1E<#0EZc)lI&@0e^C?;%skCCJrwrY@#E}CN%*~6HZ#o_2Vp@F=VbWuG zG4rfpzLj{xH*dNtavoculg2XA=VHRzV^&}liI-y&Hj_3-BXXk$ecJsX2(h~OvGhUE zVd^vTi(u9wnD{i2>CB$zSt)wZt;|v*6W(R&O>fhZlSA4Z(T>tP@zgTE+~hv;QhjX- zFZJarhD;Nh>%lzIKY4~dNlHQ@XmHS?dP24^_~W_ez>Sr_vy4AM$`)bKf=QL=m+NlL zSDg0eqosV5d6lsdEf~;!5E^d6+C@qb;S>J)Zsrc(+&X|CT#E*x?5{j91rskE)zuqV zV5NB|gxDJ2>L_%a0U7g>6?R{gZ9LkR@|%o#GTh!UG~+glqKKBa=Y}Aoa`Xl`fK9x{ z5w9Rw7Jz0RDo!gje~P2EPtXf^nmmqXqC@usI?uQ=MNd}l z6WN|8wP3;n!F>H^cQCz9@vNpwy&gaYv@=-<=~&HbxZUd-gWzd=<^XKmx2C?GS)IT| zi~HCrQc03#S025DzJ<9t^TOfcn(Ra#S?19{Z`<2l#EeqgTda;I-oM`j7e{9&oDSKn zI9b#z+|aEO)EtOLVdUsRXuim{-QC-+l1^zIpTJAO3`PzrP)Yt8XUJ znt+)Qs59iaJcSj5uH{>(ymP{_A%iL&EC2uvyA3!3X4=;t!KU$T1WS;f6!xEZ4kg@w zNB8|D*L|`T%D5^CDM>YwCF3gM62+_M4|!;sNYhimjQJZYnm~qM1H=A*D06xj5soq9 zt+?>zwqBGpb*b<^5cF7_4{dw1ps4VvR68>SY2%w{@o?;3)jyE|J1hZSfcmdcXuzVx zEgnyHZQA0fosSw-y3{HW1-bq8wxFMXa+e?Puh|6j<=I5?S%k6YmmTR+Fr7Oph^#2f z6Uo}jt!I;yl5V<6Jmt%~hFh=Y+P!buoN6YyCeiCrj*e@RH%zCQ3o~pA8&D-XH(X;m zU(cUqF8anVkAA)yR`Km_=lPi-3OwIBpBx&C&o63HnAy+S`6&5eAC>LV)VSZmHTc6r z9(O+|PJIixC>SI;Bt!|+jVz#i4+W&>n0n#U%d-%!qg9z7k! z**=SvHUH$VyV@iY;_ELmUE5+1{HxNc;J8-Yb1vv}@CARJp6(%M7Jjnz@}XUIDLW(S zRgB5WN$H;sHQUtiN@tT)e}h`ZiBaHu`ZOi{v=~m>w>o{k);q_%n3A|j-grhEX>_oI zp#J=fRbP?<(csCwQB@BXf%QKYVH0u|+`xjY0^RMssj7++TvU0ztN4GIdhd9u`~QFZ zAXG*gM5#11tYkZxB}%0T>5y!)vQtzj4T=`odvuKKQAtKfW^s}cvQAc%tTKN0*L7Xr z&+q-u^?tu^@2<)@uh;YWc--fuZKPhd3v-ObnT{LS3~HK=HRA+oY))(ghCP3Hpv{WC z_~Bz&E-ya`JTCB&I2~%7fk+`ky4TgKQVx%h=*;AkZ~0N|)%P8wIxXqwHTs60OKwTU z<6pk;`dFGC*cz*4ntEdQmJ|QjIs}(5eSM`jWDYdM;qPMGjm3>Vp$Xo*pM0d4)F+r( z-QFA{$#>nx|4@A9M&mbobY;)dL$uz$+UCp$mh?r_#O!7J?Pkn3mtEy^SkrFSx{^Y} z16oyXmop2q56wt`MA#RO9O08vTs}U<4O6hsV+|Xa?@!l^PqTVW^SQ;aJ6*ghh_fHl zn20&^yZ)c=U10!$zK$?-wp|-|zc!q;kE)scSo}ysW25H76bUEliq$3RAt~1~d0E*u?QtVTfD^(NIQNbGx}na%#DKH;isKv-3X(f&f9egou<&lM1tLYX zeuzD5DV5a?m8=xs#g#0Ih326WJm51KaX-5skF=qmWG4^)d1b8U?S&UK@%ur(v9Q^! zv||2OD80oM@u7FbxwW?&77)X|an{*NT&w|y18X729P)X%{O3!V|ctLP!TK5WhSR^^KP z4>4dD?sH4GR~Oof?uMftZ?V)filpVaz6ERq_=1{2HMocPBWTn~4tgp`@;vJ`gh+T@ zDP8B+fHF+50poYcF*xC8)cB@n;+du@Rriz1DB*f^??e-qlU#=0ti4R6uJBxuEg+s6 z3yD=(ZJpKC8fTbzruhb3vq4@V2No2Q=>i2lgohkYH3bnMaXc9*rgb?8xtfOUfNJwx zjSagWVwj=0bJ?2VN9|DrpP#M+f&LY(R(chE>Y~(Zsns@Vz6u9U@hc;SJOolMga3Kw zNugV4#54!yL?ejbw`bxGQee-P{^E9s#is+*gi3B+`Z2M@N%)!Mw&6hPEV{(n{D&dg zwGiUl%uA*C%vV$r0W)7;UuJv4-n?21y_B5j6=cOA?K4d1G)cX$dlp~Ft*9TneP}y^ z0^YkU8LYv7I1guRQa_$&Mrbo&DkbIXmNf*SZ}B5znihGBt$QDY(`)aHg8FA>KYaG| z^5h+5k#MaasXtE99dJBQjz9Yl#+)CO3oM?B z!JY;5?>m;i`rwXNm1(Plci8KFyVfgL|2b`HcxqlMMZ&R%>dtKNLSb@2{*tY~iU zY$5NeKWT5;M-&(L%42L9_b9ULfkYa4aVB}~7)=WTSPZLLHOtYSvOCt94og#<`tN#`RML=uVWgh=r z{9IJ(kGFy7lE<|{iDcTcjvUkacdq%4(hA&?NM!8t=ybs9lifM~sZsTgKkaU!Mz9%N zEZwdOaf7_X&o{XH4yNReK|nNG`R@36q4|3;J&$ocv^#*egn6MvlBhR!^0PA zUpz6^%Phg+<2`aFma+MR&yTOK&#KMU2RVVd>gzMx>{QZj--oDSIjj}{r92v_DHR@h z?P@QL)Du5)-PwcwE@WQ4y5GcJ`tlb83D0pG@AFpsSM%NCe=ikvCNmz^7ojn@!SC>f z+BV84KwlJRay&;xMCH24D3=fW92jnO9CBBf*%y=EU^V(>hBbforDW$xfy;Y0_~9j| z!!iMf$MM2&-(wiL)An0@>6_-Vu5e)TB@v-A#lHHlVbS_L^e$sVW50OWUU5>;ElP+$P&}{z zo?EfUD{T%uZK$tDZhiu`>{-a&GJh#MuTC>>vi9>;PuB4T4z94Z&Fu^l#P~T`ArQN} z{!>IlO8&8&v`T9W43gJDX4{ME1ztZQ>o_&o_ zGgIh(T^^_(E*rFM*7z*m7&)@?=hfExpd^Tz8+f=@n(!S@C_Js5rNZ%9*M$5)E2`bu zf+f1$eA;_BKP}ucfNR~8N}dSVDuU<`ymtzngc;VZ9UkpNs+%9EV?|BwGiCYqBv-E> zJe)5~6gTz!^Dmrz>sos`efGM#ij=0Q1_z9zlPk3MR?{YDq<(FM*0P!Lsg|#Bb4AK? zJwLpnAw{?xu$AF6#ruGNQlBc|v(H`ggb_Lz#@ob=c6pbKiCtvnH?EK?-EFvEAn#F6 z^o8XEEES=x0w)Ac1d@IT=vSZPF89gnPSh6*{@{2i<3lYGL?y>F`8A{ ztkRTt&J^l@bp5?sz4_opQ|v0}_iD`R1hVfZ>%<-jX?VbkJd>L+a5EYm7a;Plx})@ayQyPN=QuCSe#E9v%FLLGKYWLjALTtFPT~rge6U%T(Wan= zC=BYDL%iTyi@_(gy9kAysIr^bcQwqA`xxSYYEx4A)n8A zi9#vqjqdTMADZ|Z#A>DJuXxS@YO157iFfZ-C2s>pqhL#op*U;F*Tz!X8N!ESraz2A5!J~vo9Zoa{`3Kx<^bK0AzgXp6EKiyb z{|bm6eq~sLk_K}=+VleMV`+X7#hIV>1mmZ;XjCLdn`2Hmj;K*MR3J6FST+On`{K3! zmpB=&1U2+9AOP#v*LMP3HhR`_))uOXq9P*iXEV_!R8hfbJQdM)H*8l{@GN8+rq0f5 zq5Q%!0(ojodcQWiA+8aV|E>`PGoTu@^uZ?=EEy!98%y*Xp#p@53Y-MtiO+mMn0LlC zHjoJYJrfCX1X3LLcO&O2ZUmrEs`+hJ&42t7ap?6}Ze6d-OR3Yv=eUB;xslT?;!tO@ z{?6#+x5~NKZ~q=7WooWAL`Gr9u6}TJqPwg`ctT9KQ+!>bTh2jCEkWC;jIoN-~; zW%c>)RBkU9WI9&(T0}%d0J)-AM>{UUF^yjed^G!vb4>lyauqh~(?TJ;;>3$w!3bay zY)}Q>sPn*N#@oe3CuSRzcaW%<<=n-$g=9k{ARbTO5PQmOJ$%>LIf(`&S=rQk){XV` zaeyZv5W*3s(w*@RV+wwKnKSZ#&35?elOeF(hAAJW{@6AZ|#=L#ka!aTc6LQbqHoO?qfw{i=+ zNP~#M*5K6fzj!jV)4OuEjKZMb7GoYaed5oq6yNO^_`BvpYHDiLIW%Q$a-T@kOMmz6Pu<&vx~_^MUH5 zb_`qG|My2@Yuy%)DeLXJu%AZpwf31bbeaV=G(ci)_dO_85D&ov=N)=*|| zXiChTdBtT`iUSYCLn;+f8mk#{uO+M>9~0ge&m^i)*V?)P39JVY*oVOuJ|$ZkSD&)I z%x1i!JR;fGj%+!@EodQdgyu@4rhIzUGJFe^=hB| zGmgA%9 zwtc-VJHsHR)FC(==2orusDV%9UyH-F|bMwRd@=8iCOasW_ z^Lt`UrZGSQ6QrJ|YJQ7;`r222v=uu9OPt<56Q_4xAI8A}?s{m(?K9Fl@@1qTq9KE{ zMkKwqsj;!bDi8YNQ=3l->Ysi#wN>({6?ZFr>X?sN=@5jvcPf2xbx~v1KhqK68T^CX z9@~OVuU@|<4g`!kS3|W@2fK0dB9R%CMmq>y8_{_Cle+ZM)YKY$?_;+l{|jwE%qucR z${_e8BuNChL{N_;H&K>?sv7vFk%j+>5^sZVvCqDRPic9Xp-VVqHNFD8SE zb;ml}Pl(^dcdacH^6uEqs$4uTdJaqB^kCPr(Or_fe2Ki@ol3vBxXw*}lNl1qGq>jz z<+dwy2#>Zc_=9sc%XAe%Hh;G0ekOVcSTi=M&j$6p-jiK--G?`vmTbuy34IN)EmGpoD^j=)teunPTbnT7t#uF!iW5cs#7i)f;KYg#&Zc+V%&o}C1>I?dqo>nzl zUDK0e&Wbq&{m30QrSL6J#TULf5GNCOiSj>pzkIY7RFx~>)s0;ovt}U0{_Ua<)W{+l zZ_Okuk_7fl1km((66RXJOkf+2?-{>z5o}cYYtOgFhM5Q#F30T+{V~k(`-#fXM@N%H zd6DdlZSx=Zevh7YjB+Wvy-#Rf4&oa~qI&`u{6+>KR_sL$=z5KTp5?_J!JUmF&;L|* zcXp;Td~H{PVC}j6D)QE&UG^BE-Q>v3&bT2m@9^7P49m{_RND>75|d4i^T%)W^p2%L zC%Jd$^r8<0Y(ag^6OsV;?jMy?%x?NZ`j34+FDL?E#oAlZ_%1XCw=xFqi-G;}&&y`9 zst_swMYk9PeHfAIc)1?0edzvv+{3PzTc7{vE)|agU`m;KD}tHoIJc%NtJDqah8G{@ zw&NAO#u9Uxs{8nWo*zRTt&|%ogwTDPI)C%--57YJTt0jJe!a2LB^g?{RZ36SZMVr@ zRR{h`gWZSXe%$`>Dv5UBq-#(nSViB&ix%Vt*HFTr}LM%n5q%Ss77W2P<=j-b05_(7yX?b>e=|X!f z!-mcT;=*Xf-ajl&zkAS$MS;9a_{qPeDHFy`SbAxH*HYB3>b>mypl zodnp;z6i18^Xp2tq${nlznLI;+@lb8e4Xy?T_KO|*O*SecaL_{tp98y<%BHoRWbhd zXF+VG?F2tVK*e}~g#7qCo9vKe=dypV!FQ0IAwobDm0vmVF*afD34FYL`GE5BsS&Os`fA1YZf^OR z9jaTl2QYrNmByKm=jGVzM>YLG^~#asf-pO~ONz@}yhoq8vDCE8KyrRX(B~u@%RPK6-9AKca&TOvF%?h_ zv4(_gj-&ZidTf_o`4W{(rZUr>{Lfq&Rq{82(ADKYw;2ydHz4aImr?6B&zEmDyUr;Cp!j{hnyoBz?@mYDuW^24Fcp zy!*)`pDM>9R;k?wz6|A+Pv}XceB|9QC3)%dw_~X-2&3W3U8%BBk;s(5bVJbIJC3Ke z9Fn!=Wl@^R39cmkyBMVca3^35cx=^q5G!I6W$WM<&x$9TZJjK6~?n7D4A;zPy*v^Vd%fCwHK)n29vE zy!3W?jY{1k9mD8WXZ)>A6uM^$i&oK6u;fU$871cQHK(Me3X*g?jb4=e(#*6~k+k#V z(&bcP8tA}GQ+`ou6W~`Jd6?D=y`OiTPp5&M&AvQgd~)`tFK6`8nEi%*&uP#?NN-t7 zn>Qn|i4}zGI>^ zS|sZ{8ewr3tk+tf)UcX)HZg^n9vptDyN5u~{>cm6&J%dib7iH?ume|X<47(= z$jHjyzokI16Y0)?A?&+L_%O7IvojGTk24EDz3~GgYURi!;&Dk^gK5O@ya>etQ-u zmO`G@PW52Vz1Nrc*oh&75;AdEU{^`|$w>=wE5x;`x z>B0k&$J}kdoxY% zfFh?gED`HOGHnpUecwZOvX%fBR|7k_1uHG=Wj=9HHv#3|%ii9gpv(_Mh!yW&2l@Ko z)i=geE}%hF6HkS$08`DDmlktRy)k~~A&~3n{)%&|0HaQK1~q{NX)J6+2}A!Q8hA$& zls&2a)N3D0WXb82@YCK?;5w0!|n(l|;sb4?tKws5Q&Q%rXNLhP5^-rg=_-Bw~0 z4Te1s-#;dQ5F{r~o?JEkf?R|jWXI4Kq{-m51`lM@CX4hYU|kq8h+PNeukQf$;t4T@ zwKce=K$D?8uT@0xre$giHRt%x=@5fl-O>CYzq_qUrao=8L=+T2)mjVUw)?Cn0}2?g<_* zz;(}UP>@hQ2nvmIxbmAgntDDANhaTYCS}FME)-cVO>~9U}n}QhXepwq9-9je?{{LPWbz<(IjqL+)LWBXL3mPEuTu=JxmxxX1Xaa3$$OI64Toj zdRYbM;yA#kAKv!oA^$9pxzN?`31)=ki6GX0q7DItBT;6HPv}k?7Kc-(PNB-s1YiHA zFMKxlgwkrHZ(0qReh}k5I93k3?+R=?U4Cck}U|nKE^#D{t!U>34+)5|JtSn(XPSCEM^Af|EFW z=@PIFl39*Hr|VSO**BVgYmAj2gkNVQ)}>xymTka0FmF&cZ@#&*;4!H5k1+U!Gy-5HE*R!|{6LtoY@FqJ z4F7;Z%y`HrtdmY#F;x*K4eu4RmY@}hoM%BDJ$iyjy2aIdlqL>qS;(GE4?ak0Q+1^l zxdIvL{?9wVVy3<6y1tt-Ox52FK=ag=9;m}-T)_($$kto>mP07Cp^2$u ztag*C;{Lo4O+m~jCvU8X)WFJ1fBl7Dcax(9`0mK)oYJ%^UDX# zhkwff{{8tnbAI-gUA(r}OaPP4!1jYT}Pl$;l?Ptnn42OFd4&}9PaGJppYA-1* zE!gxT&A(zbrc6o|$wzmbthWlo1L7?q+UW3GpVmHQ1m4N8Imy@>l^OWI3hYP}Ij-re zb%PRc8xOp$@*W>4!3+xMsRA*0l-*W~LH!p~L7qQ&9PuR%aOMdI+X|n4*rN&1fGHpO zanUyaFI|wx=K7N-8P1N&j8P?Z-6EV+;44CT^oFB2KU*tJ@fo2f2X`A>XT*1iS9<2h z*FX#froc?+x@SvM<^NvttlUh>=l2Jhdf+evnfJ>WJ1i5cv~aO7#QDD^1`dnO`eLo6 z?kp?o=iY+ys)B*GAg3LX(*c|Ik}~c0rysg8ef9<b%CClsUC{__?n%>z}L5JOceBb5z0`lEM7~fyAf@%9$NUWy} zBm^LYdnmfeZQ*1EkmNDmk!*Q^M!GjNeKp7ND;YU~by2u0G9@Mfufe!wSeZp+Pk3C( ztQScaYS|?b2S*hyV26eN14h(va3`XTJDy42wJ>C7x`osX_6xT^etMuC`@rQToZ(P6 zAV6&ImlYAFyf6c2E;I=|Y?P1n#V_Pr0>gq8ac%V(cMXt~L^syU7_6tT_9q8aBRg3j zNtA^WQVw>)xuf=no5HnC_<4KX0yg|w@Kge`vrgpq-%DkXPn!>G2j8xEiD`9 zFnSrDn1plhDTP8$)Tx(1XD-gIR;gioq~=>gpN+n{CcMnVaOaU-2OCzsW#XGlX}Ksm z*LzLMP>vt9*lmS_U6Paf+@ITRvD4wE8Eo$^bTI7vf@mwc<9~@p2SJGwv=LF4#Z%)W zqKs)APFf{G+D?cdi1uqj9f57y1cw)KpsW2xqN^G4KM z=V)Csdl`|lr`PCf>KXr=6MFt`+$X?^3woLv244MPJAJc`di4@-UGbV1NErt9nxLNf z<5{f*Bng&pHt9OOhUTGR`_Lj>R4xjY1}r7%q#IV@;ny0$d!DK@$IRlqq1NYJsDNW7 zdvQl2-Ft zfa><2l3@I6B%2g=c0MnTz-3R7!hKUwrv7F#_x_gJucm?;U#(!Y!QF#ss>lmw8j$qt zHhBNnTY>)=%GHr5h)3eZDd~cQL-8k-Eb57(4TNz|{9ksmGXqu`2 zq(4U#nnj@C{e;e8#eRkT1%Bn0WiD@vDa^U;_7#0eEW3whglX|p=wGih>`rJ9&bsHi z^rsC}oT3OnXOSK^p>2nIgDnh;;U|C<&x2aN|4v{1-Cl1Ly93(}-rrP`OQWKpkL+ zJHunCz~7Pa6|Y0Rdz)o8OFx}>29O`Lk8TGAhvIv2ih~zH_6m&O9O3_IFxr--Q9yC9 zD`2F=?$`Hx|Fnun-sGjuJ03xj=TNxy03VFQ7zJw@qYwJ)`pj)-{O;xD1vx%S4x3Y0 zSA1{sNd2Gzx4-GJDM|1dc-X*m0Co^;KDle14mUCje;F}}ewGb6@Bo<0fMk`*v1>ud zcF4a@gr5DUnz^@y?6&f(@fATLng~)+tlCo@6zUK4q(Z4N)p8b*qpS z;c$(PVQc*!TLRy5vvP)kEs&6ptJJ9)5Lmc2>1%vaX*H74_R-8d&kwXlf zp%dt|a}>p5QNyR6KSsiEpAiYV7JSSb82+(frP0!KLv;;}jGo$ZlH`97#}Wii*ug+V z$9E5=e|3B&!^dKWg$sKFj23K%417?Z4)4{uz^(L@}Zbc8js zfy)_J^|Yk$h!M=qh=<@HNFnIZ2u>(I37IB!54Ld>ry*UP_m1Q_jZbj zh?F@N*#+&VwbNb_Lo>mdHWHbZO~-HCP$tsF#`y;3pVKn&iqH0z`7)6xjbj4?7M)Yy zU0xE8l%7AJ_zwg2*8IG@Wgt2qo`9xX^Yp2l6Rt=0qXq@?ZNkqWwBz1xq*M;^7^FBG zQz*9U-`q!N_4}`gi#S5^S^4ObuKmi`JS;NfUiWAm)5hYY1gdZP)iv^$GO{1X2hg$R z2sfR@dWD}1$k*+&5|IroKs<3&UfC_TrbJ(Itsi#Hz5h>%dE?flmxDYCxNo=24jnmr@()m(9$Y<- z&A~;PrccE=KJMiw8U`U`jsd&J>o?IWA)N{C~(~uEP82*l(7AJ|vZf?Fh zvr>V6ZTory)XkV&^U=?a`++$BfvNP}GB(~y22}%vFyU4-B z%v!)zp)`dA0rKq&hF-PcxFK_%L?D;#9DCsd@uQ}N;p7znMhPd#Ki$yicdq)FsNqiy z?1xt$EvqQDWyAIle+rxKmo&3#pMMxN6mJsz$_iCbLysG*1n(ZOc9$nAQcp8tq6K%$ z5KHFgbiRKU3KS}JpK~!J_2OizZbEd1u24KaZ~Uwf&F9XftydE-?s#S8;aH!a9{u8; z+`52ujG`K!hTSx?4TM7Qx0NzdhmWOz;@?EKGmp~=PjBtd^`9WpA@iD)gzKPG*vuh% zZ?(b4I9GiU9__3)Shc`!r3jM0j~GA=4?O$UhRSc*3b55szJ$p+%0zN&6d8!1_!1x} zGH8#Ryz|&A;S?^d?4$4RBPu`iQuaR2^;-WkvEK#kzWYQcp7I0 zlo;}t=WU|~V55SzDh2=ug9ofO??u3AR45QY_h4|-H~1$IS!h)EGb{@51i}zHeye>6 z;ti}?Asny2uhju7FA81*F_fLWe7MR0_y}aAKr8AN^&aSP_ccMHBM!d=@_h{i`ZUSI z&d>X98)Z43x_^3Vd0%{|eW8qZ4YK;hP2D3T`%cx@Foe3B*Vh|ZZ~)D8B1b2LoAchj zyOAPd@IQOZKbB5(tOfTh49Q|$A&iqa5D!-NC0ZBeo2A^&9`R7_89g@h%Tqsk5#0n8Ml<5^Vi`jrBP?B+i zG;E}0&V7?!>A*V&{F?fud9C-?QRB;9GOV;KCI^`fkLms~;bwAyef0S4%?6G|we+bD zd1rgZnt7!V%~dAO=@EXEmZ zo^~uekUwCK!ZXLnDUmyqKd>{M|lj_QNyrp3#365IZCvyR@_`dsui?(YOS6~&sbYh;tx~@4%-0*4)wg;b>@;a=hFWi8; zJMz-BJPRc@5~w1OhuZ11P20;PSk!3w-z{*Y0^&}S0!{{~a#OXR?~3MEvqMdmc)1l~ zOYUG6>ZrkFG;lSH>)Vxx&Smz>ha;@BP@5l8y6AnT*4UpxRslWdG=8=Zy zDTQ7?=S?JbAnu#_0rwM>J9Nmt6C}nHoBuYLor0pB$_9$0IK6*RAO-a3S zz%`|9g!HJ^0-HMC=K)sl#Xr;dnHc|A&cyf_q~$V|VZno;cjbdn3`gG`s zWYd9kHe0y0k<~W^i6=+^l^3gYGr!pp#YMnS?g72(lrk9-dY>L}P z?~-mn8!mmk8${MD(j`CX%L0qSJxTQ;)rG80ZP{0^Z={7}k1)`n3R`QCMV&9CZFjSY z+Wr7E1QBJ4bQ_g6BSc)87IhxX)ydiM_~KPu|>$aJnF2!gE1Zi#wg4%A!2#@ zN2$8DmD$uWgOOz*mO6?9U7&rhs6&j|EX4FMP5q_O=d>#;n$xT2d-F~p{^6ex$Hsr% z{%W7?Wr*Nq)Z(4H{$-zy2R-_7ALKNjqu{woq#EIOB8jp_`>u{&XEmmmdDrg6OLZa6 zG^&ZIpBxym->1?=bYKlAC(?lknuatmjicmk%Kow(V&4Hd4F7=YZ%rSb89@4sa4Np& zCw@T`tJgpeeBnn6Znml22^N(Y(hq_zmLVjZA+QeLrin0g>vtC(O{W-V+4Wk)g?!bM zd_FXmhNA{Ja7MInkuXnS-wLWHyYWH#pAeN7ZfOP%SUn@siXwhy(QFZM@%Y+Xly6ul zs&-njh%~M@eAe3S*|=x+p4yI0EEO3{rqvoG(VCMDBk5s&_#iViM@p*9a%5P~(m1t) zC#J7j23?6+SRnn$GpUZ8yfM9i$U7|JMM&qbhPx3^DzJzo(O?P|Q9k%kQ5mNU3>ZrQ z(PV^g|5&s0$0HjHw#4=@VCV3Q1}GQyzI8@KLZ3mD|CVNZy^uiL?C;+K>+Z#YR)gY! z7|#S0TbAjg5YsjqGQl{k090;5?BT`%c-`JIj>SLDYOHEhhQ$MKjO+cIol_luG1_TEif zj!*Ln9ob(XTW?ieI9Won`OugTUBNB}AvT`n%P)-(jY4F|Vh=%Xg#p!5K?#w`>xt|r zdURtT=hmCzpAvaiGed?jUb;@ULSAC?0mi7yk|tNItWq+~s^tfM*3{I1vy%QSRP6k$z=*75oGSB}4eS;S; zNcpCnvGC1EK%)F|P9*#o6+5PXx11_!NCm=4YMj_Oy@cNqY`BS)mAc>lA9bsP(73~< zMf~|5uZf)CsoEw}2Q6p6*jgCA8$090=H z33c;>{dv_ECzN8Jk-}YTtp9qxgZFEOZvUSe_W;uyFo>kGo~7}gK74uhonqJ?Kx(Y} zmH|aRmb`eAWJ!W(#7saKv>(p8y(0qG9Zq~&ggXBXJVZpot>(Kg-wmjI<{{l4V7^EK zyj&zomh6o&hWzOvWjsNURd_aebtkS$qmJoYoxBTa2xO}+FEhSr>W&UPrDmQH65JDg z29j9LRa*?WnJPw(CVqS<^O6XvBatpzBaBsC!1k>|aMNf}=Qw5^Pi)D;^m*VHTg!4Q ztHy+Dw51Pu8e4U~a!=GR%YZ&HHg~*piT6Rq?ufHN{DPmJ0 zAe}U8ALCV~aSbtlppHjdklN>#^28Jk;LxkT;7tQI3AU5~MEMVt$uu!)U^QYouO317<@uxfe{;mzp42MlaKrO~(U1zi*44MJC za?q19J`Un;g-WJ=OE$SgOJBWJCFRWV_S+nrEv|qr0T7Do^N9afBZs)@-@o54?ov8r z`6x|49cYCHb_pI8{J!I^O;dG)F2DHkzzFaJf{#U2xUI?@EdF75%7b9V^;8g}Lyw*$ zQEA!Af-xoc{vQF5y$EO$Ah;{omk1h;Rsh}ZL_^bRW*%P;vI$Zp*Vg|RIF;A(6H(q< z39dr!1rSyW+uc!t;>Q)X3!S-^feU4yAc-D)QP|q7oy>PTetK-;OU~iC6|$Q+ z2u}hjlxfd2MfZ*qKBem=%3#_ribxZjP+t4HqPL3-Mm1VhilhQ`ZXLx^P z74D|afW&b3K4rhM0iknDM|X1Fz#q4+K8+hsTO3uDsV-z)BN25Z_Uwt>ke9Sq<269w z*O#VZ?HUELiA7ZgT5Owh9)R11E1Ss;{&+QMYL;B+F!;1`ji<3~$Qm0(SMMLRdDf2U z1H~~RoRW(o%^%99qCC66V`2~h!P?+Vmx1Vpa-?e5tzy_MVDI*EB00x(Ncz(wdNG0| ziSQKZH2bYJ-<;l0XCFaxtBvZe*w;sQF@ow;OpM+eapeJmR$fH*9K`QKj1tJT{9KIN z7W^wtetKCyPzxe(j*@k^vK{dW-WMRPGd;J{OCVj<)_tYVT}z=UINxZF5@`bcZVhVL z_au|tP;Y{*l%cdmH$b0!+}1s?IzuA3N^>m}hxFtM0eV;y-M>IH^F#LPU-;(lt(M9w z@#gf^U$cNWF8h2qvmgj6mQU03-kaLtO5@sZ~Q5Gp(R2YIXMcjD|EJ1g1%Ae?-5gd=e>H7H5{K5ei9&6u=F3PF5$wV@Pb z*9D%#klj_m7hK~EAe30!fl9!DfYb#786BX2$u!b{3wHxT;!Q8~lMBo?bN*9?aIt@z zf+rU?eBEv$NK`72xsGV+_}lq*(Gb299P1mcqnrsoOE{=%bN3_*?meO%1?i)?-cYyZ zUIuI-wZHW({vLIg$2x`n&-+9Nwr3c>@C5}te_ z2n7`=OV`}_C3Gwi@6%PR&Bz5bkPa(Hhf4m!HXn}{_G|l>aik`QjbjmT---Bo6GUI^ zR6UK)0{586MZCa769#k0ux92{{{5qk%4Ex-df{`ozw=HRr_t+k04B=G4_%2nd`|3{ zpm3wtyJ_!Jw?(G;kChwl-SF<7R8ho3zVy?3kNV(MxV4Tp_T4%9RTfz``nM_(BkO;! z-AU;=`+AuzUt6Q|0^jcS}CK*E%ATfnE=Dkho!}abMixUV@1@!@#y# z^^EYf*7Nbs$}H~ezkDoDa@qzqlZRhCpVAe3cChcUXNgw%>C`E~gZgk9#i`NQu>9R* z|G_50x)J&>U{L^|7&Yl+eU;V&2CKgPvX^1Q2TJn5m->IHUn@K&-psD{iQwf3SigjN zyt0Ywvj-vSB)%C4X9)dIh-Tl0)Rv3hzn;X}ZNnr^msdZNBB*jgXHx0c`<)$y zNqNj}H5BI0gh+pr@cma`fpKc-IsVw#2N3(Eyj#}O-7&}PgQT(gtj7_jn46Fyd!CBzGkV$=1zN5oCK z@RQ^14NYJ3lQR1f^!L?&VW&BdCdr%d-rk$AACaDmI_}#&gM5rLbqdHJEOdFXRfq>T zbeEiS+MhPH#?6W;ft2(r=8z-ke^hII9(ow`CM~d{4USg~WhYgmupSk7?1OVBQy##z zkdQ7rjB#CQ!HCGuz3=*dnU#g5qodUZil>#a6MFXRlx5+& zVk@rQeMIBN^azap<8>{$+kZ44aI0CJa!b=1@zuCSF-=7HOT|k36jm?9K`FFs)(pPLc>Iy z4MZaf#E)h$M)&Dz*9Q@Wi%6%#H@IHV&i0a1m#ASOp)bj8I%gg2ud(&Y3jDR%))iR> zzK5J1saVdC7*oZdLn11c`bwUM4**bsCIE$=oIuajK)i+?SjxQccB%Mpr0`~iSS@ZY z@-d*OAjUr3wuCKLHA?7*ZRpu8`ymESOd|8!dg-N(9cZIAZg*El@IXLSf{mwJ8U?>6 zgtFsdBa3Dj&n)qYWaupqq5x z(s;O~4RrNd8Em+Dmfiy3f$DU&0Er-E7Q+45QH)my0u==70qO+(!CwO*BEV0eVx}%G z>+rSVC4Fde^UoTb1Jzr%ZEH(g+qyQM8X-_(McSd_i6{sXSH)cmiT2yabz*bCTgq>f z4ARsylbuK(x%>R@$y28iksM%raQq+!cZQ>R?N0+Zqg@Kfarz<#Yqd|(CXqV{Cs-~~ zy?_9~>8SngQ2>Q7kwK+d#2nS7!C`Z>bA0;hL475ZB0#bw-sOA_O8m^H{SEtk3a8?< z`;*)4Dctb%AN{YMrJeYeNo9+(J;8UjUR{Y-5jl}Z@Npx1s^*~S;oPqoyHe=80H4j33z|<4vbrIx(fQtf7k>`x0;|O0F=~*YS zJv3J*+;m21aH0GTA%XC6Sb=SsA|lZ_0`s(8K#!10*Zse!D$>r>KMV&mj1gqZ=@>}g z2>r3kWW@a2cCx&u6vT|c>j1&t>rZj)pZ$A4PVPDLwt69;8BoS~uq)vt(N&x4_7@0|QGJTsDq~`FRG><&H}PP7f!k2Y}b@>sg`u)3G?ZyagXDX zABb2m=O5&If{dbj`XhT5QVQ$&Tbo-0*x~}a8S2WbXG>=lMK{B=(p7JHtFpdW7~Y# z`V!pZxk`}@eFV-nV&?Jrd34#&dpQT#`-|CCA+*4}_L+M%v-9M98oAT{f;sojXf3<` zT;&Vy8MK^>K+p`r>g`V=i-YiF@&tBF)mp1H6ra-4BG?~n%eb1?pIo!`2DWXV*0S6o z*7Wu4LiC{_ap000Uanq#z434$Vrikn zw}UVkA3$HR6F+0aOC$GRwgnNSKVaGg>+Dz_Nk(RNZ=UL6%>4Wm9ShUlmm_X4*3hnD zejy~#sOnT+yV^RA+XOj;dQsO4&7BpLcJkl+OZ9P@p0hu9=H_;mT?9|XiGk~O^6B#C zAv;!zDw$y+EwT2mlfp;U?pc^NAwxdrcOt@6Gattm7#F8R*p| zK`L%>Rh$|l=UGAG509>6h!))kfhz zPp+tZZ6uby+KhWOKk>EsXkGieFxAwLymp?{i{SAOnQGoqXJ$=5r?Zr$ z^H{>JY1;&L!19`z@y@|JZ*j0atAoO=P~JGHT6Y`}ltG@=$;c{L%G@rg~m$Q9U~%{Y+EP`6;PH_KxfGbr(J? znjEPHvjmp;ac`YSafGLqI;^x-6eM{{ZwRCsp0jjr4yg8(xT^rSV zIZlsRn+|B?m>`@Sr_%)gAw}~lWz<-=3Vd!kspGYfUu`(=!X_dwcVXE6mtI0$)skL*DiHun7ABw%gi8P_O4`x z#MYl0m%&$;<+>&p>N1GlnP63(V?byLarL&ZArVbvrUmg>jpjdE88(4z4-OGN+lN^^ z%w87MAX1bIhCN~2zPopG({cBK)_x|@npr|RTr!cgUqVT#d#&W5JF4yhc!dD!N^71h zh`mt7S^|Y4P!}~Q5)d;7x53VEO5sg$xigX@U`ty#NN(0YLKxSY$a@{vILdtu{+~-N z%i?FnbR#a9oI9-Muo;;Lo-^&%4Ec7M&AI^;y({Mxx4Cw*-;7dEj4wzEpOz*mPR55t zjg&;3^KTXSWZ$|3AZ(ot&Ufu*7J>Tn^?kUU34{I6Yh==epD^?EG{_c|Qrfsecny|h zgJ2zMQcf6c)E)l7BvD~yv~TjRZ@hPI+ilkm;{@S~l+}~3VcxZI#s7DoP{U$TM?FOu zN}h24-z6#&mAYc4%2n>r?W%8+>YBRY&otr9ER;3Vms8Htoz@l>f}9wavETl0iXf3; zmK`yul3>-c=L@XqFJAan=ig9oZ+oNx>7Z98y8y)l?H_dA#Lfa!uJjo2NUoH00D|+4 zAR>q;f^~=A5Nr-(oT0~|i2sh^YsjamzyP*i`2%VrIgLHPG2=V{(@1Pum_3UB{&-#W z)3Q2PRCa^_q9Bej$VT?}2BP{TmxC|0@KR3@)bki9G0G5EAL9M6n^j0v9l%q9gk8{P zDZ^gRZ{WIqhsi7l{SNrd)H-9YLIqbvdwM>90{&C4D$e^3 z>jJ?RYHsG~yv?gC-h6;P@xhB$BfbMgw{j9xf2x*3#05yeA0%yUQn~a6w&t`2NK1jY zBK2B-{apVv5m5k(Wajyil1;aLiBIMJeH;6;gma_?#%wJ5E0*ERLNzH~VIbl}@V*IR zXA_|va#QxaDRlfA03YEC2l*aXd@~+|WEa;t;sn<7 zzb6cTfV5X*0)l7(@J|9$g6*}iuUtx}bd!){&3N=IJ?y@Lm5&0+;XH!4F|6+0V7s1A zdfNrdF-D57Hey$6`X7A&ICW4=m?yh_gm&@Tjz?SzoflK1Qyw{y$|@=j8FJ-AuMxy< z`M+pchLy&?beJmVV?nRwBfN4Ry<(rerlvfv1wSE!<6;X8>amK6Vr0lmFi&@Lb z@|;NO9sNmEfSkQ>O`6E>t#esAk7nay#2$9{jZmqcR^7r4%v4q8cV zx>i+RLT($f8}te~72@*Z%n>#{Zw4k!?oPCC)iRuAQXf$dE86)+j48uJ{7d!S>NY&l z_-{NLekxM!4mLcvV%P`*i?X8othG?7cu z{PrQnMNhPE1^ccnI_qqRniyU#7RQkVr@wCG2gsv@R$;8TMk}>t6U#k=9U+;TZ+1&o z70y#=%Yc4ORO;K}mQIbTahh)3Y@?XUd8abXkEovh?&bnZK_Oz(|u|1!qO4f?|k>Y;Vx|L zZfO^oJm)?_ORIUiH2T(73%OHIdvA^_+E<;H%uTB_f-DA^VKTcv-A$W1m#HruDi)-{ zdelKUuid`GYs#77jr#-cP5MDVOj3R_s5q&NWyx+1>AS*D?p)ExRA+w*j-JsF>$h)K z|IlAn_88-ZAx4M#bO8{#=-la(RT0^iANl8g-q3HssR9uuRnHQYct5xC@s;D!>fte| zGCRN#YZ`MQgkL54EjHWmDP(|QyXU*FG=qMr} z%Ed$XQ4zHXN)rf8XG{Z--5z;BW1lGDn#netTphUhyF8>J3() z#|8S+vZBMfky@lC{$C`4BBXe8OS5l-0e|yh*#M1DiJ&5)6O1@*&9!%|=Rv^CA+O}I zzru^l)Kk|qd5h}*UhY6sqLE9jAW;jSCAxg3=0@^(P?6%eC!c+gEG{l8l^SGDbxjmI zXgpn3azDkY{b2Z2E$noD{O-!gLaX9FOVnM+{g93*QtlRB?m=Y-Og*hbac@5@Sy=1D zZi4~0!?Y0_j|?uBl0nf+i%G~MNlY5lfu;fLZi$b_C2NegPmE$UXMvAz*Ufo#xO)UXx8z}o~>aC z^%tjkB{s%VxchRE4cPy)W*cjEJ*QRal2%)?;}fk^L zUDL!)CF=lWCt_d;bsFRM6MN%h5+Si3QR>sQ@=2%ZD-U^vPKwT&y5 z_}m@(%|Hu#|bKc>DroXhs_|AUf* zG7=({c1TE8k|Ie&$cXGs_KH#(GAr4ulw_}vy{XLXoyd+zGUNBUx}WDce#deD^S!_2 z=Ht4~^ZkC!#eQcCj}=K1I*-And-VQ48zh(DW}T=l{q^~{m=0$xmu_j$uY5Q0oi7vw zu045FsmF8PvLryE#$d-)1y$>`YvGCNuFC$bb}daIbt7j#WniDU%FPVLl%nkHD}LGV zWNA`Q#vb=uEIjP8xCx1qUpw?)TFpg}kwmmno=6PbSuJwF!)EcrSlo+ElCH4V74y@0 z{cr;xcB0$KFvPy}EGMPoF_vl&7Z7-DRy^B-7Xv0Fu2?v0lgZ}64&$Rlm!ua|0l=^l z4Vy78C4Dn9We*BLE{~;9FKNCb6nx2$z$++Rl^}3&E zTYtT$b4>!dPrAA<{iE*J>Nmfd3?`+H0de^_A1r&o(7J8J-DvievK2)tEQbx|s@w6a z?0#bY9|!#;yf>-7vAw>Y28SH@{TIjT7Cv{)?9bEOxXj7DDd4{v@e85F>03& zA7h_qF}|{c6t}a?$Z|8kiEBf*uCB;SB>nv(sB^9-p4%k7;Av#!=Ex4k0yID%U&j^a}N<=`Ec!=l1V~`BHBKQ*gH~4?^n<#$(wCK z4PcOf^pWp~d0`89sgS?dH@1Xm#0bZ*hS5n{JfYCE2f9b^FSB_EEw_MqxK|Av)v`|! z6Z}<32WnLi^KWD3<`=*>m^^)BT}84NaUuul-}w!E0(?$6sutX4 zHoBU~*82y|d=Fmve7$t+_Vw)UwikU3O{=j5yb@yKyH1gn46x)7ON(uL)yd{@cr=03c#!tlz>jlT}`F>pp5H&F~H1Kjfu zQ>AIn*@lJ&Jv}`#{3NgdsJmi6g6if($ zdRY1GsiiGmCIH50bT~w1+UN&n60o$=VO*qLh3FSJ5pFkg8uP9*`i7Q%C!FOma4=8U zf4gIFa4&qF$V4=>Xs<8qxliaOnUV|=YhOLA)T6yg;-+|#?>A&L$pU~Fq?fv%@ZJAn zmdn~zgicd5E`Ju1f2;nA@4^FU;vT(~8g?C&BH2eX6k)dkVKoAEl>dZ7#Ry z%;;@;8cHGv8-A!t({Ea6k@(SceLgCAzGir?3nF}6F!-BTc)>~bR2*Rnl|MwZ!#Z(p zdq@r!D(cn8D{b;2bgZg+u<63uSNAIh1_n4ehkgX0Jl8a87{ySS6IEW1pV}}CI%5G_ z4X%{e7iSD7*N;)`YI4RpjUag}y1@Uj_n2?at; zECeJpzWO{`?N~%fJ_9)B05wgeBpkU)l|%!r59(@}f){W^ArRX6O-ycm)>)JPL9JQN z4Q@ZX#^-5taU93s&yOB=S0)mMadZd!eM^v7IErhIxL??ovQ>`l3;F~_m4o5#xRck` z36um+K7e!rHLcPkS`DB+Kimr)I6xU&b5r9jDQN5}e;n~w?YKbSEq8bJe<~$is4xi6 zP2Ze*cZk%aj(~b6yKr+C^L&Clt%M5A1M@bat2{6`>q=e~r|?Ix8t6~(5L<6}U;fuB zo+?8I@c6&{|32Yy*+geA7Et-}+57-xSgT3xJ1R}hVxH>xHty+u!r~d0E)w*K z2osi;I-?*OJ)=H-HINnEy)2YEiv^vZej&tTY!o@PPDe!}+MM}fgOJ=Q$X5Md0RMq0 z;Q_2sZ_Hn*8AD|^jC`JQ*rcJM`ys~9AE$aW-yPLiQlLne2A4qPL3?CX{@AS%=yX?+ zs{j%B>F)36r;D&Sv0CuYiS>!)Tl{wtCq0D6dEXA2(y)+p!Tm6>ziVelrV~^nUk_ss zf<`=2$cnt#twvpB5{~I(my=q5Lc)&-W9VLAQ3_pOCz&pW^@8ajfLGNWF|RwP@G;5k z-64tG(b>J0mFPCse7ICJ-x%b1KlU6BQ(Zo9`~&PB&)L1F5gEFJkca*D{1bBG|CgPp ziMGWoz!-=Q*rC?g_4m5xx;nDpu68F{htV&Rfm|QUp7&etqNw1+DhC3EaJtqV%JP5G z6^Jj)BM%D}4h_vZf3%OG!}NIS+4bCB>mbf)9IWrPYmwh~qV}pk9k%{wPK@ogl%9d^ z-?{cH%RK%L_-v`Cv*b{3n3jrYj&oS{w^1X*2zhs) zDO_GloL8V_vab|KEyLQc@W`+*(?ZG~#Ha&L^A<`{_kw-sll`Ry3pF+xgYYZcJiTw( z5oZRR4T4GvX`E=^1gE0M;(`h$WSu@bp(HRxQ*hLd>b+)Go5^`vDKXa0L>>hMjs%5d zDa^;IEkkTP3rtvPAbWr9F;+%$_Vd+GNjp**1!^iS#bfaVO>2DfP&=MSy0CSC?{|nw zB_9!KS2t6-acAp(*(4ITfuHBds&#fZ#%$bgBOgY6B0*KW``_!~$It5=qwUGpeJjh= z4vG3FQ5aEP#g_l?u+F=?dfG~Sz2Bkr$`T0cBXzg1Nx6U9cC$Hm8Q$8*ge>I>XO(yd zp(IAa7mvyDU0Q)lNc1427d6TY&jQ^y5oJWZQBn|H60r@!HZQG2Zi+Q*9{WbYxkM^~ z%EXlC+Nf>USw(q@=88i1F|n^?&D6|YSv6J-#w;>gzTwLQ@5Zc3t`nO1f0M~HR3MbP zTlO}dsWrK+ij8aUg<^@x+ZhW-Vw)^9%yTNvn;b{bUGAi~lE4Ly*nnHeR&bpZJe!0A z+Sc@1BGUk1^dYori6Fv@k0HFbXZ>)!wfkAz4M_XU9lKE6sL=GhK&QA9Zy@OM{c)Gn zQWO+SQG^NEMA;o2TwSGi@Air}Z@r##PIN*e;Q=p_DGdM6DqVx>5LlBg9b%@XKwmjI7}k&ErZq z0id5ECXsjHt+s06k3EfQ5o%%9T`?>&N4PKX9|nPysb5WfWzYs^R?A~+I>Dte2T|DS z9%$4Q#oQ$%UULn%?qpy26m^$y@E!{`uWLyjyK+%R`xaa=_^V2;>#@zNB`wLzca?Lp zPDBl_{=Ea86HezV@__*&?ez@?Y>!?g7F=WP9kF{NC;GLOEMCalhgB*4(oRpVKb6qo zRKBuST(EW2;&m2gk6=61`$OHM&XC`VG_TMooXWUi zH^wy*yL({+Z>X8eGX-OKYNr1PdAcTCUvFBp)Kj))GT+?fK46)SEVvfsWz;ybHA$>; zM>H@yW==X}7xP9d5cvd^A*56vsUu7+Ng>z>uqLs2-aIAoQ`}j1S>se*AX)PR2E=m0 zx%%#Xka>4ex)0em5fBsRWb}C;+SvxRtIsLXdtlH)E_+D)18ahP_%HKBCZZ2TdKvy- z;ZBBgh1i7c>GG_4DPLya zq1*s8hG5e#RZF7X1h!$OJz)rrt(EjEI!Y6Yri%MS*hO_>L{wDO>er1sq-cS64niu0 zaLO93;#z=`{MGP+?Rt92iaC!#MBNCK23YO{pGlAy_J;YFV{Z!8m2%ORJRa2W8LFv! zIKC&tz>;Nm{}9eik|@R&sDTUEA5d^!^p@kh#3hci!{v*@DlbyoJvHW7h0vz4sW5Gc zfitx58?0CBlA1TKLJ$W+uJ)wVs~u1h01nZmW?9>@1!waYA|5WLJ>&o-li;by>G|dF z^Br*SKokP=2+?B7{u!5LF9ausS3`-F8MLk^Ye-RNQSSFmR zmTcA=1G2=9^W*dCmrw_A=4hs-j+p7{l0Yy9FIwKR5M%>94RV^1z79h{s*iXRQK#Zf zW-7S=<)1oths<$y-|9Ed^w!A6GKChm~Gy z|4AWn-Q4JDjY;U(IEqY!$L0v=^T-22@b$kaMW zSTlJ91s$V%>|Tp5!pT@Lz6+W-wnle}bgQoztfsF+Iu6;)ixMlL-9 zdB^6&R7LB{pJ|fW7WQjSJ=g5Nx`DA7;IQFs-|9H#12*!(w3p9VLkFJS{ebZHtKJ*k zM9bJ(HGM?o}1+i5o<1L_p+Siw2;iRTB96?H?fCsd-&{no$9{uqkG@^Aba=%aia zv%T3ZZDQ8h?qSD!Q3o3~rG)nUf zETlP;sXImJ+*vjvs=lA5{6X1^n#ST2MR#)I4k_JOEgc3oF~q_lY$I7CL_@*3+Er_V z!q7Oht0BGsGfK}g$~M5-uuHh3a&ZsRzN{@SgNGkFz=>lcBw00PyZa!;(D&VsUS(>r zKDq~V(A-j(B-n96vT29F5_J2P6iJUOzX|E)i}Gp|$X>gA@(iBLyy9uAA$9?8G}=N_ zu6aC6NLk0o5la8pxLS3KmWNG0#!(_-U7qu2{GcVRAjVr0e+VTCk!~H+e|x6rKfr zYil}8=HzNW!FE)BYnIs6wu0QwkP-1lq_y@9gV=Btf(0f1AOi{9MAE%nRT*B4DNg%4KvD@H}6#yHE(ARj1HtRsNE*fws)85 z=H16unVXZsY&W1nYohXcPINph4KA>!%T>-T`w5|FFqr6eF#I3(xi{M+FysqS{%OB;r{G!9Z<17Ds-g26c%z7?WeLp_?P~Ne>2!pZsa#8Du5x&% zN@x6>)xj~snM8i*Yrg$qB05xPEbU*Znt1_&e=fgiLXpp(`a$QNWRfyJ#e@r)su4HPtT$Y%sc84>%!x z0>46V!lsOG1e$im9yZRVt!&O2OIAXVD({nc-fB;q!CfWaVOQ!ZVz0h5i2M{eWBLtH2U%Vn-5Tc)B zguqF8114pNf*^&e35Pi7pulV)3|e+Kav4m=-_4(cmjIlmat;R@TbAT_NVLuWSkt?T z7YB}4+)m=6eLqXK=~QHN2Jwp^N*W$&jBEdN+Xdsu`W5>!RgNbL0g-Mg!^JltmPY-$ zaMvV@IZ2;|=kw*<^Whvd0*%5Hs4NjyIJ2Nqp6x*G9x!Iu;>s`9($;>pc2bbezZONt zh|3x9gf_JhEJ{}f)}f5$4AIwWa`kk~fveJ#^E_Cqa`X`e4 zt;ppbt2BMKl@K2@aKF3f74gOBYe~QL&ySX;0J?+eQ%E{ru?+^|MC z#fU097xl68;2pt_RINT;jc&qlhp?sci!ico+HF{*Kn!;pJHt&@!s8f5wQV0?fCxZAgKl2$NZ};PMT0V3wgVAIs?CL`nV_?0 z>=RWO_AUY^$MJFTZ3^Pt;Qf*HB!3_C48Xq#r@bVisim_`kztXjl0+-msOuGbxA7VZ zE^Ghr-QoRs)dWJZI1R;I?BJV-0w;I0x9z;gO9!YqjCTJn$xQHL}Y`H)T;mKfpje!kMS*Rl_at* z;J#B}-}xoK^ceYD(em`Rl5>!g?CLm1ile&nV@os>X~;6ni!Ub&u8674+$2 zF;c$}9BB4|T)-H?0H}mDGbE+ByxYm-7;tYyGOP*RU05={NSuIZ#L6I}0oChq2tN*& zr~cppjU*JGb^)7w=3QA>S)W1s?M<4y9(O~PY|y>w?@=s%AIJPZBlDL-YMe6&X`WZhvzSq1~U0u~A@M&`b_@qa|%hXGS;;)h0O5-|r=A zdY|w3#sXd+!b6NQ%FAS)$WG9DP9H26Z*c!3%pRMvH8VhX9>^dZJ6PMwF$ebcJO=Uv zZUqkPSukeUX=IIW2e?&CHNgN6Iw3XrL#0)UY=qzMD9XTvJ;J;8*z5A=!ZBR&NU0uNEOYzkxBcpucdnWqnnY=`dJU7%OM)KD&58BZ&Not^9h~u zK8b8YbAvSaHed?0<1Qgj_2U@}_@T<~n=Ved+S8D#F`94SyG!eJSHO1%(HznfHyU>* zminhP!idC$gaukqOsB{iy%wn!7>-U({2>#`_VvLz^-D^%IISxnoWbSbcKbGA!BqH( znO;`}*!Hbkx3n_Mj4jzXIiUio#28o8@kTTMMh#MBYMu(BFi<#9otrYfGg(i<^G{MO zfI57*Wo0DrcYF{m;r2(0&Xq6Ne+*#Xgq(7g4af`OYxG5vj+Dgi(Oh`hlkRx;TjS*1 z4&9`#04VDNN4h@PzJ99u&cz>Kr+vJ+HEDVUk$txClN%Kd=OoMp*D8;B5FXN_j+|NA zwwwaTnL!K9&c8Wyq`qJ;>5Xy%?6&Xm>lgd>KTf|4*fD*13uJ%q>p*T0Q5(h>|0nP+ zp=E+}K|1x~fM4v@4HG_H=B?lPl{*GphW&{^$^j%F?{Q|wR8CZZEy0T!pol$!=#f0o zJIfx{etl7?=^5SGK!7(kpN}RGrUGbGU67dkSnF1C&NO>k>AD-llI~Crc8mt_92RHp zP4wpB+p?FvX>BDGibaBhPq#9d3IE9?h-(rXDeda3faIYaB-xkEX1m$G+H2)b&*~(q z-^V&Oh+wU1B=OFH8K}DvB*Uuy_R}dpqgj~#%OFVfA#@i#%X98+1`gDe_=SQveMq_| zvnDCXCVecJA^L@aVCCfPY|N7Ox{V_ci-fz0FGN65aEe;XUib5{t<9fplJ5;e!U#r2 zP4#M1kiu~@hjtMqh#Mh6$jRB}*rOI-#Q_ZjQkrv|cwb6WtYQ+*(PM4LzqK9_e$o%SX0tv*dhC;ynHZM$&-X zzyE(96W2C1JWYg@!y6t!!V`iSvBEOm_y-X|g`6fQhxe+7d3I3k4Xx92a8NOb|twE`uu`L*O`JN_daid&=QwH?}?gzCPH8RI~^?@*b2 zJX@ir(#tQdSj_EyD3BaZN#cnxc$Yr9(Qah9Q~7o^#XS<3IQ%l6A=Q!H**un%>O`N{ z+fUyN4mv)5)MZSje@CMc(#s_ulF8r(Ot|S7liYp`QZ9sLQ>7JA||O zh`!F}T?m~F-zY8K@O%D)yQz3C*)DA|tx+$_SP(tG4+K9mFdQLc&|tYB$YN<3GVL{O zZ&~RY%`DVaHySo#-YK2xmYXVf*fES>X2JSp!XXZX+0jsrr~@~G0umT%y>9j%FT_P#$N-T+ zm-1z@60@hR&0{Nl%!`DQM=kl_!=X`i`=ab_ZupN>Ysn;k%`^yxVwJaGj*bN|LXvEH zqOa*cEZpJR(A1QNTB##~zJeWg!p%#Vo zK>d*@De*Pi?%hEV7{#2_10IU7l77cXUkl#jsTw#0y7{*VC+<5iWY6Ck4O}l<+1OE` zUsB%xeb0g+BFJ9#J2u-z8UN5#JXekKZln0TJb@tLG2(Nv zwr)&XHNlgS&0=soQxW*k?rxD$^R3h@)N=AY|oCW~|?Xze_RIhLYx0-*W(TxLXV4~MJ zt7u>EhnIIEnaB3-NMU<(gTukrO#6#xSdOy35o$E*r{AetY(}wLlrP5$2}{c4_ZHq! z=QSM(-rD-IK4QNVG9t% zojN}A;{7>ch4h_2@yie-kBhQ1-E=`ofdZ;yI#OK6-;Z2@x{$iTw4$am2)WI>3yi{A z4RPO))v6RkWEU>M2Tr3~EXEibzmL#REpj~8pxQ<{-Dl&aIy;4=vB=WpNRk_+91d&; zs6S0v2P8M-EmPH|>gw(grsP{r-TZmB(3+i;aG81ABJygbu$xW@c2goYhFwfI&p7_x z<}zi$!^TtEt;;wK0Aj#8^DCyC?KILZd$nSlIy=+CX2lUQlvXf#>}ZTo(+0K^CN3c+ z>Tlqhr!K=jsIOLgRFo2S=vP-i_LPcAmA&B8^%(uyuexo#e>c*-qITrXmkVY%75b|s zx9>WK1Y3AiT2!gZ%BW;v^#~sY$d8Bjc#D* zR(ZWSarD+>Uc)CvMSbvFd4@6okh}EV}pQa>>OlzMT;pS}dD6cJI!f5zswz%(U?Av}(p>@prZoZV5$)HZBUrm&T(-+EWP@+VQ|`J8Zm3U_y)mrU2Y}7|%|w-zmX;+6iv#ke?)eM!EM{6-T9p`xpoxQh z*T_TWu2U2*|*dNa{jy=DRyTKY1!fmQNsIj*owAadHe^Moeg^} zzdGg|yL@(>0rBnM2GU*cK0`U+3Uk2l%XV{(Q`GPZ1PJeupGK0Wy0$hD^l+-$*?zS6jZd)B$?=wD&T3|(|Sm&USXk(BCp*3{j*GjzdQ4!`m zDqZAT%n!}+tt>)t^?6A-zeG9lV{z5knH^Lg7f$~yb(i>hI8GgI!T~L9ZE3TvZ{ zVO$jTJBqj?30v>3)!y}!dPNGXHJCU+DhpDSFdGS>C?i6AILcObO%gU!OyY!_G-Sb4 z+$J|TV!vhS>gl`lr?a@Zc9SxYM_d<%z%L?H(rB-?>qs!EL$$D>sn2W&$uRctWs?tc zHM|xfvnF9| zZFPg0{3Fd(4oA>CLv$~76XVLIr+BdufhfX&u*?o=hSRX+;RUrVgZ>kOKwZamyUu7* z@mM&$AgN{OG&`NIfSHW4g4z!SZMyl5tyBDctG*ZZ17IhDiOIT}!~&mvDT2lZU*mXE zWME0k#IgD#CN@nXW9-5K zT+^lk;eJ$6A^~1a9y9iMMIiaZ0r7#b6JqoEMDxl_lK%{0V_T{>k?_KQPB_2|cwll` zfn15@y_i8g*lH+7vt$2qd+PY?@b?UQVwZpW2gToUt$Ez?_9FSiHfEQojiDl2%m zdz+rfHF>W(KfzgN0Pl@zOxhG393K#njd_DEGXK`wABeP;9`i z)4~u&pN|}#02!gk9OE+9Z+zcQhS3*e6Vo_#8ahARKP^-Uks^egFfx7q8(KH=k1%m` zRE;s&)AZ{XKaj(akPtjCsVXPtT|NxpKyc)kK!ed8Od20EvqF4UPx`N54!_R6Q}?gv zmwm3kJ{^wN@!^wW`e#?-8CeVt-s#$KyDG{L-WU|A;r1e;u!JJfLXsDu&BvghG!WS< zkXQW`Ec6LGir-NT>x!;u)k>LuhG7AK0KP!o)*Cn6M&o<(#@e+K97vCmG76mQug2@Q{j6G$9m(!827tp4Ri`TJ_BfdA-#T zZxOf>F=Du&OvR>#)x`Um@!RIvQ)lbO=CM2Az~GxHS+z!Vb+!T5wA9GZcN?%6lOyi#qq>*!E}4G zfGP15;hKV4Az8t2$J=2eHWp(gF-OwU^2?9j;ZB%f%63Vzzl=v`cy<5pnOOT2yS8=xyW;vLh#GbdhssC^vojm?S~kd0e70pj z$wzpIh$A)2AD<;KCn}-_D((_91tWs}9}NL~Tn(5Gid{QEl8wCnR+m()Q4PzSV#u^p zq5c6IS7x$En+;&E$}E>n!(w5M7fuGK-|P&^Y{s7Uw(mxztEdPA^N`8%9hF0 z9iU3`+`Zdj=`n)dlo^}_b9iz~*WS1=K;Rtmawc68vbfZ{Kspx~H-U~PE1t3M6{ zcnmXfZFV1yrpIE+cvt~J@hGcnOxh}9JG6JdZf zKV2Q&>=yHT_zp{{?=mhaEhsRzD($zfTfw3C=jY;T>Ns;c)@uCzLLO~4zVPs_JvlN! z_p{LGBX#`j%9lVsDr6@#Ue3`B&Q#aXcs65=lM{<2&Kr-3j54=uuXFTT(jux?iED&w z=EE=@-fFl;w3R9GDH1OPp5EitcTo3b*B2vW_D1IN)F(de; zTeo|ANh!OeOEGcCAOpi|4rS3xc#<3;!-;P)HEh_@Y(e{3B?7={h57EwT+8se5&9s5 zedvIqS%bS6wG0Dt+w`i2QM;tiG}Dz?*+WSyoUI;4SYvmae&2rV*XF$ioRTLh^x1Bb zF8pXp-B;03DcPi+;Fdou+~O39$D;^_eM--g>D$FR$Ae9pMNaZxs6`v1E`n6BsNm-Y z)c$-*A@dY3{9vim@D%p?RPiNMEnLFuZZPd2ia(W=(tGdJl!!dUA30Y_gchXgo9a)( zhjwE3K0OP=0Mt5EVn)IFkI@91w|5(^Rs?^6bXvTpQA+wvqb+mQb0H$!syA0d>YjW* z;AbL~)$q{Q9~si$ittDAsV-l+7_gW#EUf1LxM_^Bfi??5wK2th##-B|3CBSH{4)cW zN3(y-$e)JiD(5J~zuD9&S6C)Y^LmvF$-0zCPVWU4O{kZG>x_>3n01VP;PQ5l9fQg1 z3BV1)GO`Kb*av|BCrm`b^s0>C?tV@9h|M5Q2-7u3>hr=-y=ei zbx59M1fh+~m5`<#Nxv<=hbXa!ylQiEMj1$L@thnQ{ge2au4`rQT9oF&zVxj( zuWi4_kxc2>x!)_~6}5PwoTDhmCh~hF1v~a!N3`zPA&gfu7s0i`9ckoHYm2i!)cNJ+zYT5Vla#Arl)>L6tsJ!gx?1vw)+0!kzA9GbEMp3D=&vB+!mOO*_d`VCo)QT zJb@nd5_=dC(*v##AC`w3i)X5B`F__ufsk7n*@%j_QG6?DsIattTx&hkAY(;ElY}iK>*t@ZSY}eW1zxU^RZgC$X?%HD~ z89F@a*jm4kf`%k)oC;#E-4GZA*8(zwx-Fe_d-ipTA>MsOGwk^1yN&=AjwF!XTpQYz z+Nmf$x9JW<+ML8MgQ849>6M;oNh$cbA3ZtS(2e39uYFD~{`rMVc-#Xx&ki`#q`Nj) z7`|P@($rC()18^vZmtJCvI_{>@0UX#`R6Mx)^l@lHJ#lMCk>{rPo}46GPUZ0q7GsT zi-Zmjg2bu_9>;`D;bC=i+4yE_WxV_!$tSeZ>q#^6?xKD;3)Z__ZX4ShXT6x#y;~mk z>P=pPAs*nMb)rn9!Kb$82z^8>SHE-DDfF}q5jB1-6)qBzO%wdD_l}NAPFS#@S}^{WczX%FBe1$2VXYiNPO}+D}f2x>$)u)8QXHXeT8k0)QxlpZr>S zw={SN!E)86ebCB*-UZ3LS|EW33~)n!&FDfnyxX97fhfKaBrAhJ%i;`|SDXoA7F@hK*{%WG*Uttf^RIzI;JM zVn&lO3q4&xV)sWY)GhQNZjjx}bt9_;Di5dzybTI-a}RYUd@+^vVdZd9kR&t~g%%yv zfYfnjE^|(FWGTJ4M&|E% zE=|mvEuv}QId_A6e_s?66BAN2eUAPG{N0!ub;fry^5(%t0Hlm?mpiCHLdY@|HNwI4 z>f~Z&Y2wpHDlb-l8!@$h>+5mHRqhFK_+vgWpBC&e$Ykm0=r|y1oAqw%-E7qpTzds` z8{W}|CfMbR@t0ftm~%-{du{ySfxexws2r}t+^jxk=|g5N5p1+x`?(~Wx&=5iu9ZW* zCfBp;HJ62jh4N{sS}*$jLFYR05yZi*4(sI9!sj2Qwb^M;ts4gd0|V>kaKn=m`UbE5 z>8ph%%_!*VW5>5#-_-=xzu5b>OrNcr=$@H?o03h5V}C1TLle~+7xOtzzvW+nzx-yM z^`h;E4X4st*&xkvZ<+0TJ>day2$?^A7xfF{7pTs-2}-e zNa*B&2aA4>G^!Um74crj_L_R*jWAmtWYGJ7P z)psKqfL{}F*nb6=u%nOj{evGy~9 z%+>ijd0k6J3qE};Lw>){S^2u2sejj67avHKvM zg{@S2-dcD7ml!zv^fWIkJ=eavx;nhk`rcU*hDgkXFw)~0xsb`A2%lqTlsqjzrdrSr^PzMS5I^8A#op$iOsZNZnobP8WOU? zF2cw>9C`Fs)wDaGs8VwK0XtIH;x`;AxUUJTwt#?vw6e<{;7IIrxqC+cRvN@7i7nT0NoRCMzq@{9ra-j9DpUNWl(BZ=LD)(1pl>--mD_Ow|pXDHK6K{!h3q zK7jZ+kW|hbBH1K)%7R;ciCS0p_GescgQ#=#!uS?&@kHqMGx*cUKsruq{&b(UdE^0x zCna>+dlVHJy&gRXZ=oRN_1XDy*G`8SoD>u59qY6C)91$*))?L06Dkv4+j@;_aoOEY z9-IeZR`y(wy6%)k?p7n8*Spi8PEa1bbnV^LEaEfVOQ?x}gxNGl^LY;g)uyT=Mzl0y z-0;W(PCefLS%nS#w^;7sHiK|8*XI$ec{mhsVP8#d|ISjRb7BTcxsuN636Aiz?zQ=VYp3=Qb17!PgZBfk&70l%&~&iP!6ow*oNmP z;PJ*aEI1deNwSyqq($2Q4f+(;#S3cgKMXi4hXc>jm8H!SAry0Bm;gM!&^wLq14>IY zQsBY-zRdx!kcZNy++cB=K~ML5C~|ONKpTc9tLRTodi7(8-}^{qt%_%9QA2s(Su&lA zIq)$(6*IfN6AMdJ?O#G5@SHgp0oFwue}8hmLc^P}-NUxy%CUmVcs$(bvJn9MtSNH}1P((O^G z*7i7*O=Z^qsS)%x?b3$79D`bi6J#$ePhe?5o9l}L0J$iQK$JE2Bc4`kmM^w}uf?foNx$YkA@;iGNJX^SI=p~IZ@Q^hCA+IW5WaSZ2Ve6 zE1rr<_#Yw0>q&Uf@Io};3%;CctmI?VIA5wgwrATopUSg4sL*!u8tF(5weD){wX9(>Ml#hTEia0UpNiSnSV_ryaplVH zzUAD*{;KjU?)=46)~%R8VB9QgYiql-S(ZqZ81ql`fZv2)%`XdWRv+yY8X}l#HFmvz zGqWWQpKtjTA?S)Sdo#1sRN^eLY*I(G}_Yv&6n|IyV>$(PQ9LAmxKeFnceloN{&7Sv{ zm6*k%Eriw_I;UA!#PZYz&h`e7ml23Gp{5|YJxc3#VkE(7sKi! zwm3gNDDy6>J0<=iS=q6-2)jb;eFZ9GSgL2nL-E-A#3Kphnh(;jI9%6u41V)5!(dzKQoa5pK&Y=v{$I-y z&i*yKlsV^C+y#ZY&U{4Y;4KtkQZieWPFvlooX-2oTN771Tz*mF7d@(YzCkUBR*aDM z?9d><3^ZKzgb~={5|OYL=<&mVo@04L@XLZ@W5=x1yQ@9su{{_*;#m5evNBZDR%O2`JNGg^xJd^kv4CS#%`$5y@(q0fBd& z3$=?TuH3315)f_Kg)QnUZoEBh8#`lQf2-++bu&LHWyq1_G36>=Y)k=vm*rPCrYMJ7 zv7K!m+&Fmk#eaF=oO?Lf$Ly#;{g51tl*9HR6R*avf06r{x8D^*1$@@*K?q%5EOA|1 z^FY8W5f)O`)fJYr0o6nGEW4NtGk^m;#oSFz4JZG&h!e3$z!U&M!r%n?*oyhSzS>Qa z54JX_oFcX4c39>Y!A|>y@#L#(r?KG2$H$i_=oF1nN^|2=z-&#lpg-x@+Ks?>c}kij zFS~2F@wk~+MKx&e(=+J+3_qFC+EbT$Yhc*RXBhS~;MSlzQpN*@g+m4)e&Dw@=0Fls zB~Wbw^MTDnA93*1?TW^lv~IDBd}j?#iufIbxe+!j zXE5-$WLd=bJA&gJ2zjEucx#67XO2Q=TlsEj3)IR)Y1yA;S48@;c4-#}A^t#G#r|WK zLS?3Oi;(BP*}X$dJ$w~2BBl-s3w8DEaylLNz#7r$28@)L)P6Sz*>hqzC(^!t{mObD z!xLJEZ{XhmQ#DRu!SUe^3{S`yJHWs?zJ!z|J$#BT1j0}5=v3?`GHx^HMs!Em*|khQ zsy~^)upGtcF%Y_KI_BJ8)R_3XO91~ld6nuX zUPHTl4lTGLE&n==_}z->8*s!h9Mz8|`5QHs$n2+&x;>LmkfBB*)SLO0ng0oR(J_gH zX{;M5;bt+69IruC0Z@zHzHn9c2-9TpQB35Ip=Ng5nJvIrOMIeH+eHI@m{%JO&k2KT z#ODpj0awdG4rA~#I_Y`otxs3?&^?smR|8ps;e!YefUG>_{c)d*VEidP3~+$gEu4Ui-Ll3#tRL5#i43SS4m<_zVv)l>h$yzKwiNG;-C1dIXv3 zZD&r(NsYnVqPd_yvl?Mjj5RMubkuF_>xh#%)C}kTPoXq;D&3XVx^P;E2sf`HQcg>l z_OIht`4YO#;fbSB*QQXOF?4?$Zy7-6{Z>bb`oqhIH;pR>58dY8-#9jpu`fgl2oFv+ za?tMeldVsQlz)P;CCXd~4nwv9KLid~*nm^0=oVdZIIEja0e)VY%P!{Asv$2v;hs0T zH846XmKG(ZH+X%|S;~+>c~J2Ucnq76RnGyrRLy;>up_luf7KbhsFG=B+tQX#DEKF!rzKC6?uph>X2M!tY%kFXaDrLwZ zEDZ5Nw?RBF0@-N57!g&3BiWb;;hMt1jPc5Le-d|rY;|~{5jumr zQiQ-_01|EFc`?$(oM>CZf7pJ2<8<~=V^uMK))cE|`;|jqrv%MWtju6IN z3gYK@0p1l>#V6ma*PA6zP3>Ab|Lio|7eci1`Lj>_$A$%5qP!(b`h7^%C6XcMP=x() zE|M~-bE+CplJ{fIJ6$V349#@cmaA{BNkc+^)3tk~VS#W?eoMajH)&+YtU44*1Ivxh zL%Y8&QF`hCos#&Pmx?`X^Gx4NUHrxTjR0g{X&|~J?RiHbyJBnNpdxt0NtF**M0c~_ z7UfRahqD|R;rzo57Ii(8^NTM}Qn(2$;-~|O3=)vWx%CSq5goQb+@3Ip^}e=s;jgXY z(SbUVyO(tn$9AX3aV?Jcl0Cm8%v4e+SX8(K#@%S`-ar9yOuKh{Jgg44BLsSQcnAR- z0Jph1lgCpL`<)IeL}eJS-*Y_ggyTi?V}wlRvLlhA4gtazn7CYGeew&Lzn|r2ToU0B zUi`I7+kt+M`|LT_e)F8C|5-4^*O!JvT2H-`c~~no=D7;#Ka}5U{o5TMxrooX^l3I} z@2;@Q{Nud}#Rh;S+z0zz%qHUj5#U#Jx$t0Y)1vf)4X!QCYvN#>Y;C{Dt&+c|ydZMw z3lld)N?maJ>8$p25-%+&BPK12Xfc{H+qQNFTg=TGjFCHa+$Iiw@~8BK1w8r0ochzA)m{si4Uqj52T_hMqa@wL4-~~rJMIvGW+MgZ;~qDs*R7uhBK$x{53hkglq8iLfy9MFfvDFQ|<$J$>d*ghiS?|j8pPwzL7^Yu6W={ z7h-2{OH=cC1YJNvcSJX~OSz$zmgghxq^CV^pf`bO3E2UO z+bP_dzE)HaXX$trL9I|RLt~^Um(P4so~#lE(hN%hkndegP8<%!Xq#%TS5Gb(qxuJO+Av@S2VOUz3lw< zGY4wTW&$786S)QM6PIs3U+msoXW1$uBxEdYUvKa#Q|@&a##H1kfu&j^LbCC*ip_k0 zj6|l!XF}^O?-rBiTTPmY6zw6jZZ0dvbGVA{Id1h(U;_cx##L7q?Cm3`opwV=NkI&~ zmmc^ABxrnWBzKUJLXH@Lq8bl=z>9k6w!R8bcfiGe;;n6ai=0T@_oTAVb#dUep4l%py#ub#CldHqJGXXP!DT* z#Fi}NqnO>c(3yu<+K}IDCx0Zt{Ik)zu7?%Ka&`F0hxI2D-v*&LF_-RQA2j51^bQCJ zpUlCbfTJ1_`$d+{$FqHR#wOq=$1P!dpUs~Vi>PWBK`ggZi3D)bZ`Capju~tCJ9}r3 z3Oi?D?HuS8qPr?w9#L_!9PFW|TeDfVZrw!Qu@upJz)v{*CoMzm0o)OoSu&LyK3L-h z5smJk^0t$U`zJtYCOg^cTLP$v*b%_O;$e?#kfadU6F7_*5rL4k@XhfQRn~HNkO<-1(N(>_ z)c(uo`jhLKxaPx9;ED_rEUgFKfQ?D-TsIu6mwnLN{c_HFk5~7|XEeN&(Kj;M3@GWzg}9&U`DdLf z^&2)dA2)oPTe-KEVqBUI(>XqQru*6FcljIhHd#ede`RM(O1tJ!2qQ3szRx6C@%W3v zIf{${J>S*gzp{s^GQ{*!!3cPrcU(9Z_S3!>=L0Z*V4VQmAAmwmC>OuZD07n%q5m0v zj`Q{bos*MEZ7*i+Mdg4D5hEVkt8)QTP*`|@)Xu8POOo$*Pa8wcA_5B z08*Ti;Xpfa2ad%BRtHR`7**u%#ku|b@gwEbl+~SAX=!Pi0^U(ut)Uabm1-DLf}Y=` zV6nWr5K%DfGfdin!~kUHlrHSKmQkN3ZGF&j(W!pXw|YNhfW$Q|clf1)XupXcUq6ky z;$c0XE6}^)=9aNN)>YAA%TAP9=yN-Xauj`|y~PriRsPO5@xUH`8QY2Y&_D5>Uj|2W zUS2zY__08d!yD9>_#Mt$fT)cIN2|GHy(xL~H7*k$pC^S&xi@&Vv_H6r7Y%@RpVB%D z9uX_e&|bivDQRhPhcgk_Xri0lkhR;HE!M0ID+2ztzP^6kt@D3oxE6Gd>aDV*`fzw%&DNH}}0vRgjij{RaN4RGpgQIys*cFwn>N%aM zLojn0g=!yi?GtmJA$&(G#;X|N1Bam!dShxv6A?WI)dVqGVm?QPf;0!)|0C)>z`0)k z|M9nkQb@^4R9b}0vNfzq8XO_nqcXBZDy5WJ>SV7JQpnyTS;?l1tc;9gg(&OyxI3Tk z|9@TQ`h2ca=bZ3XZJ$rYZf(quc#E2)<81rHK3 zRd)wa7hDPL6m9U_UQnC1<_|CbP_NoKZ0cGf8KZ@+vSYhklA^!bub$I=bR$0ewu5=6 zpGU6m*X~5s3F{UPqjM=bikA{K1zz}07QEo`+eLAXlIgXe4;sx38Aezp>`Z@RxhHyu z@*~bU^Ek*yvz0XpNG6z2&+g(JI@A$EHB$FP<;6Lv)}!?Sp;2ZN4YKksRR=hG%$c3x z>rc}z2V}O6DJ;aGchoWe?Otk+te)yvv7ugUaK<}kB>!V_coRe1t^0+zEnWvDK0>oP z0s3tvt>SivE&}=^sy&GlrQ&fAEXG+;6^#X|?jGdkeyQ=^=2HBYBi1YqTpineCqsu7 ztArp>$E%8Hc2otsakhL5IV7=vuI3Ye-*WY3)>Qqj31yAce>Ir@lDL%nU242@@U;gu zsO|5ncm;J!1-T*(Ug3uhOj@It?OXJ6M>JKc3^&!ggR4Ie&S%{=Tdae0l3cG)4#1^- zdsgN4J;ihb|q6YmS}m$>J&h9u3_)`~H4jf~#dRAr~w=Gnwv zZ2qat>e+_&ip+okyb9g}k_&MLfA;9~BBx`BK2`rA4$GwRJjf9HH-;`6hHb5w_lRmK z3mrM|wy>E~Lu~G$ShJ%Z<_P07c2IXa#D)r8$e`b$j1+Gm?{VjuPcu5`L3#4ysWQ-% zty}G$pc=UQ0Ic-m8#=eqc^=L4inrx$6dSzUl~6e_=j;s7&Bx1cu3=K;W@e7 zC>_V5%uLg|KLti%EDU%*_TF~T>#C>Qz9oJ$)#H&}FJ$yw;4H^D2yrTi#D+UqqqI!C z@IqtOffPi4m+D@C=E+*8Vre~h@c_5N@+_K-C=L7k4n^FwCth15Yi(nHhUsh1oZW~= z#>2>S%9NiGQ#pwTjzzK^QnIyd0s{<-@oFk{XzbDjrN526;Q2aD06v@fLnN9x6$;Rhz+1;9Oh{hwhU#MvIybgLQKC%fn zI;avwXaB*K_tLEM!PjKDc|x8{q3L1`#d-Y89@+&UnPwk^_tbRlZLweAwzxV!-uZx- zVS_LeHrum}aHNn(w{H?J_~ZX%7MMI5NP6=kyuO@dj6w@sV?kh9cjY)+WxW-#y!j*7 zmUP5xzi^6)R;Ye})5zq~0cTZX#k-3MX*9Fv)4RTG0tn0ooa*HuY7!CZK^ zpRam9Ss=!R7z0gFoY2OwcgykUn0H+mgExX`pTzY z9zSl9swZUP>Z+5^(NrCC&L-PkVoYB$LtlV(&mWQ(=4FG*5ox-VnrV@O(55DPnI>5_ z&ZT%CsmM+7_oyE0zJf|rJ%Lw3u>6O;c>r~zShh}%QT(xdOMQr~2)6L)5M^_<*mhp! zw64IzF~h>s`RPMuuCA^z{DlLO{d(b5g^+`fKWcke1t&RCa$=?du%pw!zq${r)o+6j zW9FMT((aLXI#y{c0+;p){(akTvet$t{^R#5wcgUyR5A7-T60jN#l`K?CZr^_^)Im} zc55u>)~OX7ytn*mpew+! z*p}Hz?d5Ef3g)qjO<%xUAt8j_4M1S*7&*7n(|7swL+A(1tND^g&&C4`JX;(pJML9E zk2LNSnRpAEu#PhdrXdE}QbryaT1w426NZhAV(n2kCZvAXT1^saNJTf$e(3PClYJ<# zee&gDxJ~{EKL%yx%d-w@>$m%X&}5|iIS&Li;2En*`&OT(os{dVi@i)S7KWi2U20{q zJ595x4~;R6nBrkNWt87hv`+g%H|-8a?r=Jc-b9DyuUvNWqUiG`j*YbNkPtYGQk1{b zp%X1(tqJE&S`WWMPgzHc!#f@*{>II7GV1s*9-K?<7 z?)Jh70N_a1pO6qb_2A%U?^&rmM<#m?Z`+$Y77V2Ah~;0IUm4GeFw%Ffgf7P)(FX@g zq;??UK5kTC=q4!X-(V49I3{T(eF!o}Vm=YM5cGh=4vDnla3D(<)(14dCB9|XJWha9 zxJJ1jJCXH+kU99g5hYr&;b5VmcMI4Yo-z^=i_rzR?XG+932tMxcv9!KLF~)B0hgQ} z3CqLS&V^-U{t8j99zP}FWYeqpdnn($Xd{E^RhOwbw4r#y8`UCRfAsd#r=Jf^$+?6f z1;a$lV&Qb%CJnX6m1l7eOdruG>g>7z${uR6ItFP`5ISy08m`YQE6Jl1M zuR~cTUG&|gLmlF_!rJTtOG$Pb9~6h>^n)%8V|EGN?|fF&$bZwA5bfu=sf?p@qFoaJ zK{H!WZi;ab6(Kj3M)htXIvJM(-Xy$lv9l}o<`pqF{+Qaz$SN2sM|sR?72>x#tXfZ& zk+583oDc~EDnX!B|2Trn0y^G1y^}ocXV;AQD~t_8y#{@nVChjYH}-XF|3uKlw_|H% zRvMh4ez*Bk?eI)`@#qL6`db(t+JAh`Ox$BJ%nTGTEXB1fa8eP<^QYOz2n#f}0a;-> z@^-%?;v`7+u0Y0TCb`+iwd~F9a?U?O6qP|6vo8>8mqIC-JymkgR?=^Avcw?33=&6@ z+FJ~YpJa|b(LLsLnX-T*#@qK`vG&GMP$3_(`G%C+Bk8pdZ$#kAx_2pCgc&5O?2yMb zP3#7c8C)LHGQoEb>7DJc(nnWLp!|-^fPQcY3EI9tsnXOV{d&pVh6@t~}%l~P2P;MKa;QoUdkDLJ{D=j*&NnZx1t z0y!X(;Z2n9c_G_h5epknp?cQ^)S!U*fss|!-!-|*_Gb2N|4PX>P6Wn`dc>fN4rtY@$+!Y1+^}z3C-PyEpD9_k;^#yr8_=EnZ2sbPjd?S zKuSnLB`d>XOkWL$dB#YqpW)O}t}Me*l-lfbQxOgIGwYhp%{vX6kT&uL7jFEs{LlVe zm0;%swi(2GeBen_Kr|>PGA|vKN)`HWnez#D3Rh_k=w!bR|7fiY3x^mHEV^!6wO{s> z^f{?Q#4fqIcG!PeXDNdabFKrEpCQ!6v!XlS4X&Q6>Th3`?0?*?fTt_uoscIsS9LX< z!`F*bByCvi{p>M{)}U8|sLU4;p9)p)QSEQ`Qf~sv1j-xCGeoD2T3v;mKKUooG9|U$ z3`x9%#XGRxc*$>g?9fuE*TPg86J-6mi-XIIv%5?j>qkD#NExoB(LQ6jje}$Kz2G@i9^`SQ>Nn~Nuqp{RE$4ytA2V2WbH_E#6XIMkkL&cMRAiD| zN~pehF;fy-y_+m|#O;;eWcG;D_sHk5d9E~XM;YTge~(WXYueS(Q*|3A4zv1t=Z1w& ze1@$jLa2rB#i-k3Vv{*i4S|M(moLa$$7(gW&r z9c8M7@)qZDxTxkT*13UNj&}vZMx4n{X#f42V=5?e{8-az!%KYlyn)_*a1OP{es#4m zEf7|YAMgoW9kvpJVb%1P9IG#g;5C?xo+vA|f2%7T?t0q|FwSyK-?!$J>!&&9;4?^< z-AB_EnT!+?$L!crpb$;t&VmEc-NV>hGT)h<g(#N z+48PfLA9^vHX0`yo(F|~2agED-*mxue+2f;e5KOPYw`2gRDZI#OSTsBIGGNKGM)6`eOHxz@e(=sC{*szV0S1XjOtQ9VD2kvTNhC(u;FsF7B>WMPKb=#dc=n z3(p;;t-IeqruqD=>V!9(KLb^dE_Sy9OmkuiALyT7Ov9i$-(AL};D`Aas%mdPNHMW9 zcuS_Hc&QHo>HcS=Bs<#+EKeXKzj;CAqi^5LuFJ~FF@wY#lMxehI1lf((!eq#DcbO5 zfBC{`Zeanz&z^sL=9zT#^fF@=e|RZjy9d0ur{Ag@vt3#G?jr<=I?@E(Mha;Zj04ReTy)e6jCDZM@OGvzYK`D96{pYip`TGyt3BRQ!-)eCYi^sER>qHmSRb*~Qws!aJ@C_Ki+ z6BGQWB1OAo!M9|c;iWt-?Y9H?&%oqMam#4LS#%UyGsxV1dzZU=pDl+a?y#Ym#eRW} zb|Y5;yewTv0B^lPBsP4vTSi1g#`f($U&&$U(rFSO7wY8zZ>%P(!ws7dt@1D?2imai zyX@NyAMlCEk-YAVFTHEzhDlCXdL)*r%B?w7>%4R(`|F?5xp}eq7OhwMBDp0>ZLeUU z;sgwc1c}pPKFDxko;_b_x%RU6=zb7n2b8KEWiM}Hd~G4p@N~FhAi<%0WUYT2F5pwz zsqjl>W3ER8`{y}+B!4>)5e=GkENoFUdh#BeA|{w$kSBOP4D>lO!U8aAjoz3rfts{m zHKgh^Xz#P&dg{iY&_RxL9G~Zv#|KeZ3%K~%NuAE;CgoJ#G zNi}m_10);-eN)Tn&wBLN5{wOi$dAWwChjqoFPQkU6Q-^IP}v1?w3D?7InHI|Gb`47 zN!0Z4P!T2ex=OIMI35^@Tp93(3f5vFZD`hyp{afE(^81Y5vPK|PGX>XHgWa0!s@=_ zsrv^Ft*uYfzd^jAW2&fz6;5pSf}v@dYaBL@rjT3MC7ZIWm z#0eYa=AHS7cCK*Dt<3=Io=qGP9g2oA~ekV6e(fJOGbf+5c7{dY&{|77mQ6ac-T z=T0QYwtC4h_*{`N_q~+8IQz%8Vm3g^RY)sORV|NOP|J*YBy z^Ll0yObSL=P>Nb_Fs|%rVz=?VlNUWD z00Zfjzva~5Z&^^8OT zZ1;3`t?$D`foF}43Z+Cvca0SkT(aZBxbv-1c>H7c9bcLi=4bhtpz>Ahtif>EbQEFKTs%oxD5FJ8>p2&J$ujLW2Org_}gwHuV$8TsG4p{}?`?8qcw z4NZ4zD3WLtQCcjQ-MfW#MwBiR1D8(#orOLD8oHpb$VE_((z6KVy}q|)A{oIWnRz{< zo&0>k!20uh;p7TGM|gm+qB8Rg<~eX=cYd=Hu7Zt^3C_p-b>i`PavE%RGAfex*{U!= zFoIV+>7i%G^uzkQ1(aOyLr$@=RE)C`qE0Y~|eVw#w@zo|M^T7Db8EbBV7-Nfb3)=6T9 zm*R1cCyJw!aU_6{@c{}Zcbz_T-GFM$f5&w}WFue?`*vF>lF3EvktV-{aR2N53G{!r zhrhzk--`&BW(6R>ma@=UvV4(bF2LJYL8=NB7Bdve7!G%N;Av46?@zB0D#PNxBaw+* zJD)`=lLE=>4?8KzN+3ywq8T?jt#eM4j<*&6k{D}m&u_zQE3p^%3`r$`lz0jgLqEC_ z)7??zoBSyPmprs2*iZ4A;z$?icTIYGz2X1?F8Ya(oT@P)S>yotD#kt3Xkt z_S==zP`CBw%jw2@h zQ)GK0o(_&GMoMJz4G?U`Bxu3xk*Yx2HYLxR55L9zp+hsg)X8GITP^ay zvy-A6CaqF5o=2O)MeGiT*eYzniT5`7En$V|gAg*H=iFgt{8CuLCs71-Lu84BpksG5 z{1Q5Q**EqNga?|smN-pc82!TD=O%Rv5&T}u!xh` z+dJu8@Wk?(&G7A|N2_-fR%aA~&1y{s>`p^8PdH*>N;`y6G4*T95Wf7J`^~cHi11mMfgHKMqluMJ5 zTc>^4x$fcm`4c-&#$5Qiide!^cH-L~5wsqG;P|Ov3`YRJw4oh%K&9s3_PU2O?dC_N z$JlEJ#5H2OMoo~{d9AiVd#2!YUA8z`X)@sN3!fe=HDz^%8M z{J7I%X|G+a3u&P7eZ|JgnzZJYsH$zCF#X3FxQiz*Mpf0%ckiW-G|8a#8MqvOM&zzt z`grPwRwOO%d2e*9(z(?Abghu?#CV>_#q%1b%~oCS=0c*rVSt^hQIq?`A^>SG(c*;> zC8_n!-)7YuA4pe?JwQeL07`xyC|MI@@gr2}>ja0M%~+UaN?==(jG<2hJnLSV0HFy& ztjftcs>V0gM7;?76R-PvC};1#IdVPqBpo&7UD^p$NeQRmFmWHd*qg4KpMG<1jwPqlgsdyKC-lik9!Rz`;pA$w%aQn& zCj7dLHTPbapyyY6E|cQ;r{?w9^7#DVFA(&v2Sm?i;C{}h(N(bfTp%_9ltf?HWe&cD zM~!z+0nr?NgWk^V+lkMub2717Vhp{n>U)~4-5{PMl;gR zTe2#JGqO!eaJITLB6$dvW|s$%s0kVTiQUM(@ZQ_J5ET}dUQW{y6Wck?C?2%EDy{O_ zs1goz;|i1E)dMc&sI>>?;7th~$j!~YtmIxhR_cJCXQszOiPxxKVH>s)*qnB+seJY5 zMy+elkxA*9Kc*QZcmmTgf)AKOxioDX_HSsBDCiby9(9b~^crvPU%v+x&+>>$vLmxY zc4N8PhJ}*V`PDTeaX*N!@8`;m-bH;d67D0%dD2^pmVb4AyT-YfIDjd$e)*OudFahM z?H7&k1%$=WgIYwx4E`cD)QbUckV1he^ye1=#zU^y?d|ZZyqng2C7=?WP)(>|MoSB7 znf7%GclQ01Fl^HfIr(8C<#@STN6hhGT@DS^!zf7O+B9#qxC2lVM3I8jtr3ymR<1`t z=Uw0JceOEar?@-T>9}PC;-tYg3MnEU#LzTC;?>&u-TKoKO}>IBu`1LO+>gqJXLRo8 zfu6{yuNjy|a4y6i1kqY!nA2jmQY4jv969>yiO+GRBk2ME$;AlO7D<2Q7eS?xBY#4D zT44)9dH}?;k6&%93S`&_8GRDRIKUK;EB~I{gFhV_8%XpFot!@F1|-T1If*rF)pm(u zH$$iZwMG&q24S?BcZ#&Mj(iGX*g%Gkl=Dlsp-aOz`1oWTjDNfX19NV1UwiD_l9NVaAy~}zs0t2Ze?1|K?go5uBqA7-$7crLrq`gD zr7YDFM7IlPD6iFaQR`@`xyj%$H!P1)diVW0Ml}?w7|;ns9q_Gc8O_w2KcD!it-^mS z?T&|Q3EENc)A^9`90=lHc{f$?C1(zg9=6EP9(#ba#z;-b9SD#~nm2&YL)8>Qr@sY@ ze}e+;aj2v@jcIWy-0C`cRGKJo(VMGNul5MbI)}CfL#1in$INUp_`PCHrX1_*;%w zM1Fayx*O6TJL0(j^-b$>XU*-2OaT}ARt}4ZJLEWMP76t|m=zQ{MZ0w~Fn)}O-Z3G= zl=gZR1Z;VNh4q!>Y!tIPvvGJkBU_YQ7`Ju4cTUAA1 zMz2o9=}~FSr$h1QrsbSHe_oeGGT2r*2A3~wG1K3A9c}jSxXf(<8yTbpO4cs>4!mz@ z&#L~>qZra^!tGgb{8N8_EPKw-qUXeh!)oVZTduOEgyUt+Q#RlI)pRU%j3v?&WwdwJ z+>Z}6idPA}d&5)OnEC1}#a)4SF6DHY*WzDS+#GUO>avt`I0DC?1eiNlONY9eI_HW8 z_brH=t|tM9-et027-2|RHOcL%Zq66Sg0QuTEC|ZPl9!{;gD3{#_gkdjeGLNau{k1q z!jG5;eD1O*N8C5J5ZK}}&x-Ju3=aWa_&y8B5DbVEX7hmop4Hq>w4Xs_c>hhMG)N{Lyk1JZaf8{6GQ#nDV)n4}-q@>h zeH&9oKL9L35_~^i>btlu(3oWw_b_xUlry4dIl^ z9vlTE88WP@-jR1cWX;Hd??bs$?XK2Xi=ecJmjZho7^*+JHrbdQd|DlYoL_L|hd5kg z(yDLVf*TiNC*1W~{&zTZ01*A_ToN$wAulX|^~IB}VyQB9lZa6R*yPCj37Rmo!|Wb| z_L(zWYwvG*(ez4kGuS|~8zF^o=G*V_-jZH>*{=Er9ft2@wYb^-`9{Yv1?d8^H)3l_ zEi0{aoxDBtSspadMKUQ%)Xi8N=)nyHVw}YP;ENz8#;tbrw4S2+j^Ek$h^)Hgg^N*& zZZujsj89)5lnv=WIsD@6ewYq%xUS_V4zu$=cqxJWocJ$m;Ea{k=2UoVRi8B0h1H;>GPrfy-Idqr(UY--(wmwcb2GpQzpM zW*(R&#Vt!$KN2oBgYmmL8C2U&v=lkgdW`gf_92)OtQoM+7%znda&oH_=?9OAV?cCc z@8Whkj*T$dW<9v^2VP!D;>AEBkjc+TyJ0-92%sL8MyVYFR}C&*dS*8uS3O5N`Y;5e zdy|5Z;*&X93@ztlUAR;twz~M5(qB3sZenLQ{W5H~fV7JWXV;{eD963BzT%+bm#{>Z zl}^VFm|cAk@q=9rdoF|&dMZ_RyX}W110*G|a^XY&KU(lB8xjLZOYWk4!MC7`rW~B+ z@NI9PJlwnPw)WCeAJgsj@0Y3Dvv|yngnTbheZ`wEiFX4N4L*G(Ur#YFO+`Ax_3PKj zdG>0G9P(|3npIL%d~hLLf=BbnVT)%Q?%#c8Qm5lwUtWSXiytj}saiYJNwGy{WqVij zs6ej&BlNF-?iXOrVWn6Y*6p%(SRGmjYucECtQ9;=ur{GQVDP3;O^R0hL{-c#9UC@} ze?m};t3+4&Vkd!vu+uJCZj!AjXGMNv*fm9TGI4cu-&J|NeMk zO=@St8aWH(;gWwRaE>V}zLVV=LpTsu9bSXIeerygc zL+Y{2SRj8LR#3pIy{G0unGq(nQ)-Tg55q}d|90b5zS{AyD|cDa)OK$Mq7(E2(jM~* z2Lkp)GOk=RI$E&kYW6y&^EVLW(HqyStZJKo^0E5+HQnxfR9Ez3CTw@pl`+Kvkm&OGlX zmwP6w*@XPp59zLg4$h?if+WK|dq&UTV4!M`^aL!JNw)$pXkSgtA%E&2XNctgG6rz| zH$|L_!cou2%pk?hc5y0wA?ykVK7x&ak)mH<)S0>lVScL}4y|4{>s$5FM=M$>0uKlp z_-_N?9NY!;NRbT=Q=;9f4!`u9IwAjU0f7)nTBlBwbwx>SgXs&desDShD)+4~ z5EtC<$;G2w4dMD9M?;S}W3%{)>1%+gAceQn@9=TK5l^!7UjB1VuklBWglcW`cnE5J zjR4%{i+#Q-k!OQXj#?x6dQmM)2C%I>98DRDKx3gofRbWEjLS{jJFqM5HV#MsDF3sUGOUzy zKEuoPGZOypRdysJ$+ZYRU+@>+EF#vgL3IH>3{|V-R8bzfekQyV4>lG@D6U)c@*@}U zkKsWB83IEkrVPFXydF5yBth+eI!;xz_=qtxHp1t>oq)vmK*b@oer$__7Zh!}@?yTG zNEvXy;}EpKv(m>yX}@#>h-BvX!C+~a9Wwkb3C*CDmGwx2A?~tVfXx-TG0ao-F2yY; ztArV*mKW!tGLG%G86Zj#vs#@K@9G~R_Tgql9p#U7S0PRT#Wd~A^9h)B_xKna$DgIh z@w%OLep7HHco_PM-RY*;Z4x<0slPZ%0%HBAY@^$W#&0uT0b`hTnyYicbGR%fo9qomyRoci^#hmpW3G`#Gz~i3`Jw?CK z@lJ?9m!>js2fvB3IIGKaQ7xQ!eU7C@LFwzO%2RbmomHcu)&a48tzy9^c&8Ee-=swm|VGPd$V|`9iJE&f>WRTBxDe{rkJRC z3?fL-F~7z;1^;n%5hsT9K4SX;AcUv&z{#p*dj;G(WJ_739{GGvc-(jH1hiuK3kWX$ z7r%M4KLHv<{7J2==QJ+D$MXW#R`du+XQ2c2$}OB_KsHSa0DDXnsO}a`9XfGNed~Wl zt(M0{=$zdB&mGzhJgneU*j0O%h7R{h7O`e6^$)Qv$GlgorwywNLaXf^YP!K|8r8B` zvq=9s!eOW(p$3(@jWPvcuwszWsV>r5sf;VTO8%D!?)<`^&)$Nw02oba=VSX3Q|@rc z|0`zKN^AaS>9MSP@hwxaE21Ej(q9C`$Rd{uRPrr6ynm~0col;P_ws~nT$F#}pI**qe2&+1f{?089=^B6%yG@4V&eX1Zl*sdbou^b! z;Y7sl=Zx!gbac|b`(Vo1E!YTR4U+B@nND`#=c_RUxp{q|@4++?`xSq> zvKvtg1h0QWTdrezOXtj)GZ({H0P-(nC@hMdC`xP_Bkeb-HqN@=ksnzY2|b;$DFO9z zqz`D`dF@iW}initZ<e_A2X7F*)B=lkP2RyADq7YTsKJ$j+o-E7Y zXR8cH&ZmYPGI^hwL69Qit4jKJ8|SF_$>N#FNlGfOtoUBFfyboMIWTm9G|B6`^k@U@K7RY3ycpc7Lc7nZTSHp z5dMfg{a|2>5jrLL5`FC2C~!^ptU z_j@=xcs*Oy=+=y3dnwRjmiC_WDV?sG|7tM&{nYPdL?0}iT0{B3W`w}&?~HiWu&I@Y zz)E}GYU6CCVr{(?2^<%=-Lju_0V6@4paJ|Nl&KV#qEkT$M{i*kc{9F0)uuL_IRM2` zecLCmKNx&Z{Z6K5nvw6%P;iOV2G^F;hjsYJfpPB}*BbDmeR}b>r>$D^XeLX}%*~ed zeO3py$AOMTqhj{K;NIbxK2po`>o3&6{Rr9oC-&`&ve>aHbr+|Vw+t#Az=M#}R+r#mf|-fBS#ISe z{A7}Blt{};sN0qtXP@dF2|<;=VpxiC)?OC((d566Zvvp8$R8;$zn>Jjs;3Ql(vjFY zQYDpb?z?XGR~D3GjOR?_K7d$KnE#;_qyO0PtKHAbe)Y)YYAy724^PG!D><>pLG-)u zyWzG_{n|!fez03LGo@zTm@(+}!-Gdc6?I_#3_Z?$2&j#9qQ} zlc))1!AG{N@HWSZfonaB<}BBtDz3cG7q%A^^Ld1oxGF>^orAfD|J7fso*u%Jw7ze! zIG>P^fM^2%%2U}nMGT~p2?FpbbVvPFd2aSzizLIBih4K3vvL6jrTwQA^EtQ8Um4nQ z;BqT4?<|NeN$fU13@C2gz+~loj+hG>mN2dRS zH?Q6-MEmV~xP7}5#Ee<}asJ?$C2ieCM$gYYLKwzofbi*DIc0}Z_O%SzmJCU4*0^m7 zVvmHAX7$8ahg63-%0@Sk5pU{1V^sz&| zfJ`#~ae_@@SoQR#l(x&_Ai0&O!p2aMW*3DCq411FS;V(tq8C6sT^w~<$~{^~V0wa^ z96}NFfv1mFtTT-DuNVq4(vIMZyycqeNFIhc{HJS?zRG@+jtz(pSeJECtyJY$H`9zPLg$3R z_|~T-TFH;-QMAXXAFDn`riX2VrK9N6YDX4ez=|hpPk0}#(GYI2YjrwV+ko&igu$dp z@A$~~K>4qD9Ho4RfD^gqKCms>(VLhFkgdn2 zXM5+RFA48Go~2tss06-UBn%VUS8>j9vD#kZ#D}h^3<65HQ;1`QunxJ<6hOF-h2IX& z0*YMOAOdtAo0z}Rryzfi(tO2x5h{@DV2G|>*+LnMd=R2C!FuojtMB#B&K~o+lyKU6 zH+~_j>FeCv+eLFdWBT*bnDS+(%eQQ1-eoQ?-DsdVIrH0=@c5$mX24DcHBdb~ccwPf zq5dq)Lw2bnA;~EzAKe@akwF99%ClQ00T;*xQ@*mi*e4mF$ckT-+-I{4d#D!?ztv#% zg!G<1wp&>6qnZq_`Ke|+2E?aAGX*{z)YWlZ3lC3AM+SXZ{}y)@gum(8!|ZdXBLokR zIjbdn{3P@ImuKkDiW$fnjcqfu=te%%OfAFvt~8(~$0>i97zSo4=MZ-$4+e+KA6+ZoAClCFEVMr-GU-coMyR5*C9#vIfdT@MV*&U zB(!LKx&t+18Bc)RTW_7?#}gc&<&53%$;qf&2bxuE8Th~;J;(R?X&UrJ-;RFP%!dX>x+v7J#{3~cdUi!&90+or8fM5_5`G+~eZ6ZHf;f)rv2 zO>6CjCsd+^XCo~+YRLYO)9oDoQtE8r5(H#rVDC)`o16=JDp_ZWFt?Z!`rU1Bd`d?y z&(A$J%2o?smQCV0F5UY5Eq8Tg(M&n?dy~282e3fgk9b+=y?6HaaR2idVC<`sEyXZ~ zy|fuJJ;yS$jF-E+z~$8N9OOe=UO_z!p@ER|TeMyK(I%t< z`A{38$_D?>+_0{y+VJ>#hXPHmeMuQ;TYNHZKZFo?W8fBe4~anGSc}!6iYvlfJHqSs z04DjxIL5Ur%JMH&*i1Z#se)}b23HsWp$SY6zT(lEcA%D(7MT3 z^#(9m37wL1tuoAOTz*SavR%<4cdsZvP0|Ky%H9<9Iq`_C~&M;At5D5?jNa$SY!>hC9+L=s)0HMNNVljloTmy1(Qm>1`{7SH$IE?I(6?;dIr zkX5Ojc=|}g{^!TCw`us^*?wKEow-I_y-wCOBkLfH2PBG`=u%*Jp+l3@%+cCc)Rsqx z<50>b;~&1N3dmE&%rbNk#(O%zbI~c`_rmf@af+OI&4t#FQ__GAM7m@m18QqCG9Gt) zhQC~A@$}xOt-0yh1TRlFm}v0}66Qa4?sUd}NlfBCO!GPdtZ7i7!b1bXqI7be7tX~S z+VRgIv4BKn7XXW!R>~~wG)DUbW}?JitV6`7RtpvDg>e4wj*4mn!A@f4_sRqV=w>HW zGNODsB_f%)pI!|1Y71J#+=DIRndKo!9M313`X~NY!!FR#3#r+s$k2IC8H3ZOPwV`V zUxF=;wAkPd!pRHypPyJ22Hq3n-ZD=@)b=JX(vQd);7E%=BZBqTX))=_%hZ#{rClNK zfOiGb+W;wKRTNyCxxF@;v=Ct;d9&9P(+ofb95|a#^v&c!aAPM=vZIgDI_=d!yzqas zrdKD{ZS)^1a@Z73EC1eXDv}fWWHz+Ho%UURAaDopfk})fdRJHG&0IOR0Zv^Y2qPqG z&p-@t-G|h%UN`C-HK&rf%8neW<7Mxx%08OC%B|PjjGbL>^$hEjgoe!ZhzgnE+0*ZPl>=j<2d&DqDiU1UC3DIX#b>lv*>F^fEz#MM4yU z5=s^QnH>&=EK<9zc{mY!C4jCoJm0q|bg0Y(lktAJ*NxQs~N3M5l+)08J)Fzg7u6At1_c*F8=Zs6`?eFfc*M-e+lNb zO~4o3Tf%$pVK1}PnjOD(TgGGM(7&~-@t(F=FG}s)1f4ufcaZ*vhv(jFMG~;cV@_xE z0zyK9M2O#)rbk>$ZQx*VO`q2`8xXTqutl$>H0M>>alKgx=QFn%F12gjpj*}_OhOLu z6C#Rus277wn0+h$a53l?AS`wA<;SU|&L6MKF0YkPx*la-YZd8j#55sm=ofZpoTe!(XZUrm1578l;Z{wM}_MibKkGnbD$CPrcxii z*LL~rADz8V0|!iJAsS`?QwjX%=KT>XEyM4LSg?^=gkP__8iB#ds`JOEih<`XRqNDI z)g&E>#IUqjZIAJCrteS8<%2nw&#IgK7gFp}GRb0VZ-|pJmbhE9aAoLl314UIovh5vwM4X(0GvP>v7CS~i9@a3{4UWsmx*Ar^nC+sr#ff=gdvF3FSIp^ZcK^)S$E_L;M`J!v!6F4xtc7Gj*JvY+~ z%;t@kd7$#%`}8P&!dOuT+GHEDp{DsNbyc%#YvV=qvztbRD=0xwU2%VcGwOAp_By&nNAj1Q*oFuBJZjI922M3|w= zU@^-eC2It44Nbf>5h@cpd!%8yz4)dKG7bPeqO=lEb(#MSu5*AtsA&n6+oyIwblX}?3?~P9A?xwg zHeCMla}u|)>)V!5)gC!sck^-Dwf$pSx1GoI27Ei@o-EQ6_E+Me{3g2H)N-cR6q6U z6=pZ5e))YsR7Pmu>VW^bnpC@b5V6g{C8G;KFfksTSet~oB!l+7WwnioV*ofPrzMZ= zqqZudYLQ__qnVhP5HEPrCH?8>Cccli|2sY?LsMuTbBD{Z0z3(nT+{atx|e?MJv03V z8dr5cI!;+6QDV?Vt<5T>%8po%3GSV<|^`&^1_z~ zMD8xTwVJ09odCDWj!h#qD#Ozi#%XsFcZvTvov?Ow zI!lw7OMEG~E07>5+EIElpJs=OvNCRHAe=&N89?Q@87RMRmm;E_msw7KgR{2SzCBzP zMb#d^lhW(NCa)-8S=bo5y=*~h&YYs%z==2!(dmJeUf5pO{3l$$Q2dj`9*C?ZQJJF~ z8>dL<&>wLT$%4zR;*=tm{TfC~&mVBVhjPi6$df6pmXhmAQb%`BWnJk~dC_#|ceh#< zCpR}MZ$I)V(^%@Z)JuylUxp41J(tiu=36xJ;aebr8;Oj`R(?Z4Z$43r?@CK5jn|=& z)Nf&9HbW|fgfS%#V+crz5 z!9J(;n830mb>yWP?70v6?~xWTf2#js9fh*P^@QS4&8UJ=%~0B%jRh;v8xYF7p-y^j zM)7;7&Yw$nPSAQD6&V3lDRQ)GG|sPy4n*hi@rooWpx>CE&o8uc;V%SFx@*7krzV0^Nb8HhI?w`Owu%9J;VnfUN!MSg9hqs@KcR}U{bT1%On_h~v zyuHqt#B^fsQ#hg8As_21urmL;Vi2=Gf%ESNzJ}p=j-7;qX^RUv!4>4?2cA2PC=`#xUsJH5yLFT(KmNV?5wX|7A>Z|UF{ z>P2%zd`H)CC2N`crDUxNZGKi?e167*u&kycfgH}{dY{m{We@Lm=zTTaUQW?&^U&o4 zu?ta2{-rd-eBI<t{H$+I{O3PO<|AcNUP9DXoVN^pPDK=h-%!F1@56o3ZkqK&RxsrK=M?JD## zZcORoKxFDCIygR-OeE97qS1EWY5)~P7@H{q{lu~7JD3?`*!Ey5MgYR*Qt@b})G&umG1L{mE0~=l|89`8yMMh`D0DJ6~`}q{Ir~mr3yO!rpBD=6flliHQa&*w<@|%4Xo2QlU)ah0=wkg^-reRtWlC(9*2V+}%%B5( zEhVXA3@Z!CQHiZ&NIH&z3w0T+dSOMVX3k%$*Z`TlW-%1)r0 zKAU4Nw2N>~Z^nS9NZ0DHzE`@fj|qN1s2*tIuUgQp3Y`T{{uaVjBD#@id{smo%w_P*ydDv78!8@Av=9v~z8v5kS`V=-)HBEl zk9!;z-4uc*`~C$sUnK!S!M-LHo@X65GA@U8r_+vySE&ypBvSj_9grI2^&NQk3MJg{ z01x7GetAhq#tY(+dsnD?05BCWff&0j#QU6r= z$BsFBF*EPTkgo3o@qy|p`<_;RSZK@z*}ZwxaG0;k?oTDsTnJ?OS6Z2a#FZ917~XchRzTTe`B9>zy2O=Y^Cnm1On0(se2~0RC%%|>AUGC0YtmV9wg5`0w;pq9T5=KZK zA%dyqgLk5M@!{=l!^i8hrRH1r+iqdb6W^TFH`*2CN{_XOYw>S-a&sBt4 zwi<=-x9c?^Q%dX2PB7pnw=|x*V06O}C=q_dFctoQJ$#~YM&0rUZrtM*q*x3GvFzqi zVczu=9Rd2MF89T zd6zEXHDwL8+3z9Rp?`a4?$`TwZb!2_l;D&HB~aIV+Pi81e&BK>$p5I4Z2)U;?T9RO zMP?3Wy9j-wSxCuQPUUo3UvUXn-EFEC+BzUv)NkKln;&NPcgKNju2LbmxN_`eBf(4&K#lw+@ z(E<|Z3?D}-nSUNu(%d!xeUAj?n_qSicdXfCylmv0g?=G(ZFl=emQ>%?EADuVE6zW3 zG08UgX6IIq@BOUp}R%F1Wo zxD_#Cb{uWNk*CeyRM@s(@k&%Ue0^$SRok^)PPTWy#@&+7Y<{ow5}oj?sV{oKgJNM| z53Z@ciEd+4gL_UaepdLQ0b^q3AN*B2rgoEl#axrhbzvc z5oKJt!hxlCOrlY&hlxjiBW0h2vs#r9`$nI@5ASiu?H>QPL%z*%okx_I$<^r8maT6& z-ah51_-Zj!bfoOxN_onOs@OXa8tu-yzbGzqP-z!4U;;b!S`j`LdXo8Q;OJFZ;L7F6 zCC530MB@}b8Qy>1{G;=T78mre-5yzk8SURc!bX*V=xJ?dvsAra z9Ix@T+SGIjcm>?00QNus3>~f>@h?!WEsAheLk2LIr}TSlP`{O*Q{Vga{ENPo)3uj} zZhOHdOVjJ;HKo^GWsRstGrx-fy|Qhs(8vYYBLGw46>VJSVPdv=P!?tJo_E`vXE+-}oK#R?Zp`@{xFAV>$y)%D@dhf$HIi-cRla2|c6H2K=V~G?k zBw2Dqr7Tf1p_DzuNF+5{4$;`l(otk9DWM`-Ffkc~vV|l|XzcUcAJ2KtPtTuFS3kt1 z>vGL}=lgws?)QDa?xnG=kGFj}Obf;27mn+oqV1mfy=uzk`N88cBY+nVNvY*q4Y)&O zM$e@?Q_~6fS(JDs=?0>h@G3=CK9ZzMvkf2%R_SbrHoBAEwi+o_bP0=aa4OjS`t}h= z-M<>2D0lp50*&0x*7k+I)JDfu2l}3UE_bCyOJeQ3%xmq1fZZnQogR2bc(?Y%sceUAT`J8g{Bp6XP{U7-~@%zvQdr}DjXzp z(BCP=)WSh*gP+L60Tq-Np-1O?1rre1^ z*T2B^NBsyo1j0FiSkmJ8J>ABLegNPuP`16~Z=q*eQ*BxVOuv>k%X^ed*b*Yo;)Z>( zvOx@VR zPoDQR&3US$QSxHY_h6+x1M*#a9hlSo(M6-t*^=&J5DbTFt^c4_C@(P#a!}iE?W=-s z5kMA(s|l2ygbi+N7_cG&a()kLM~GUg@(ayGQ)S!5eDT^?Q}$s7byt%@vld19nxS2Y zy%{Dt5uX$WOINF=$}}~+yj5t6Q2EIrcsyC6YWeG`uBm)-jh9%pGNm<-axVJj%}fN| zVr>Q(*e~0mL%ek#snNi_UUL51P1yQW-BxN?6sY;;Q7F*mBh3gIy*Z0$+II{)Kzy3s`N_udfNSqh?f~bxCNvGNoglI z`m)>+mkChu7VRkHd}+pqD2wTEWksS)kBxgNh5qUQ77O;nZ*mK;w_|#EDxZ6os+bY> z*m&93^3THFmAVKrAklzAV_EwbzQ=Ov3!_Nq^f+&P(l*mQ-*S;_TVchT;)J%OTU)#8 zu;-6sOnG5m+0pHpCzboU39L0Ni6Csi@~bdBi1U#WaTs+S`SgTvXOsh;P@)t* zHgVXKte|G)F7a571_F5I}FLYmvWt|rn(j3TAX z2U#<@894KYvwSsn<6_k~G~k!?Jx@YItC!_e_63lMvFNr^J8Co~(jUO>fLk4;h_b3 zWFquJ4(_q_2$~^X$v{})Ud^%tbfl={t}o*1?{$1xaOz46Hj_IOyEeIM&YJV<#_DGe z6ymfprzGbup4r~M6Hb080-X=AO|#6HrE>xE|B0}1MI9YssXPDcC=|qiO-yL*r$7ZE z>mTTWR3v5kiLOVuBjr6|;DWtXXa~>EiQETrzAGH{lz{5~L7$!A?3imLB_WYhz2$f~ zVa0bv4>!PKRAYakAs^?3&jpa6?UfXgI(k%&&~HJeW!V=^+g_Q6JyMVOwA2>#x)b?p zwy4H_Ue@kH>gzh)ngjepGU`_XV}#oswL8c9#%p3?DlV-*I76mBT2Fm#`TEC?U4+r$j+ zp}g$j`>gDeM3s2cW^_;tyU)gX>a3r#4kcmCQWzd+4wR? zT|M#Cg!5S3Bh)V2y`6rsp>^)~Bw`!I0(D~;HCtML7LV(}bD2Y~PhR9Ix|tNEW34%(s+|71MQPKk!#9&RDrn%UrH==w=(y?CC6|X% zybNR(b7h3yxypH2Q^hVzbpvX+eHw5*Qe!=7M&|9p7?wUU4hac-9E{^N@39_REHL!2 zG|734_yZt^FAOd*P*dXM3Fy^IxP0>E31;TchMfXOo!#>cU#!^!Ox5|0kkxV8isBC> z7hyj|w#eFSe?|JO&d$zs^YnHgOiAGvv0EUFiP5N$!Jj-GW2Ke~114|?KoPY*%0C9qC&t1!xrg`adxZSceB+;jvnj26z{dHh2n9MFU%av^`^Pv+lU8oBn&o(1rS?N zb>Y%fZr7@Q(_5aYO&kbkP#A{@B#!XH8pfJV)a+s&WToCBz90J0<=bhZOZhAMr|9qL zZffqnZZK|l*c@@_|}lomR74I`ovgR$Hbug z2iq;m8jWO;mb*-**4?(8##w^N@`%Q|S98}bwKcN))I{m2X7sZVV%6BbYQZ7*B5yba z(}&R$JkdY7%`z3o3itp>8>!wlRkIRUIi_ofNXi_lVgODfCKS_SF~0}EHShxm!J_5F zZS=AWQ^#~?(b)kVG83J9&f>!TB5PpB@Ds?w3XPlMawsiAxgs~7xQ;Mmmv_`|EWqGe(1+VS~ zTG>RG+$^W%a7Btq;bmlj_$eBhlwy8w}bLrY~5(u8 z1YOqE2zDiZ%`?4ZREkY5zxX4Mwl?C>pg*E5k24Z$rHL7TgXQ^=tIQHCUltNv z@x&C{Hn(n)Kh}2!k+#bCE3EN69+CiH)VK0EKS&f-!9+hB10%6Oczb`SjuzyGa)gBE zH@5_@CD9d7LnA~Ml_G=q^#icTw&?vT>^Xydu-Dwr@dwraq5w^|TH8ZxvE>6+- z4sv*^VT|VK&3nl%Afgo^(=LWX%%;kYoYDs+e6^S!2JQFj;s?ebo@~f1q@Vv*JSrhR z!T0d?_D&eQw}yWM^(-b!bjaUu(H%g1Pycg?PQ4=Jv46feR!rFN|NanervH4QDuTVq v&;Rh>SK#NXB)a40rSbFg@c-``ZG2(P`>n~>|4jQNfIqu+jCS7NZWHhyR!$y; diff --git a/packages/scratch-core/tests/conversion/test_subsample.py b/packages/scratch-core/tests/conversion/test_subsample.py deleted file mode 100644 index 0d3d3980..00000000 --- a/packages/scratch-core/tests/conversion/test_subsample.py +++ /dev/null @@ -1,71 +0,0 @@ -from math import ceil - -import numpy as np -import pytest - -from conversion import subsample_array -from image_generation.data_formats import ScanImage -from image_generation.translations import ScanMap2DArray - -from ..constants import BASELINE_IMAGES_DIR, PRECISION - - -@pytest.mark.parametrize("step_size", [(1, 1), (10, 10), (25, 25), (25, 50)]) -def test_subsample_matches_size( - scan_image_array: ScanMap2DArray, step_size: tuple[int, int] -): - # Arrange - expected_height = ceil(scan_image_array.shape[0] / step_size[1]) - expected_width = ceil(scan_image_array.shape[1] / step_size[0]) - - # Act - subsampled = subsample_array(scan_data=scan_image_array, step_size=step_size) - - # Assert - assert subsampled.shape == (expected_height, expected_width) - - -@pytest.mark.parametrize( - ("step_x", "step_y"), - [ - pytest.param(10, 10, id="default value"), - pytest.param(1, 10, id="only step y"), - pytest.param(10, 1, id="only x"), - pytest.param(10, 5, id="different x and y"), - ], -) -def test_scan_map_updates_scales( - scan_image_array: ScanMap2DArray, step_x: int, step_y: int -): - # Arrange - scale_x = scale_y = 3 - input_data = ScanImage(data=scan_image_array, scale_x=scale_x, scale_y=scale_y) - - # Act - subsampled = input_data.subsample(step_x=step_x, step_y=step_y) - - # Assert - assert subsampled.scale_x == scale_x * step_x - assert subsampled.scale_y == scale_y * step_y - - -@pytest.mark.parametrize( - "step_size", [(-2, 2), (0, 0), (0, 3), (2, -1), (-1, -1), (1e3, 1e4)] -) -def test_subsample_rejects_incorrect_sizes( - scan_image_array: ScanMap2DArray, step_size: tuple[int, int] -): - with pytest.raises(ValueError): - _ = subsample_array(scan_data=scan_image_array, step_size=step_size) - - -def test_subsample_matches_baseline_output(scan_image_replica: ScanImage): - verified = np.load(BASELINE_IMAGES_DIR / "replica_subsampled.npy") - - subsampled = subsample_array(scan_data=scan_image_replica.data, step_size=(10, 15)) - assert np.allclose( - subsampled, - verified, - equal_nan=True, - atol=PRECISION, - ) diff --git a/packages/scratch-core/tests/image_generation/baseline_images/test_image_generation/surfaceplot_default.png b/packages/scratch-core/tests/image_generation/baseline_images/test_image_generation/surfaceplot_default.png deleted file mode 100644 index aaa7dff7fa6f39174b87062d1f1c3455007ddf93..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 228915 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

3htEKiIWG6}gnHy;kI34F%lnL>8!zI8&4F(5K z;Pyu40-IUJA4g3>hl}4~66EE>+6s-yG9h%^iCa_09Fdd$w}k$>ptz5NmTV1cIqEW* zvDD_a`7F9~n|ooV2N-4lr0;_n`;qVI9SVa7bibQ6Z^n1A!p%Wcx=Gu{gX;Ng_Uw{3zzRtMe(Tm9zic(B8FrfY2GE7-ll2^P_Xih_sH@?O;>F}LD_>>R_y{qg$qa}-jezK&jg^HSYpditS1t*3sg z0}kYTbxqs&s>E^EF7%5ihWVgOilor5sZB2wW?r0fd{q6{1rZXIW@^OJhYb1z6;ARG zX0b>yh^8P<$^0{mwk4Z0wJZ(P%U!uVrX}Ka6>Z`3=bc!sUOflq5*p_o{}%rnDwgrG zO97(~in|{)+2hVI-Z{0_Yc>Fb&qa1=@66~uaLRb9{=yi&)stj%#g;vD+r7K_nX5>I z_K@HNjUn4gxcW!;Ram5sslrIQZgvO8L`1q-&to84MwIRdG5>Pe~WdnqY7BT8Sa)6|3)(PWG|m;C6%~LEC(&sYR;1#3?YW(+k}h?QvCJA zkb55o?nYqhDYaI?5J^hNhvW(!LP=wC>ewAcGe(@ag~q-7@ki!90)4=V>E#tExvaSU zTf`AT$LBU7_Tu$6NntB%Uu;=9L*qru%6=ouk21CDaTH)^Ls?35j!w@AWcw-m3knMM zuUUL>$QjoqrePG_3t~Sj4roRkQoP5K*vWBCuh^%@06&MfcY!5O#vbT$&`j4;W zS@vpKd7-nJI5GKfyLx#;#e3(DXS3RX$I*>tJFLO{LRm%m7jP{JVW@BzZ;xEgo!u~Q31m>FFyvQ?L_f?!kO1#U&k(gGwu!co8^_;Q#8yxkXK9J}9N zKk*F=rqWL#0YGPQWVybwxDG~$O`*45QVY(HTNh_$q8wVfO1GcP-Vb~dI@P^m$C1Yu zlSQn)F31e`hm=fZ3UiiSke%La0W}IC<6+{JiOz-$G0XT`F7`w#oHzTtxO?Rp+=?ZR>b=wpD*-8Di z$%^`Q4<^*?pBA#Y;AmNc0bbkg1FF*RYr{zr<{`4;!T#qXBcb(9`=j!0YI5ZKvz?g! z1^PbUVfY`tm~A3h!}>N_2UFyVWrJHgJnp2YWByqhXSPMZljxyPX0CojEHBcITpDA# zxxW8*%fd;4YSP$BxJ%YmO9AYJ((U^@A z*W(#UI&B~?)1wzv1ho7h(ivmu9!~Yhtu} z+P5-)wXn3jWTGgKK#bR7=(IhlGDu|1aFG7kE6I44fsweyJHy9rDo5AN9L>mnxBP?m z@2AqX9E&aQxn&llmOE_k7}KG7wZP}_V0=%!9Pb#OB0x$DjlgW zrpB*PmSA11lVF}w{`fdjLZteX@x13S1i_($bN8Tt;%OB|6uH)d{^>U`cvR4p4a`;I zU$zwL9h|ICH>pvm6TS17e+et76WmX@O⪻u3sGg@|~MSbkQ*(9`*+0)Zj+Bwr6@n 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%0A$u=6?G>LZI}b%BeWRD9YBvSC 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 T_sh4hWNB^1EMeS}oJ+`T$+U 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

Ty?pv`@JS_OIYy1E(pI4Qe) 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 diff --git a/packages/scratch-core/tests/image_generation/test_calculate_lighting.py b/packages/scratch-core/tests/image_generation/test_calculate_lighting.py deleted file mode 100644 index 1eedd170..00000000 --- a/packages/scratch-core/tests/image_generation/test_calculate_lighting.py +++ /dev/null @@ -1,179 +0,0 @@ -import numpy as np -import pytest - -from image_generation.data_formats import LightSource -from image_generation.translations import calculate_lighting -from utils.array_definitions import ( - UnitVector3DArray, - ScanVectorField2DArray, - ScanMap2DArray, -) - - -TEST_IMAGE_WIDTH = 10 -TEST_IMAGE_HEIGHT = 10 -TOLERANCE = 1e-5 - - -@pytest.fixture(scope="class") -def light_vector() -> UnitVector3DArray: - return LightSource(azimuth=45, elevation=45).unit_vector - - -@pytest.fixture(scope="class") -def observer_vector() -> UnitVector3DArray: - return LightSource(azimuth=0, elevation=90).unit_vector - - -@pytest.fixture(scope="class") -def base_images() -> ScanVectorField2DArray: - nx = np.full((TEST_IMAGE_WIDTH, TEST_IMAGE_HEIGHT), 0.7) - ny = np.full((TEST_IMAGE_WIDTH, TEST_IMAGE_HEIGHT), 0.6) - nz = np.full((TEST_IMAGE_WIDTH, TEST_IMAGE_HEIGHT), 0.2) - return np.stack([nx, ny, nz], axis=-1) - - -def test_shape( - base_images: ScanVectorField2DArray, - observer_vector: UnitVector3DArray, - light_vector: UnitVector3DArray, -) -> None: - # Act - out = calculate_lighting(light_vector, observer_vector, base_images) - - # Assert - assert out.shape == (TEST_IMAGE_WIDTH, TEST_IMAGE_HEIGHT) - - -def test_value_range( - base_images: ScanVectorField2DArray, - observer_vector: UnitVector3DArray, - light_vector: UnitVector3DArray, -) -> None: - # Act - out = calculate_lighting(light_vector, observer_vector, base_images) - - # Assert - assert np.all(out >= 0) - assert np.all(out <= 1) - - -def test_constant_normals_give_constant_output( - base_images: ScanVectorField2DArray, - observer_vector: UnitVector3DArray, - light_vector: UnitVector3DArray, -) -> None: - # Act - out = calculate_lighting(light_vector, observer_vector, base_images) - - # Assert - assert np.allclose(out, out[0, 0]) - - -def test_bump_changes_values( - observer_vector: UnitVector3DArray, light_vector: UnitVector3DArray -) -> None: - """Test that the shader reacts per pixel by giving a bump in the normals. and thest the location is changed""" - # Arrange - nx = np.zeros((TEST_IMAGE_WIDTH, TEST_IMAGE_HEIGHT)) - ny = np.zeros((TEST_IMAGE_WIDTH, TEST_IMAGE_HEIGHT)) - nz = np.ones((TEST_IMAGE_WIDTH, TEST_IMAGE_HEIGHT)) - nz[TEST_IMAGE_WIDTH // 2, TEST_IMAGE_HEIGHT // 2] = 1.3 - base_images = np.stack([nx, ny, nz], axis=-1) - - # Act - out = calculate_lighting(light_vector, observer_vector, base_images) - - # Assert - center = out[TEST_IMAGE_WIDTH // 2, TEST_IMAGE_HEIGHT // 2] - border = out[(TEST_IMAGE_WIDTH // 2) + 1, (TEST_IMAGE_HEIGHT // 2) + 1] - assert not np.allclose(center, border), ( - "Center pixel should differ from border pixel due to bump." - ) - - -@pytest.mark.parametrize( - "light_source,nx,ny,nz", - [ - pytest.param( - np.array([-1.0, 0.0, 0.0]), - np.ones((10, 10)), - np.zeros((10, 10)), - np.zeros((10, 10)), - id="Light pointing -X, normal pointing +X", - ), - pytest.param( - np.array([1.0, 0.0, 0.0]), - -np.ones((10, 10)), - np.zeros((10, 10)), - np.zeros((10, 10)), - id="Light pointing +X, normal pointing -X", - ), - pytest.param( - np.array([0.0, -1.0, 0.0]), - np.zeros((10, 10)), - np.ones((10, 10)), - np.zeros((10, 10)), - id="Light pointing -Y, normal pointing +Y", - ), - pytest.param( - np.array([0.0, 1.0, 0.0]), - np.zeros((10, 10)), - -np.ones((10, 10)), - np.zeros((10, 10)), - id="Light pointing +Y, normal pointing -Y", - ), - pytest.param( - np.array([0.0, 0.0, 1.0]), - np.zeros((10, 10)), - np.zeros((10, 10)), - -np.ones((10, 10)), - id="Light pointing +Z, normal pointing -Z", - ), - ], -) -def test_diffuse_clamps_to_zero( - light_source: UnitVector3DArray, - nx: ScanMap2DArray, - ny: ScanMap2DArray, - nz: ScanMap2DArray, - observer_vector: UnitVector3DArray, -) -> None: - """Opposite direction → diffuse should be 0.""" - # Arrange - base_images = np.stack([nx, ny, nz], axis=-1) - # Act - out = calculate_lighting(light_source, observer_vector, base_images) - - # Assert - assert np.all(out == 0), "values should be 0." - - -def test_specular_maximum_case(observer_vector: UnitVector3DArray) -> None: - """If light, observer, and normal all align, specular should be maximal.""" - # Arrange - nx = np.zeros((TEST_IMAGE_WIDTH, TEST_IMAGE_HEIGHT)) - ny = np.zeros((TEST_IMAGE_WIDTH, TEST_IMAGE_HEIGHT)) - nz = np.ones((TEST_IMAGE_WIDTH, TEST_IMAGE_HEIGHT)) - base_images = np.stack([nx, ny, nz], axis=-1) - light_vector = np.array([0.0, 0.0, 1.0]) - - # Act - out = calculate_lighting(light_vector, observer_vector, base_images) - - # Assert - assert np.allclose(out, 1.0), "(diffuse=1, specular=1), output = (1+1)/2 = 1" - - -def test_lighting_known_value( - base_images: ScanMap2DArray, - observer_vector: UnitVector3DArray, - light_vector: UnitVector3DArray, -) -> None: - expected_constant = 0.04571068 - - # Act - out = calculate_lighting(light_vector, observer_vector, base_images) - - # Assert - assert np.allclose(out, expected_constant, atol=TOLERANCE) diff --git a/packages/scratch-core/tests/image_generation/test_compute_surface_normals.py b/packages/scratch-core/tests/image_generation/test_compute_surface_normals.py deleted file mode 100644 index 80278286..00000000 --- a/packages/scratch-core/tests/image_generation/test_compute_surface_normals.py +++ /dev/null @@ -1,179 +0,0 @@ -import numpy as np -import pytest -from numpy.testing import assert_allclose - -from image_generation.translations import compute_surface_normals -from utils.array_definitions import ScanVectorField2DArray, ScanMap2DArray, MaskArray - - -TOLERANCE = 1e-3 -IMAGE_SIZE = 20 -BUMP_SIZE = 6 -BUMP_HEIGHT = 4 -BUMP_CENTER = IMAGE_SIZE // 2 -BUMP_SLICE = slice(BUMP_CENTER - BUMP_SIZE // 2, BUMP_CENTER + BUMP_SIZE // 2) - - -@pytest.fixture -def inner_mask() -> MaskArray: - """Mask of all pixels except the 1-pixel border.""" - mask = np.zeros((IMAGE_SIZE, IMAGE_SIZE), dtype=bool) - mask[1:-1, 1:-1] = True - return mask - - -@pytest.fixture -def outer_mask(inner_mask: MaskArray) -> MaskArray: - """Inverse of inner_mask: the NaN border.""" - return ~inner_mask - - -def assert_normals_close( - normals: ScanVectorField2DArray, - mask: MaskArray, - expected: tuple[float, float, float], - atol=1e-3, -) -> None: - """Assert nx, ny, nz at mask match expected 3-tuple.""" - for component, expected_value in zip(np.moveaxis(normals, -1, 0), expected): - np.testing.assert_allclose(component[mask], expected_value, atol=atol) - - -def assert_all_nan(normals: ScanMap2DArray, mask: MaskArray) -> None: - """All channels must be NaN within mask.""" - assert np.isnan(normals[mask]).all() - - -def assert_no_nan(normals: ScanMap2DArray, mask: MaskArray) -> None: - """No channel should contain NaN within mask.""" - assert ~np.isnan(normals[mask]).any() - - -def test_slope_has_nan_border( - inner_mask: MaskArray, - outer_mask: MaskArray, -) -> None: - """ - The image is 1 pixel smaller on all sides due to the slope calculation. - This is filled with NaN values to get the same shape as original image - """ - # Arrange - input_image = np.zeros((IMAGE_SIZE, IMAGE_SIZE)) - - # Act - surface_normals = compute_surface_normals( - depth_data=input_image, x_dimension=1, y_dimension=1 - ) - - # Assert - assert_no_nan(surface_normals, inner_mask) - assert_all_nan(surface_normals, outer_mask) - - -def test_flat_surface_returns_flat_surface( - inner_mask: MaskArray, - outer_mask: MaskArray, -) -> None: - """Given a flat surface the depth map should also be flat.""" - # Arrange - input_image = np.zeros((IMAGE_SIZE, IMAGE_SIZE)) - - # Act - surface_normals = compute_surface_normals( - depth_data=input_image, x_dimension=1, y_dimension=1 - ) - - # Assert - assert_normals_close(surface_normals, inner_mask, (0, 0, 1), atol=TOLERANCE) - - -@pytest.mark.parametrize( - "step_x, step_y", - [ - pytest.param(2.0, 0.0, id="step increase in x"), - pytest.param(0.0, 2.0, id="step increase in y"), - pytest.param(2.0, 2.0, id="step increase in x and y"), - pytest.param(2.0, -2.0, id="positive and negative steps"), - pytest.param(-2.0, -2.0, id="negative x and y steps"), - ], -) -def test_linear_slope(step_x: float, step_y: float, inner_mask: MaskArray) -> None: - """Test linear slopes in X, Y, or both directions.""" - # Arrange - x_vals = np.arange(IMAGE_SIZE) * step_x - y_vals = np.arange(IMAGE_SIZE) * step_y - input_image = y_vals[:, None] + x_vals[None, :] - norm = np.sqrt(step_x**2 + step_y**2 + 1) - expected = (-step_x / norm, step_y / norm, 1 / norm) - - # Act - surface_normals = compute_surface_normals(input_image, 1, 1) - - # Assert - assert_normals_close(surface_normals, inner_mask, expected, atol=TOLERANCE) - - -@pytest.fixture -def input_depth_map_with_bump() -> ScanMap2DArray: - input_depth_map = np.zeros((IMAGE_SIZE, IMAGE_SIZE), dtype=int) - input_depth_map[BUMP_SLICE, BUMP_SLICE] = BUMP_HEIGHT - return input_depth_map - - -def test_location_slope_is_where_expected( - inner_mask: MaskArray, - input_depth_map_with_bump: ScanMap2DArray, -) -> None: - """Check that slope calculation is localized to the bump coordination an offset of 1 is used for the slope.""" - # Arrange - bump_mask = np.zeros_like(input_depth_map_with_bump, dtype=bool) - bump_mask[ - BUMP_SLICE.start - 1 : BUMP_SLICE.stop + 1, - BUMP_SLICE.start - 1 : BUMP_SLICE.stop + 1, - ] = True - outside_bump_mask = ~bump_mask & inner_mask - - # Act - surface_normals = compute_surface_normals( - depth_data=input_depth_map_with_bump, x_dimension=1, y_dimension=1 - ) - - # Assert - assert np.any(np.abs(surface_normals[..., 0][bump_mask]) > 0), ( - "nx should have slope inside bump" - ) - assert np.any(np.abs(surface_normals[..., 1][bump_mask]) > 0), ( - "ny should have slope inside bump" - ) - assert np.any(np.abs(surface_normals[..., 2][bump_mask]) != 1), ( - "nz should deviate from 1 inside bump" - ) - - assert_normals_close(surface_normals, outside_bump_mask, (0, 0, 1), atol=TOLERANCE) - - -def test_corner_of_slope( - inner_mask: MaskArray, - input_depth_map_with_bump: ScanMap2DArray, -) -> None: - """Test if the corner of the slope is an extension of x, y""" - # Arrange - corner = ( - BUMP_CENTER - BUMP_SIZE // 2, - BUMP_CENTER - BUMP_SIZE // 2, - ) - expected_corner_value = 1 / np.sqrt( - (BUMP_HEIGHT // 2) ** 2 + (BUMP_HEIGHT // 2) ** 2 + 1 - ) - - # Act - surface_normals = compute_surface_normals(input_depth_map_with_bump, 1, 1) - - # Assert - - assert_allclose( - surface_normals[corner[0], corner[1], 2], - expected_corner_value, - atol=TOLERANCE, - err_msg="corner of x and y should have unit normal of x and y", - ) diff --git a/packages/scratch-core/tests/image_generation/test_convert_image.py b/packages/scratch-core/tests/image_generation/test_convert_image.py deleted file mode 100644 index 3f990d91..00000000 --- a/packages/scratch-core/tests/image_generation/test_convert_image.py +++ /dev/null @@ -1,40 +0,0 @@ -from conversion.exceptions import ConversionError -import numpy as np -from PIL.Image import Image - -import pytest -from image_generation.data_formats import ScanImage - - -def test_create_image(scan_image_replica: ScanImage) -> None: - image = scan_image_replica.image() - - assert isinstance(image, Image) - assert image.size == scan_image_replica.data.shape - assert image.mode == "RGBA" - - -def test_create_small_image_small_values() -> None: - # Arrange - input_data = np.array([[10.0, 20.0], [30.0, 40.0]], dtype=np.float64) - input_image = ScanImage(data=input_data, scale_x=1, scale_y=1) - expected_rgba = np.stack( - ([input_data] * 3 + [np.full_like(input_data, 255)]), - axis=-1, - ).astype(np.uint8) - - # Act - output_array = np.array(input_image.image()) - - # Assert - assert np.array_equal(output_array, expected_rgba) - - -def test_create_wrong_values() -> None: - # Arrange - input_data = np.array([[-50.0, 100.0, 300.0], [150.0, 500.0, -20.0]]) - input_image = ScanImage(data=input_data, scale_x=1, scale_y=1) - - # Act - with pytest.raises(ConversionError): - input_image.image() diff --git a/packages/scratch-core/tests/image_generation/test_data_formats.py b/packages/scratch-core/tests/image_generation/test_data_formats.py deleted file mode 100644 index a44f95af..00000000 --- a/packages/scratch-core/tests/image_generation/test_data_formats.py +++ /dev/null @@ -1,29 +0,0 @@ -import numpy as np -import pytest - -from image_generation.data_formats import SurfaceNormals - - -@pytest.mark.parametrize( - "nx, ny, nz", - [ - pytest.param((100, 100), (80, 100), (100, 100), id="ny shorter width"), - pytest.param((100, 100), (100, 80), (100, 100), id="ny shorter height"), - pytest.param((80, 100), (100, 100), (100, 100), id="nx shorter width"), - pytest.param((100, 80), (100, 100), (100, 100), id="nx shorter height"), - pytest.param((100, 100), (100, 100), (80, 100), id="nz shorter width"), - pytest.param((100, 100), (100, 100), (100, 80), id="nz shorter height"), - ], -) -def test_surface_normals_invalid_shapes( - nx: tuple[int, int], ny: tuple[int, int], nz: tuple[int, int] -): - nx_array = np.zeros(nx) - ny_array = np.zeros(ny) - nz_array = np.zeros(nz) - - with pytest.raises(ValueError) as excinfo: - SurfaceNormals( - data=np.stack([nx_array, ny_array, nz_array], axis=-1), scale_x=1, scale_y=1 - ) - assert "all input arrays must have the same shape" == excinfo.value.args[0] diff --git a/packages/scratch-core/tests/image_generation/test_image_generation.py b/packages/scratch-core/tests/image_generation/test_image_generation.py deleted file mode 100644 index e2243997..00000000 --- a/packages/scratch-core/tests/image_generation/test_image_generation.py +++ /dev/null @@ -1,28 +0,0 @@ -import pytest -from matplotlib.testing.decorators import image_comparison - -from image_generation import compute_3d_image, get_array_for_display -from image_generation.data_formats import ScanImage - -from ..utils import plot_test_data -import numpy as np -from numpy.testing import assert_array_almost_equal - - -from ..constants import BASELINE_IMAGES_DIR - - -@pytest.mark.integration -@image_comparison(baseline_images=["surfaceplot_default"], extensions=["png"]) -def test_get_surface_plot(scan_image_replica: ScanImage) -> None: - generated_image = compute_3d_image(scan_image=scan_image_replica) - plot_test_data(generated_image.data) - - -@pytest.mark.integration -def test_get_image_for_display_matches_baseline_image( - scan_image_with_nans: ScanImage, -): - verified = np.load(BASELINE_IMAGES_DIR / "display_array.npy") - display_image = get_array_for_display(scan_image_with_nans) - assert_array_almost_equal(display_image.data, verified) diff --git a/packages/scratch-core/tests/image_generation/test_multiple_lights.py b/packages/scratch-core/tests/image_generation/test_multiple_lights.py deleted file mode 100644 index 15cb18e0..00000000 --- a/packages/scratch-core/tests/image_generation/test_multiple_lights.py +++ /dev/null @@ -1,55 +0,0 @@ -import numpy as np - -from image_generation.translations import apply_multiple_lights -from utils.array_definitions import ScanMap2DArray - - -IMAGE_HEIGHT = 10 -IMAGE_WIDTH = 12 - - -def _simple_calc( - light_vector, - observer_vector, - surface_normals, - specular_factor: float = 1.0, - phong_exponent: int = 1, -) -> ScanMap2DArray: - """A dumb calculator that returns a constant equal to the x-component of the light.""" - return np.sum(surface_normals * light_vector, axis=-1) - - -def test_apply_multiple_lights() -> None: - """test if for each light a layer is calculated with the given function.""" - # Arrange - light1 = np.array([1.0, 0.5, 1.0]) - light2 = np.array([0.5, 1.0, 0.9]) - light3 = np.array([0.5, 0.5, 0.5]) - lights_combined = (light1, light2, light3) - observer_vector = np.array([0.0, 1.0, 0.0]) - nx = np.zeros((IMAGE_WIDTH, IMAGE_HEIGHT)) - ny = np.zeros((IMAGE_WIDTH, IMAGE_HEIGHT)) - nz = np.ones((IMAGE_WIDTH, IMAGE_HEIGHT)) - - base_images = np.stack([nx, ny, nz], axis=-1) - expected_result_light_1 = _simple_calc(light1, observer_vector, base_images) - expected_result_light_2 = _simple_calc(light2, observer_vector, base_images) - expected_result_light_3 = _simple_calc(light3, observer_vector, base_images) - - # Act - result = apply_multiple_lights( - surface_normals=base_images, - light_vectors=lights_combined, - observer_vector=observer_vector, - lighting_calculator=_simple_calc, - ) - - # Assert - assert result.shape == ( - IMAGE_WIDTH, - IMAGE_HEIGHT, - len(lights_combined), - ) - assert np.all(result[..., 0] == expected_result_light_1) - assert np.all(result[..., 1] == expected_result_light_2) - assert np.all(result[..., 2] == expected_result_light_3) diff --git a/packages/scratch-core/tests/image_generation/test_normalize_intensity_map.py b/packages/scratch-core/tests/image_generation/test_normalize_intensity_map.py deleted file mode 100644 index fec47d50..00000000 --- a/packages/scratch-core/tests/image_generation/test_normalize_intensity_map.py +++ /dev/null @@ -1,51 +0,0 @@ -import numpy as np -import pytest - -from image_generation.translations import normalize_2d_array - -TEST_IMAGE_WIDTH = 10 -TEST_IMAGE_HEIGHT = 12 -TOLERANCE = 1e-5 - - -@pytest.mark.parametrize( - "start_value, slope", - [ - pytest.param(10, 100.0, id="test bigger numbers are reduced"), - pytest.param(-200, 10.0, id="test negative numbers are upped"), - pytest.param(100, 0.01, id="small slope is streched over the range"), - ], -) -def test_bigger_numbers(start_value: int, slope: float) -> None: - # Arrange - row = (start_value + slope * np.arange(TEST_IMAGE_WIDTH)).astype(np.float64) - image = np.tile(row, (TEST_IMAGE_HEIGHT, 1)).astype(np.float64) - max_val = 255 - min_val = 20 - # Act - normalized_image = normalize_2d_array(image, scale_max=max_val, scale_min=min_val) - - # Assert - assert normalized_image.max() <= max_val - assert normalized_image.min() >= min_val - assert normalized_image[0, 0] == normalized_image.min() - assert normalized_image[9, 9] == normalized_image.max() - - -def test_already_normalized_image() -> None: - # Arrange - max_value = 255 - min_val = 20 - image = np.linspace( - min_val, max_value, num=TEST_IMAGE_WIDTH * TEST_IMAGE_HEIGHT - ).reshape(TEST_IMAGE_WIDTH, TEST_IMAGE_HEIGHT) - - # Act - normalized = normalize_2d_array(image, scale_max=max_value, scale_min=min_val) - - # Assert - assert np.all(normalized >= min_val) - assert np.all(normalized <= max_value) - assert np.allclose(image, normalized, atol=TOLERANCE), ( - "should be the same output as the already normalized input" - ) diff --git a/packages/scratch-core/tests/image_generation/test_translations.py b/packages/scratch-core/tests/image_generation/test_translations.py deleted file mode 100644 index 11e21b20..00000000 --- a/packages/scratch-core/tests/image_generation/test_translations.py +++ /dev/null @@ -1,119 +0,0 @@ -import numpy as np -import pytest -from hypothesis import given -from hypothesis import strategies as st -from hypothesis.extra.numpy import arrays -from numpy.testing import assert_array_almost_equal - -from conversion.exceptions import NegativeStdScalerException -from image_generation.data_formats import ScanImage -from image_generation.translations import clip_data, grayscale_to_rgba -from utils.array_definitions import ScanMap2DArray - - -def test_grayscale_to_rgba_converts_nans(scan_image_with_nans: ScanImage): - rgba = grayscale_to_rgba(scan_image_with_nans.data) - assert np.array_equal(np.isnan(scan_image_with_nans.data), rgba[..., -1] == 0) - - -def test_grayscale_to_rgba_has_equal_rgb_channels(scan_image_with_nans: ScanImage): - rgba = grayscale_to_rgba(scan_image_with_nans.data) - assert np.array_equal(np.isnan(scan_image_with_nans.data), rgba[..., -1] == 0) - for channel in range(3): - assert np.array_equal( - scan_image_with_nans.data.astype(np.uint8), rgba[..., channel] - ) - - -def test_grayscale_to_rgba_has_same_size(scan_image_with_nans: ScanImage): - rgba = grayscale_to_rgba(scan_image_with_nans.data) - assert rgba.shape[1] == scan_image_with_nans.width - assert rgba.shape[0] == scan_image_with_nans.height - - -@pytest.mark.parametrize( - "given_scan_image", - [ - pytest.param( - np.array([[-50.0, 100.0, 300.0], [150.0, 500.0, -20.0]]), - id="values bigger than 255 and lower than 0", - ), - pytest.param( - np.array([[100.0, -10.0, 200.0], [50.0, 150.0, -5.0]]), - id="negative values", - ), - pytest.param( - np.array([[100.0, 200.0, 300.0], [50.0, 400.0, 150.0]]), - id="values bigger than 255", - ), - ], -) -def test_grayscale_to_rgba_invalid_values(given_scan_image: ScanMap2DArray) -> None: - # Arrange - scan_data = np.array([[-50.0, 100.0, 300.0], [150.0, 500.0, -20.0]]) - - # Act & Assert - with pytest.raises(ValueError, match="values outside \\[0:255\\] range"): - grayscale_to_rgba(scan_data) - - -@given( - std_scaler=st.floats(min_value=0, max_value=100, exclude_min=True), - data=arrays( - dtype=float, - shape=(4, 4), - elements=st.floats(allow_nan=False, min_value=0.0, max_value=100.0), - ), -) -def test_clip_data_bounds_match_expected_bounds( - data: ScanMap2DArray, std_scaler: float -): - expected_lower = np.mean(data) - np.std(data, ddof=1) * std_scaler - expected_upper = np.mean(data) + np.std(data, ddof=1) * std_scaler - - clipped, lower, upper = clip_data(data, std_scaler) - assert np.isclose(lower, expected_lower), f"Lower bound should be {expected_lower}" - assert np.isclose(upper, expected_upper), f"Upper bound should be {expected_upper}" - assert clipped.min() >= lower, f"Minimum value should be clipped to {lower}" - assert clipped.max() <= upper, f"Maximum value should be clipped to {upper}" - - -@pytest.mark.parametrize( - "data", - [ - pytest.param(np.array([0, 5, 6.7, 0.12]), id="Array contains no NaNs"), - pytest.param( - np.array([0, 5, np.nan, 6.7, 0.12]), id="Array contains single NaN value" - ), - pytest.param( - np.array([0, 5, np.nan, 6.7, 0.12, np.nan, np.nan]), - id="Array contains multiple NaN values", - ), - ], -) -def test_clip_data_ignores_nans(data: ScanMap2DArray): - std_scaler = 1.0 - expected_lower = -0.45949361789807286 - expected_upper = 6.369493617898073 - _, lower, upper = clip_data(data, std_scaler) - assert np.isclose(lower, expected_lower), f"Lower bound should be {expected_lower}" - assert np.isclose(upper, expected_upper), f"Upper bound should be {expected_upper}" - - -@given(value=st.integers(max_value=100)) -def test_no_clipping_when_input_is_constant(value: int): - data = np.full((4, 4), value) - std_scaler = 1 - clipped, lower, upper = clip_data(data, std_scaler) - - assert_array_almost_equal(clipped, data) - assert np.isclose(lower, value) - assert np.isclose(upper, value) - - -@given(std_scaler=st.floats(max_value=0)) -def test_clip_data_rejects_negative_scalers( - scan_image_with_nans: ScanImage, std_scaler: float -): - with pytest.raises(NegativeStdScalerException): - _ = clip_data(scan_image_with_nans.data, std_scaler) diff --git a/packages/scratch-core/tests/parsers/test_parsers.py b/packages/scratch-core/tests/parsers/test_parsers.py deleted file mode 100644 index 70dd123f..00000000 --- a/packages/scratch-core/tests/parsers/test_parsers.py +++ /dev/null @@ -1,68 +0,0 @@ -from pathlib import Path, PosixPath - -import numpy as np -import pytest - -from image_generation.data_formats import ScanImage -from parsers import load_scan_image, save_to_x3p -from utils.array_definitions import ScanMap2DArray - -from ..constants import PRECISION, SCANS_DIR - - -def validate_image( - parsed_image: ScanImage, expected_image_data: ScanMap2DArray, expected_scale: float -): - """Validate a parsed image.""" - assert isinstance(parsed_image, ScanImage) - assert parsed_image.data.shape == expected_image_data.shape - assert np.allclose( - parsed_image.data, - expected_image_data, - equal_nan=True, - atol=PRECISION, - ) - assert parsed_image.data.dtype == np.float64 - assert np.isclose(parsed_image.scale_x, parsed_image.scale_y, atol=PRECISION) - assert np.isclose(parsed_image.scale_x, expected_scale, atol=PRECISION) - - -@pytest.mark.parametrize( - "filename, expected_scale", - [("circle.al3d", 8.7654321e-7), ("circle.x3p", 8.7654321e-7)], -) -def test_parser_can_parse( - filename: Path, scan_image_array: ScanMap2DArray, expected_scale: float -): - file_to_test = SCANS_DIR / filename - parsed_image = load_scan_image(file_to_test) - validate_image( - parsed_image=parsed_image, - expected_image_data=scan_image_array, - expected_scale=expected_scale, - ) - - -def test_parsed_image_can_be_exported_to_x3p( - scan_image: ScanImage, tmp_path: PosixPath -): - save_to_x3p(image=scan_image, output_path=tmp_path / "export.x3p") - - files = list(tmp_path.iterdir()) - assert len(files) == 1 - assert files[0].name == "export.x3p" - - -@pytest.mark.integration -def test_al3d_can_be_converted_to_x3p(tmp_path: PosixPath): - al3d_file = SCANS_DIR / "circle.al3d" - parsed_image = load_scan_image(al3d_file) - output_file = tmp_path / "export.x3p" - save_to_x3p(image=parsed_image, output_path=output_file) - parsed_exported_image = load_scan_image(output_file) - # compare the parsed data from the exported .x3p file to the parsed data from the .al3d file - validate_image( - parsed_image=parsed_exported_image, - expected_image_data=parsed_image.data, - expected_scale=parsed_image.scale_x, - ) diff --git a/packages/scratch-core/tests/utils.py b/packages/scratch-core/tests/utils.py deleted file mode 100644 index 7d241b9a..00000000 --- a/packages/scratch-core/tests/utils.py +++ /dev/null @@ -1,13 +0,0 @@ -from matplotlib import pyplot as plt -from matplotlib.figure import Figure - -from utils.array_definitions import ScanMap2DArray - - -def plot_test_data(data: ScanMap2DArray) -> Figure: - """Plot 2D image data for debugging purposes.""" - fig, ax = plt.subplots() - ax.imshow(data, cmap="gray") - ax.axis("off") - ax.axis("equal") - return fig From d133d0f0923e0c01de514204c7048c2f4fb6cd76 Mon Sep 17 00:00:00 2001 From: "Sharlon N. Regales" Date: Thu, 11 Dec 2025 11:33:59 +0100 Subject: [PATCH 10/16] Remove unused api code --- src/preprocessors/helpers.py | 18 ---------------- src/preprocessors/models.py | 19 ----------------- tests/preprocessors/test_helpers.py | 33 ----------------------------- 3 files changed, 70 deletions(-) delete mode 100644 src/preprocessors/helpers.py delete mode 100644 src/preprocessors/models.py delete mode 100644 tests/preprocessors/test_helpers.py diff --git a/src/preprocessors/helpers.py b/src/preprocessors/helpers.py deleted file mode 100644 index ae02d4fe..00000000 --- a/src/preprocessors/helpers.py +++ /dev/null @@ -1,18 +0,0 @@ -from pathlib import Path - -from conversion.exceptions import ConversionError -from fastapi.exceptions import HTTPException -from image_generation.data_formats import ScanImage -from image_generation.exceptions import ImageGenerationError -from image_generation.image_generation import ImageGenerator -from loguru import logger - - -def export_image_pipeline(file_path: Path, image_generator: ImageGenerator, scan_image: ScanImage) -> None: - """Given an image generator and a scan image, export the image to the specified file path.""" - try: - generated_image = image_generator(scan_image) - except (ValueError, ImageGenerationError, ConversionError) as err: - logger.error(f"Image generation failed with:{file_path.stem} from error:{str(err)}") - raise HTTPException(status_code=500, detail=f"Failed to generate {file_path.stem}: {str(err)}") - generated_image.image().save(file_path) diff --git a/src/preprocessors/models.py b/src/preprocessors/models.py deleted file mode 100644 index ae3d2fc1..00000000 --- a/src/preprocessors/models.py +++ /dev/null @@ -1,19 +0,0 @@ -from pydantic import BaseModel -from pydantic.fields import Field - - -class ErrorImageGenerationModel(BaseModel): - message: str = Field( - default="failed to generate preview_image.png", - examples=[ - "Failed to export scan_image.x3p", - "Failed to export preview_image.png", - "Failed to export surface_map.png", - ], - ) - error: str = Field(default="ValueError: ....", examples=["ValueError: ...."]) - - -class ParsingError(BaseModel): - message: str = Field(default="failed to parse scan file", examples=["Failed to parse scan file"]) - error: str = Field(default="ExportError: ....", examples=["FileIsCorrupted: ...."]) diff --git a/tests/preprocessors/test_helpers.py b/tests/preprocessors/test_helpers.py deleted file mode 100644 index d6abc24f..00000000 --- a/tests/preprocessors/test_helpers.py +++ /dev/null @@ -1,33 +0,0 @@ -from pathlib import Path - -import numpy as np -import pytest -from fastapi.exceptions import HTTPException -from image_generation.data_formats import ScanImage - -from preprocessors.helpers import export_image_pipeline - - -def test_export_image_pipeline_raises_error(): - # Arrange - def faker(*args, **kwargs) -> None: - raise ValueError("Boem!") - - expected_http_code = 500 - # Act - with pytest.raises(HTTPException) as exc_info: - export_image_pipeline(Path(), faker, np.array([])) # pyright: ignore - # Assert - assert exc_info.value.status_code == expected_http_code - assert exc_info.value.detail == "Failed to generate : Boem!" - - -def test_export_image_pipeline_creates_file(tmp_path: Path): - # Arrange - def faker(*args, **kwargs) -> ScanImage: - return ScanImage(data=np.array([[1.0, 2.0]]), scale_x=1, scale_y=1) - - # Act - export_image_pipeline(tmp_path / "test.png", faker, np.array([])) # pyright: ignore - # Assert - assert (tmp_path / "test.png").exists() From 8a6e3cf6f87aeb31e4e81979c0b29c9870edf177 Mon Sep 17 00:00:00 2001 From: "Sharlon N. Regales" Date: Thu, 11 Dec 2025 12:44:38 +0100 Subject: [PATCH 11/16] Make deptry happy deptry notify that dependencies needed maintainance --- packages/scratch-core/pyproject.toml | 6 +- uv.lock | 95 +++------------------------- 2 files changed, 12 insertions(+), 89 deletions(-) diff --git a/packages/scratch-core/pyproject.toml b/packages/scratch-core/pyproject.toml index d4e6a564..c3a3cf6f 100644 --- a/packages/scratch-core/pyproject.toml +++ b/packages/scratch-core/pyproject.toml @@ -10,11 +10,13 @@ authors = [ readme = "README.md" dependencies = [ + "loguru>=0.7.3", "numpy>=2.3.4", "pillow>=12.0.0", - "scikit-image>=0.25.2", + "pydantic>=2.12.4", + "returns>=0.26.0", + "scipy>=1.16.3", "surfalize~=0.16.6", - "numpydantic>=1.7.0", "x3p @ git+https://github.com/giacomomarchioro/pyx3p.git#81c0f764cf321e56dc41e9e3c71d14e97d5bc3ae", ] diff --git a/uv.lock b/uv.lock index a2a0f927..a9f7f069 100644 --- a/uv.lock +++ b/uv.lock @@ -694,19 +694,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] -[[package]] -name = "imageio" -version = "2.37.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, - { name = "pillow" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a3/6f/606be632e37bf8d05b253e8626c2291d74c691ddc7bcdf7d6aaf33b32f6a/imageio-2.37.2.tar.gz", hash = "sha256:0212ef2727ac9caa5ca4b2c75ae89454312f440a756fcfc8ef1993e718f50f8a", size = 389600, upload-time = "2025-11-04T14:29:39.898Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/fe/301e0936b79bcab4cacc7548bf2853fc28dced0a578bab1f7ef53c9aa75b/imageio-2.37.2-py3-none-any.whl", hash = "sha256:ad9adfb20335d718c03de457358ed69f141021a333c40a53e57273d8a5bd0b9b", size = 317646, upload-time = "2025-11-04T14:29:37.948Z" }, -] - [[package]] name = "iniconfig" version = "2.3.0" @@ -894,18 +881,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" }, ] -[[package]] -name = "lazy-loader" -version = "0.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6f/6b/c875b30a1ba490860c93da4cabf479e03f584eba06fe5963f6f6644653d8/lazy_loader-0.4.tar.gz", hash = "sha256:47c75182589b91a4e1a85a136c074285a5ad4d9f39c63e0d7fb76391c4574cd1", size = 15431, upload-time = "2024-04-05T13:03:12.261Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/60/d497a310bde3f01cb805196ac61b7ad6dc5dcf8dce66634dc34364b20b4f/lazy_loader-0.4-py3-none-any.whl", hash = "sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc", size = 12097, upload-time = "2024-04-05T13:03:10.514Z" }, -] - [[package]] name = "loguru" version = "0.7.3" @@ -1072,15 +1047,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, ] -[[package]] -name = "networkx" -version = "3.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/fc/7b6fd4d22c8c4dc5704430140d8b3f520531d4fe7328b8f8d03f5a7950e8/networkx-3.6.tar.gz", hash = "sha256:285276002ad1f7f7da0f7b42f004bcba70d381e936559166363707fdad3d72ad", size = 2511464, upload-time = "2025-11-24T03:03:47.158Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/c7/d64168da60332c17d24c0d2f08bdf3987e8d1ae9d84b5bbd0eec2eb26a55/networkx-3.6-py3-none-any.whl", hash = "sha256:cdb395b105806062473d3be36458d8f1459a4e4b98e236a66c3a48996e07684f", size = 2063713, upload-time = "2025-11-24T03:03:45.21Z" }, -] - [[package]] name = "nodeenv" version = "1.9.1" @@ -1142,19 +1108,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459, upload-time = "2025-11-16T22:52:20.55Z" }, ] -[[package]] -name = "numpydantic" -version = "1.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, - { name = "pydantic" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/59/ba/6bf5e0a7cc3fbfbb02a7824926cad73c58df374322eb7728b378023a76dc/numpydantic-1.7.0.tar.gz", hash = "sha256:268285bee026d9dfdf23efeee13f60c3b75d47de2ffdf2e58b4f0c17a6824e3b", size = 80412, upload-time = "2025-10-11T05:23:56.654Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/21/e0/42f0ea229a8b5ada24a1eae4f5522b80ab65b4083f5cea4c7372bc894af3/numpydantic-1.7.0-py3-none-any.whl", hash = "sha256:81314ed00423efa954a711a48003dba5382156e899f677f405ce043f5296b090", size = 86734, upload-time = "2025-10-11T05:23:55.003Z" }, -] - [[package]] name = "openpyxl" version = "3.1.5" @@ -1884,30 +1837,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8b/c7/6c818dcac06844608244855753a0afb367365661b40fe6b3288bc26726a6/rust_just-1.43.1-py3-none-win_amd64.whl", hash = "sha256:28f0d898d3e04846348277a4d47231c949398102c2f6f452f52ba6506c9c7572", size = 1731800, upload-time = "2025-11-19T07:49:17.344Z" }, ] -[[package]] -name = "scikit-image" -version = "0.25.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "imageio" }, - { name = "lazy-loader" }, - { name = "networkx" }, - { name = "numpy" }, - { name = "packaging" }, - { name = "pillow" }, - { name = "scipy" }, - { name = "tifffile" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c7/a8/3c0f256012b93dd2cb6fda9245e9f4bff7dc0486880b248005f15ea2255e/scikit_image-0.25.2.tar.gz", hash = "sha256:e5a37e6cd4d0c018a7a55b9d601357e3382826d3888c10d0213fc63bff977dde", size = 22693594, upload-time = "2025-02-18T18:05:24.538Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/7c/9814dd1c637f7a0e44342985a76f95a55dd04be60154247679fd96c7169f/scikit_image-0.25.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7efa888130f6c548ec0439b1a7ed7295bc10105458a421e9bf739b457730b6da", size = 13921841, upload-time = "2025-02-18T18:05:03.963Z" }, - { url = "https://files.pythonhosted.org/packages/84/06/66a2e7661d6f526740c309e9717d3bd07b473661d5cdddef4dd978edab25/scikit_image-0.25.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dd8011efe69c3641920614d550f5505f83658fe33581e49bed86feab43a180fc", size = 13196862, upload-time = "2025-02-18T18:05:06.986Z" }, - { url = "https://files.pythonhosted.org/packages/4e/63/3368902ed79305f74c2ca8c297dfeb4307269cbe6402412668e322837143/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28182a9d3e2ce3c2e251383bdda68f8d88d9fff1a3ebe1eb61206595c9773341", size = 14117785, upload-time = "2025-02-18T18:05:10.69Z" }, - { url = "https://files.pythonhosted.org/packages/cd/9b/c3da56a145f52cd61a68b8465d6a29d9503bc45bc993bb45e84371c97d94/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8abd3c805ce6944b941cfed0406d88faeb19bab3ed3d4b50187af55cf24d147", size = 14977119, upload-time = "2025-02-18T18:05:13.871Z" }, - { url = "https://files.pythonhosted.org/packages/8a/97/5fcf332e1753831abb99a2525180d3fb0d70918d461ebda9873f66dcc12f/scikit_image-0.25.2-cp313-cp313-win_amd64.whl", hash = "sha256:64785a8acefee460ec49a354706db0b09d1f325674107d7fa3eadb663fb56d6f", size = 12885116, upload-time = "2025-02-18T18:05:17.844Z" }, - { url = "https://files.pythonhosted.org/packages/10/cc/75e9f17e3670b5ed93c32456fda823333c6279b144cd93e2c03aa06aa472/scikit_image-0.25.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:330d061bd107d12f8d68f1d611ae27b3b813b8cdb0300a71d07b1379178dd4cd", size = 13862801, upload-time = "2025-02-18T18:05:20.783Z" }, -] - [[package]] name = "scikit-learn" version = "1.7.2" @@ -2050,20 +1979,24 @@ name = "scratch-core" version = "0.0.1" source = { editable = "packages/scratch-core" } dependencies = [ + { name = "loguru" }, { name = "numpy" }, - { name = "numpydantic" }, { name = "pillow" }, - { name = "scikit-image" }, + { name = "pydantic" }, + { name = "returns" }, + { name = "scipy" }, { name = "surfalize" }, { name = "x3p" }, ] [package.metadata] requires-dist = [ + { name = "loguru", specifier = ">=0.7.3" }, { name = "numpy", specifier = ">=2.3.4" }, - { name = "numpydantic", specifier = ">=1.7.0" }, { name = "pillow", specifier = ">=12.0.0" }, - { name = "scikit-image", specifier = ">=0.25.2" }, + { name = "pydantic", specifier = ">=2.12.4" }, + { name = "returns", specifier = ">=0.26.0" }, + { name = "scipy", specifier = ">=1.16.3" }, { name = "surfalize", specifier = "~=0.16.6" }, { name = "x3p", git = "https://github.com/giacomomarchioro/pyx3p.git" }, ] @@ -2181,18 +2114,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, ] -[[package]] -name = "tifffile" -version = "2025.10.16" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2d/b5/0d8f3d395f07d25ec4cafcdfc8cab234b2cc6bf2465e9d7660633983fe8f/tifffile-2025.10.16.tar.gz", hash = "sha256:425179ec7837ac0e07bc95d2ea5bea9b179ce854967c12ba07fc3f093e58efc1", size = 371848, upload-time = "2025-10-16T22:56:09.043Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/5e/56c751afab61336cf0e7aa671b134255a30f15f59cd9e04f59c598a37ff5/tifffile-2025.10.16-py3-none-any.whl", hash = "sha256:41463d979c1c262b0a5cdef2a7f95f0388a072ad82d899458b154a48609d759c", size = 231162, upload-time = "2025-10-16T22:56:07.214Z" }, -] - [[package]] name = "tornado" version = "6.5.2" From b6705ffcc47240ef1ee6bd320be15858fdd7025c Mon Sep 17 00:00:00 2001 From: "Sharlon N. Regales" Date: Fri, 12 Dec 2025 12:37:14 +0100 Subject: [PATCH 12/16] remove surface normals --- .../src/container_models/scan_image.py | 2 +- .../src/container_models/surface_normals.py | 31 ---- packages/scratch-core/src/parsers/__init__.py | 3 +- packages/scratch-core/src/parsers/x3p.py | 13 +- packages/scratch-core/src/renders/image_io.py | 4 +- .../src/renders/normalizations.py | 112 ++++++------ packages/scratch-core/src/renders/shading.py | 166 ++++++++++-------- packages/scratch-core/src/utils/logger.py | 2 +- .../container_models/test_surface_normals.py | 31 ---- src/pipelines.py | 51 +++--- src/preprocessors/pipelines.py | 1 + 11 files changed, 188 insertions(+), 228 deletions(-) delete mode 100644 packages/scratch-core/src/container_models/surface_normals.py delete mode 100644 packages/scratch-core/tests/container_models/test_surface_normals.py diff --git a/packages/scratch-core/src/container_models/scan_image.py b/packages/scratch-core/src/container_models/scan_image.py index 5dfbea4f..8517a9b0 100644 --- a/packages/scratch-core/src/container_models/scan_image.py +++ b/packages/scratch-core/src/container_models/scan_image.py @@ -13,7 +13,7 @@ class ScanImage(ConfigBaseModel): data: ScanMap2DArray scale_x: float = Field(..., gt=0.0, description="pixel size in meters (m)") scale_y: float = Field(..., gt=0.0, description="pixel size in meters (m)") - meta_data: dict | None = None + meta_data: dict = Field(default_factory=dict) @property def width(self) -> int: diff --git a/packages/scratch-core/src/container_models/surface_normals.py b/packages/scratch-core/src/container_models/surface_normals.py deleted file mode 100644 index b6481c1b..00000000 --- a/packages/scratch-core/src/container_models/surface_normals.py +++ /dev/null @@ -1,31 +0,0 @@ -from typing import Self -from pydantic import model_validator -from .base import ScanVectorField2DArray, ConfigBaseModel - - -class SurfaceNormals(ConfigBaseModel): - """ - Normal vectors per pixel in a 3-layer field. - - Represents a surface-normal map with components (nx, ny, nz) stored in the - last dimension. Shape: (height, width, 3). - """ - - x_normal_vector: ScanVectorField2DArray - y_normal_vector: ScanVectorField2DArray - z_normal_vector: ScanVectorField2DArray - - @model_validator(mode="after") - def validate_same_shape(self) -> Self: - """Validate that all normal vector components have the same shape.""" - x_shape = self.x_normal_vector.shape - y_shape = self.y_normal_vector.shape - z_shape = self.z_normal_vector.shape - - if not (x_shape == y_shape == z_shape): - raise ValueError( - f"All normal vector components must have the same shape. " - f"Got x: {x_shape}, y: {y_shape}, z: {z_shape}" - ) - - return self diff --git a/packages/scratch-core/src/parsers/__init__.py b/packages/scratch-core/src/parsers/__init__.py index 9372b322..77dc8cff 100644 --- a/packages/scratch-core/src/parsers/__init__.py +++ b/packages/scratch-core/src/parsers/__init__.py @@ -15,11 +15,12 @@ **Input Formats** (via load_scan_image): - AL3D: Alicona 3D surface files (with custom patch) - X3P: ISO 25178-72 XML format for surface texture data -- Automatic unit conversion to meters (SI base unit) + - Optional subsampling via step_size parameters **Output Formats**: - X3P: ISO 25178-72 XML format for surface texture data + - Unit converted to meters (SI base unit) - Configurable metadata via X3PMetaData Railway Integration diff --git a/packages/scratch-core/src/parsers/x3p.py b/packages/scratch-core/src/parsers/x3p.py index 695746af..281e7481 100644 --- a/packages/scratch-core/src/parsers/x3p.py +++ b/packages/scratch-core/src/parsers/x3p.py @@ -91,14 +91,13 @@ def parse_to_x3p(image: ScanImage) -> X3Pfile: ) @impure_safe def save_x3p(x3p: X3Pfile, output_path: Path) -> Path: - """Save an X3Pfile to disk. - - Args: - x3p: The X3Pfile to save - output_path: Where to save the file + """ + Save an X3P file to disk. - Returns: - IOResult[Path, Exception]: IOSuccess(Path) on success, IOFailure(Exception) on error + :param x3p: The X3P file to save. + :param output_path: The path where the file should be written. + :returns: An ``IOResult[Path, Exception]`` — ``IOSuccess(Path)`` on success, + or ``IOFailure(Exception)`` if an error occurs. """ x3p.write(str(output_path)) diff --git a/packages/scratch-core/src/renders/image_io.py b/packages/scratch-core/src/renders/image_io.py index 5944a052..b5811e2a 100644 --- a/packages/scratch-core/src/renders/image_io.py +++ b/packages/scratch-core/src/renders/image_io.py @@ -58,7 +58,7 @@ def _clip_data( return np.clip(data, lower, upper), lower, upper -@log_railway_function("Failed to retreive array for display") +@log_railway_function("Failed to retrieve array for display") @safe def get_scan_image_for_display( scan_image: ScanImage, *, std_scaler: float = 2.0 @@ -69,7 +69,7 @@ def get_scan_image_for_display( First the data will be clipped so that the values lie in the interval [μ - σ * S, μ + σ * S]. Then the values are min-max normalized and scaled to the [0, 255] interval. - :param image: An instance of `ScanImage`. + :param scan_image: An instance of `ScanImage`. :param std_scaler: The multiplier `S` for the standard deviation used above when clipping the image. :returns: An array containing the clipped and normalized image data. """ diff --git a/packages/scratch-core/src/renders/normalizations.py b/packages/scratch-core/src/renders/normalizations.py index 3472aa3f..fe71c5bc 100644 --- a/packages/scratch-core/src/renders/normalizations.py +++ b/packages/scratch-core/src/renders/normalizations.py @@ -1,31 +1,14 @@ -from functools import partial import numpy as np -from typing import Final, NamedTuple +from typing import Final from numpy.typing import NDArray from returns.pipeline import flow from returns.result import safe from container_models.scan_image import ScanImage -from container_models.surface_normals import SurfaceNormals from utils.logger import log_railway_function -class GradientComponents(NamedTuple): - """Container for gradient components with optional magnitude.""" - - x: NDArray - y: NDArray - magnitude: NDArray | None = None - - -class PhysicalSpacing(NamedTuple): - """Physical spacing between samples in x and y directions.""" - - x: float - y: float - - # Padding configurations for gradient arrays to maintain original dimensions _PAD_X_GRADIENT: Final[tuple[tuple[int, int], ...]] = ( (0, 0), @@ -37,11 +20,19 @@ class PhysicalSpacing(NamedTuple): ) # Pad top and bottom (rows) -def _compute_central_diff_scales( - spacing: PhysicalSpacing, -) -> PhysicalSpacing: +def _compute_central_diff_scales(scan_image: ScanImage) -> ScanImage: """Compute scaling factors for central difference approximation: 1/(2*spacing).""" - return PhysicalSpacing(*(1 / (2 * value) for value in spacing)) + + def compute(value: float) -> float: + return 1 / (2 * value) + + return scan_image.model_copy( + update={ + "scale_x": compute(scan_image.scale_x), + "scale_y": compute(scan_image.scale_y), + }, + deep=True, + ) def _pad_gradient( @@ -51,37 +42,58 @@ def _pad_gradient( return np.pad(unpadded_gradient, pad_width, mode="constant", constant_values=np.nan) -def _compute_depth_gradients( - scales: PhysicalSpacing, depth_data: NDArray -) -> GradientComponents: +def _compute_depth_gradients(scan_image: ScanImage) -> ScanImage: """Compute depth gradients (∂z/∂x, ∂z/∂y) using central differences.""" - return GradientComponents( - x=_pad_gradient( - (depth_data[:, :-2] - depth_data[:, 2:]) * scales.x, - _PAD_X_GRADIENT, - ), - y=_pad_gradient( - (depth_data[:-2, :] - depth_data[2:, :]) * scales.y, - _PAD_Y_GRADIENT, - ), + gradient_x = _pad_gradient( + (scan_image.data[:, :-2] - scan_image.data[:, 2:]) * scan_image.scale_x, + _PAD_X_GRADIENT, + ) + gradient_y = _pad_gradient( + (scan_image.data[:-2, :] - scan_image.data[2:, :]) * scan_image.scale_y, + _PAD_Y_GRADIENT, + ) + return scan_image.model_copy( + update={ + "meta_data": { + **(scan_image.meta_data or {}), + "gradient_x": gradient_x, + "gradient_y": gradient_y, + } + } ) -def _add_normal_magnitude(gradients: GradientComponents) -> GradientComponents: +def _add_normal_magnitude(scan_image: ScanImage) -> ScanImage: """Compute and attach the normal vector magnitude to gradient components.""" - magnitude = np.sqrt(gradients.x**2 + gradients.y**2 + 1) - return GradientComponents(gradients.x, gradients.y, magnitude) + meta = scan_image.meta_data + magnitude = np.sqrt(meta["gradient_x"] ** 2 + meta["gradient_y"] ** 2 + 1) + return scan_image.model_copy( + update={ + "meta_data": { + **meta, + "magnitude": magnitude, + } + } + ) -def _normalize_to_surface_normals(gradients: GradientComponents) -> SurfaceNormals: +def _normalize_to_surface_normals(scan_image: ScanImage) -> ScanImage: """Normalize gradient components to unit surface normal vectors.""" - x, y, magnitude = gradients - if magnitude is None: - raise ValueError - return SurfaceNormals( - x_normal_vector=x / magnitude, - y_normal_vector=-y / magnitude, - z_normal_vector=1 / magnitude, + meta = scan_image.meta_data or {} + gradient_x = meta.pop("gradient_x") + gradient_y = meta.pop("gradient_y") + magnitude = meta.pop("magnitude") + return scan_image.model_copy( + update=dict( + data=np.stack( + [ + gradient_x / magnitude, + -gradient_y / magnitude, + 1 / magnitude, + ], + axis=-1, + ) + ) ) @@ -90,7 +102,7 @@ def _normalize_to_surface_normals(gradients: GradientComponents) -> SurfaceNorma success_message="Successfully computed surface normal components", ) @safe -def compute_surface_normals(scan_image: ScanImage) -> SurfaceNormals: +def compute_surface_normals(scan_image: ScanImage) -> ScanImage: """ Compute per-pixel surface normals from a 2D depth map. @@ -98,18 +110,16 @@ def compute_surface_normals(scan_image: ScanImage) -> SurfaceNormals: and the resulting normal vectors are normalized per pixel. The border are padded with NaN values to keep the same size as the input data. - :param depth_data: 2D array of depth values with shape (Height, Width). - :param x_dimension: Physical spacing between columns (Δx) in meters. - :param y_dimension: Physical spacing between rows (Δy) in meters. + :param scan_image: A ScanImage where the mutation/ calculation is being made on. :returns: 3D array of surface normals with shape (Height, Width, 3), where the last dimension corresponds to (nx, ny, nz). """ return flow( - PhysicalSpacing(scan_image.scale_x, scan_image.scale_y), + scan_image, _compute_central_diff_scales, - partial(_compute_depth_gradients, depth_data=scan_image.data), + _compute_depth_gradients, _add_normal_magnitude, _normalize_to_surface_normals, ) diff --git a/packages/scratch-core/src/renders/shading.py b/packages/scratch-core/src/renders/shading.py index cd177f03..0034b761 100644 --- a/packages/scratch-core/src/renders/shading.py +++ b/packages/scratch-core/src/renders/shading.py @@ -1,14 +1,13 @@ from collections.abc import Iterable import numpy as np -from typing import Final, NamedTuple +from typing import Final +from numpy.typing import ArrayLike, NDArray from returns.pipeline import flow from returns.result import safe from container_models.light_source import LightSource from container_models.scan_image import ScanImage -from container_models.surface_normals import SurfaceNormals -from container_models.base import UnitVector3DArray, ScanMap2DArray from utils.logger import log_railway_function @@ -16,97 +15,116 @@ PHONG_EXPONENT: Final[int] = 4 -class LightingComponents(NamedTuple): - """Container for lighting calculation components.""" +def _get_normals(data: NDArray) -> tuple[ArrayLike, ArrayLike, ArrayLike]: + return (data[..., 0], data[..., 1], data[..., 2]) - light_vector: LightSource - observer_vector: LightSource - surface_normals: SurfaceNormals - half_vector: UnitVector3DArray | None = None - diffuse: ScanMap2DArray | None = None - specular: ScanMap2DArray | None = None - -def _compute_half_vector(components: LightingComponents) -> LightingComponents: +def _compute_half_vector(scan_image: ScanImage) -> ScanImage: """Compute and normalize the half-vector between light and observer directions.""" - h_vec = components.light_vector.unit_vector + components.observer_vector.unit_vector - return components._replace(half_vector=h_vec / np.linalg.norm(h_vec)) + meta = scan_image.meta_data + h_vec = meta["light_vector"] + meta.pop("observer_vector") + return scan_image.model_copy( + update={ + "meta_data": scan_image.meta_data + | { + "half_vector": h_vec / np.linalg.norm(h_vec), + } + }, + ) -def _compute_diffuse_lighting(components: LightingComponents) -> LightingComponents: +def _compute_diffuse_lighting(scan_image: ScanImage) -> ScanImage: """Compute Lambertian diffuse reflection: max(N · L, 0).""" - x_light, y_light, z_light = components.light_vector.unit_vector - return components._replace( - diffuse=np.maximum( - x_light * components.surface_normals.x_normal_vector - + y_light * components.surface_normals.y_normal_vector - + z_light * components.surface_normals.z_normal_vector, - 0, - ) + meta = scan_image.meta_data + x_light, y_light, z_light = meta.pop("light_vector") + x_normal, y_normal, z_normal = _get_normals(scan_image.data) + diffuse = np.maximum( + x_light * x_normal + y_light * y_normal + z_light * z_normal, + 0, + ) + return scan_image.model_copy( + update={ + "meta_data": meta + | { + "diffuse": diffuse, + } + }, ) -def _compute_specular_lighting(components: LightingComponents) -> LightingComponents: +def _compute_specular_lighting(scan_image: ScanImage) -> ScanImage: """ Compute Phong specular reflection: max(cos(2*arccos(max(N · H, 0))), 0)^n. Uses the half-vector H between light and observer directions. """ - - if components.half_vector is None: - raise AttributeError - - x_half_vector, y_half_vector, z_half_vector = components.half_vector + meta = scan_image.meta_data + x_half_vector, y_half_vector, z_half_vector = meta.pop("half_vector") + x_normal, y_normal, z_normal = _get_normals(scan_image.data) specular = np.maximum( - x_half_vector * components.surface_normals.x_normal_vector - + y_half_vector * components.surface_normals.y_normal_vector - + z_half_vector * components.surface_normals.z_normal_vector, + x_half_vector * x_normal + y_half_vector * y_normal + z_half_vector * z_normal, 0, ) specular = np.clip(specular, -1.0, 1.0) specular = np.maximum(np.cos(2 * np.arccos(specular)), 0) ** PHONG_EXPONENT - return components._replace(specular=specular) + return scan_image.model_copy( + update={ + "meta_data": meta + | { + "specular": specular, + } + }, + ) -def _combine_lighting_components( - components: LightingComponents, -) -> ScanMap2DArray: +def _combine_lighting_components(scan_image: ScanImage) -> ScanImage: """Combine diffuse and specular components with weighting factor.""" - - if components.diffuse is None or components.specular is None: - raise AttributeError - - return (components.diffuse + SPECULAR_FACTOR * components.specular) / ( + meta = scan_image.meta_data + combined = (meta.pop("diffuse") + SPECULAR_FACTOR * meta.pop("specular")) / ( 1 + SPECULAR_FACTOR ) + return scan_image.model_copy( + update=meta + | { + "data": combined, + }, + ) + @log_railway_function("Calculating 2d maps per lighting source failed.") def calculate_lighting( - light_vector: LightSource, - observer_vector: LightSource, - surface_normals: SurfaceNormals, -) -> ScanMap2DArray: + light: LightSource, + observer: LightSource, + scan_image: ScanImage, +) -> ScanImage: """ Compute per-pixel lighting intensity from a light source and surface normals. Lighting is computed using Lambertian diffuse reflection combined with a Phong specular component. - :param light_vector: Normalized 3-element vector pointing toward the light source. - :param observer_vector: Normalized 3-element vector pointing toward the observer/camera. - :param surface_normals: 3D array of surface normals with shape (Height, Width, 3). - :param specular_factor: Weight of the specular component. Default is ``1.0``. - :param phong_exponent: Exponent controlling the sharpness of specular highlights. - Default is ``4``. + :param light: LightSource as a Normalized 3-element vector pointing toward the light source. + :param observer: LightSource as Normalized 3-element vector pointing toward the observer/camera. + :param scan_image: ScanImage with 3D surface normals in data field (Height, Width, 3). - :returns: 2D array of combined lighting intensities in ``[0, 1]`` with shape - (Height, Width). + :returns: ScanImage with 2D array of combined lighting intensities in ``[0, 1]`` + with shape (Height, Width). """ + scan_image = scan_image.model_copy( + update={ + "meta_data": scan_image.meta_data + | { + "light_vector": light.unit_vector, + "observer_vector": observer.unit_vector, + } + } + ) + return flow( - LightingComponents(light_vector, observer_vector, surface_normals), + scan_image, _compute_half_vector, _compute_diffuse_lighting, _compute_specular_lighting, @@ -117,7 +135,7 @@ def calculate_lighting( @log_railway_function("Failed to apply lights") @safe def apply_multiple_lights( - surface_normals: SurfaceNormals, + scan_image: ScanImage, light_sources: Iterable[LightSource], observer: LightSource, scale_x: float, @@ -127,27 +145,27 @@ def apply_multiple_lights( Apply multiple directional light sources to a surface and combine them into a single intensity map. - :param surface_normals: 3D array of surface normals with shape (Height, Width, 3). - :param light_vectors: Tuple of normalized 3-element light direction vectors. - :param observer_vector: Normalized 3-element vector pointing toward the observer. - :param lighting_calculator: Function used to compute lighting for a single light - source. Default is :func:`calculate_lighting`. + :param scan_image: ScanImage with 3D surface normals in data field (Height, Width, 3). + :param light_sources: Iterable of LightSource objects representing directional lights. + :param observer: LightSource representing the observer/camera position. + :param scale_x: Physical spacing in x direction (meters). + :param scale_y: Physical spacing in y direction (meters). :returns: ScanImage with 2D array of combined lighting intensities with shape (Height, Width), where contributions from all lights are summed together. """ - - return ScanImage( - data=np.nansum( - np.stack( - [ - calculate_lighting(light, observer, surface_normals) - for light in light_sources - ], - axis=-1, + lighting_results = [ + calculate_lighting(light, observer, scan_image) for light in light_sources + ] + + return scan_image.model_copy( + update={ + "data": np.nansum( + np.stack([result.data for result in lighting_results], axis=-1), + axis=2, ), - axis=2, - ), - scale_x=scale_x, - scale_y=scale_y, + "scale_x": scale_x, + "scale_y": scale_y, + }, + deep=True, ) diff --git a/packages/scratch-core/src/utils/logger.py b/packages/scratch-core/src/utils/logger.py index 06e9a30a..c7e7e5ab 100644 --- a/packages/scratch-core/src/utils/logger.py +++ b/packages/scratch-core/src/utils/logger.py @@ -12,7 +12,7 @@ def _debug_function_signature(func: Callable[..., Any], *args, **kwargs): - """Print the function signature and return value""" + """Print the function signature and return value.""" signature = ", ".join( chain( (repr(arg) for arg in args), diff --git a/packages/scratch-core/tests/container_models/test_surface_normals.py b/packages/scratch-core/tests/container_models/test_surface_normals.py deleted file mode 100644 index 0eb87e9d..00000000 --- a/packages/scratch-core/tests/container_models/test_surface_normals.py +++ /dev/null @@ -1,31 +0,0 @@ -import numpy as np -from pydantic import ValidationError -import pytest - -from container_models.surface_normals import SurfaceNormals - - -@pytest.mark.parametrize( - "nx, ny, nz", - [ - pytest.param((100, 100), (80, 100), (100, 100), id="ny shorter width"), - pytest.param((100, 100), (100, 80), (100, 100), id="ny shorter height"), - pytest.param((80, 100), (100, 100), (100, 100), id="nx shorter width"), - pytest.param((100, 80), (100, 100), (100, 100), id="nx shorter height"), - pytest.param((100, 100), (100, 100), (80, 100), id="nz shorter width"), - pytest.param((100, 100), (100, 100), (100, 80), id="nz shorter height"), - ], -) -def test_surface_normals_invalid_shapes( - nx: tuple[int, int], ny: tuple[int, int], nz: tuple[int, int] -): - # act and assert - with pytest.raises( - ValidationError, - match=r"All normal vector components must have the same shape", - ): - SurfaceNormals( - x_normal_vector=np.zeros(nx), - y_normal_vector=np.zeros(ny), - z_normal_vector=np.zeros(nz), - ) diff --git a/src/pipelines.py b/src/pipelines.py index 51ec6362..2468757e 100644 --- a/src/pipelines.py +++ b/src/pipelines.py @@ -58,35 +58,28 @@ def run_pipeline(entry_value: Any | ContainerN, *tasks: Callable[[Any], Any], er """ Execute a series of tasks in a functional pipeline and return the final result. - This function orchestrates a railway-oriented programming pipeline using the returns - library. It takes an entry value and a sequence of tasks, executes them in order with - monadic binding, and unwraps the final result. - - Parameters - ---------- - entry_value : Any | ContainerN - The initial value to pass into the pipeline. Can be a raw value or - a Container from the returns library (IOResultE, ResultE, etc.). - *tasks : Callable[[Any], Any] - Variable number of callable tasks to execute in sequence. Each task should - accept the output of the previous task and return a Container or compatible type. - error_message : str - Custom error message to include in the HTTPException if the pipeline - fails at any step. - - Returns - ------- - Any - The unwrapped success value of type T from the final pipeline result. - - Raises - ------ - HTTPException - With status 500 (INTERNAL_SERVER_ERROR) if any task in the pipeline - fails or returns a failure Container. The exception detail will contain the - provided error_message. - - Examples + This function orchestrates a railway-oriented programming pipeline using the + ``returns`` library. It takes an entry value and a sequence of tasks, executes + them in order using monadic binding, and unwraps the final result. + + :param entry_value: The initial value to pass into the pipeline. This may be a + raw value or a Container from the ``returns`` library (e.g. ``IOResultE``, + ``ResultE``). + :param tasks: Variable number of callable tasks executed sequentially. Each + task must accept the output of the previous task and return a Container or + a compatible type. + :param error_message: Custom error message included in the ``HTTPException`` if + the pipeline fails at any step. + :type error_message: str + + :returns: The unwrapped success value of the final pipeline result. + :rtype: Any + + :raises HTTPException: Raised with status code 500 (INTERNAL_SERVER_ERROR) if + any task in the pipeline fails or returns a failure Container. The exception + detail contains the provided ``error_message``. + + :examples -------- >>> def validate_user(data: dict) -> ResultE[dict]: ... return Success(data) if data.get("id") else Failure("Invalid user") diff --git a/src/preprocessors/pipelines.py b/src/preprocessors/pipelines.py index 1a47adc6..4f351967 100644 --- a/src/preprocessors/pipelines.py +++ b/src/preprocessors/pipelines.py @@ -21,6 +21,7 @@ def parse_scan_pipeline(scan_file: Path, parameters: UploadScanParameters) -> Sc Parse a scan file and load it as a ScanImage. :param scan_file: The path to the scan file to parse. + :param parameters: all parameters used in the pipeline. :returns: The parsed scan image data. :raises HTTPException: If the file cannot be parsed or read. """ From 5d665a1f97ab440c53a0f62f7a810a55c876275e Mon Sep 17 00:00:00 2001 From: Simone Date: Wed, 17 Dec 2025 10:37:51 +0100 Subject: [PATCH 13/16] mask and crop functionality added (#67) * mask and crop functionality added * type fixes and some efficiency --- packages/scratch-core/src/conversion/mask.py | 2 +- packages/scratch-core/tests/conftest.py | 12 ++++++------ packages/scratch-core/tests/conversion/test_mask.py | 10 +++++----- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/scratch-core/src/conversion/mask.py b/packages/scratch-core/src/conversion/mask.py index 4cfd1377..a37de9c4 100644 --- a/packages/scratch-core/src/conversion/mask.py +++ b/packages/scratch-core/src/conversion/mask.py @@ -1,6 +1,6 @@ import numpy as np -from utils.array_definitions import ScanMap2DArray, MaskArray +from container_models.base import ScanMap2DArray, MaskArray def mask_2d_array( diff --git a/packages/scratch-core/tests/conftest.py b/packages/scratch-core/tests/conftest.py index d1a151ca..4fed8800 100644 --- a/packages/scratch-core/tests/conftest.py +++ b/packages/scratch-core/tests/conftest.py @@ -1,16 +1,16 @@ -from pathlib import Path import logging +from pathlib import Path import numpy as np import pytest -from PIL import Image from loguru import logger +from PIL import Image -from image_generation.data_formats import ScanImage -from parsers.data_types import load_scan_image -from utils.array_definitions import ScanMap2DArray, MaskArray -from .helper_function import unwrap_result +from container_models.base import MaskArray, ScanMap2DArray +from container_models.scan_image import ScanImage +from parsers import load_scan_image +from .helper_function import unwrap_result TEST_ROOT = Path(__file__).parent diff --git a/packages/scratch-core/tests/conversion/test_mask.py b/packages/scratch-core/tests/conversion/test_mask.py index 30df9f39..eb6923e7 100644 --- a/packages/scratch-core/tests/conversion/test_mask.py +++ b/packages/scratch-core/tests/conversion/test_mask.py @@ -1,3 +1,4 @@ +from pathlib import Path import numpy as np import pytest from numpy.testing import assert_array_almost_equal @@ -8,9 +9,8 @@ _determine_bounding_box, mask_and_crop_2d_array, ) -from image_generation.data_formats import ScanImage -from utils.array_definitions import MaskArray -from ..constants import BASELINE_IMAGES_DIR +from container_models.scan_image import ScanImage +from container_models.base import MaskArray class TestMask2dArray: @@ -154,9 +154,9 @@ def test_raises_on_empty_mask(self): @pytest.mark.integration def test_get_image_for_display_matches_baseline_image( - scan_image_with_nans: ScanImage, mask_array: MaskArray + scan_image_with_nans: ScanImage, mask_array: MaskArray, baseline_images_dir: Path ): - verified = np.load(BASELINE_IMAGES_DIR / "masked_cropped_array.npy") + verified = np.load(baseline_images_dir / "masked_cropped_array.npy") masked_cropped_array = mask_and_crop_2d_array( scan_image_with_nans.data, mask_array, crop=True ) From 0e8db97434385f1f901f869b83ab5cc8227325c3 Mon Sep 17 00:00:00 2001 From: Simone Date: Tue, 16 Dec 2025 08:50:05 +0100 Subject: [PATCH 14/16] Gaussian filter - filter_apply.m (#60) gaussian filter --------- Co-authored-by: Sharlon N. Regales --- packages/scratch-core/src/conversion/gaussian_filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/scratch-core/src/conversion/gaussian_filter.py b/packages/scratch-core/src/conversion/gaussian_filter.py index 09e5a936..6d97e509 100644 --- a/packages/scratch-core/src/conversion/gaussian_filter.py +++ b/packages/scratch-core/src/conversion/gaussian_filter.py @@ -5,7 +5,7 @@ from scipy import ndimage from scipy.special import lambertw -from utils.array_definitions import ScanMap2DArray +from container_models.base import ScanMap2DArray @cache From 68d65badbfbcfe705dfe09775a7762f58f25869f Mon Sep 17 00:00:00 2001 From: cfs-data <145435153+cfs-data@users.noreply.github.com> Date: Wed, 17 Dec 2025 15:03:34 +0100 Subject: [PATCH 15/16] Add map_level.m to translations (#66) --- packages/scratch-core/src/conversion/leveling/core.py | 2 +- packages/scratch-core/src/conversion/leveling/solver/grid.py | 2 +- packages/scratch-core/src/conversion/leveling/solver/utils.py | 2 +- .../scratch-core/tests/conversion/leveling/solver/test_grid.py | 2 +- .../scratch-core/tests/conversion/leveling/solver/test_utils.py | 2 +- .../scratch-core/tests/conversion/leveling/test_level_map.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/scratch-core/src/conversion/leveling/core.py b/packages/scratch-core/src/conversion/leveling/core.py index 6ebbb654..c726584a 100644 --- a/packages/scratch-core/src/conversion/leveling/core.py +++ b/packages/scratch-core/src/conversion/leveling/core.py @@ -6,7 +6,7 @@ compute_root_mean_square, ) from conversion.leveling.solver.utils import compute_image_center -from image_generation.data_formats import ScanImage +from container_models.scan_image import ScanImage def level_map( diff --git a/packages/scratch-core/src/conversion/leveling/solver/grid.py b/packages/scratch-core/src/conversion/leveling/solver/grid.py index ed251be9..dcf99ce1 100644 --- a/packages/scratch-core/src/conversion/leveling/solver/grid.py +++ b/packages/scratch-core/src/conversion/leveling/solver/grid.py @@ -1,6 +1,6 @@ import numpy as np from numpy.typing import NDArray -from image_generation.data_formats import ScanImage +from container_models.scan_image import ScanImage def get_2d_grid( diff --git a/packages/scratch-core/src/conversion/leveling/solver/utils.py b/packages/scratch-core/src/conversion/leveling/solver/utils.py index 45671618..77925927 100644 --- a/packages/scratch-core/src/conversion/leveling/solver/utils.py +++ b/packages/scratch-core/src/conversion/leveling/solver/utils.py @@ -2,7 +2,7 @@ from typing import Any from numpy.typing import NDArray -from image_generation.data_formats import ScanImage +from container_models.scan_image import ScanImage def compute_root_mean_square(data: NDArray[Any]) -> float: diff --git a/packages/scratch-core/tests/conversion/leveling/solver/test_grid.py b/packages/scratch-core/tests/conversion/leveling/solver/test_grid.py index 3b02130c..f8244716 100644 --- a/packages/scratch-core/tests/conversion/leveling/solver/test_grid.py +++ b/packages/scratch-core/tests/conversion/leveling/solver/test_grid.py @@ -1,6 +1,6 @@ from conversion.leveling.solver import get_2d_grid from conversion.leveling.solver.utils import compute_image_center -from image_generation.data_formats import ScanImage +from container_models.scan_image import ScanImage import numpy as np import pytest diff --git a/packages/scratch-core/tests/conversion/leveling/solver/test_utils.py b/packages/scratch-core/tests/conversion/leveling/solver/test_utils.py index ee6f2c78..8e515259 100644 --- a/packages/scratch-core/tests/conversion/leveling/solver/test_utils.py +++ b/packages/scratch-core/tests/conversion/leveling/solver/test_utils.py @@ -3,7 +3,7 @@ from conversion.leveling.solver import compute_root_mean_square from conversion.leveling.solver.utils import compute_image_center -from image_generation.data_formats import ScanImage +from container_models.scan_image import ScanImage class TestRootMeanSquare: diff --git a/packages/scratch-core/tests/conversion/leveling/test_level_map.py b/packages/scratch-core/tests/conversion/leveling/test_level_map.py index 778e7697..d217c55d 100644 --- a/packages/scratch-core/tests/conversion/leveling/test_level_map.py +++ b/packages/scratch-core/tests/conversion/leveling/test_level_map.py @@ -1,5 +1,5 @@ from conversion.leveling import level_map, SurfaceTerms -from image_generation.data_formats import ScanImage +from container_models.scan_image import ScanImage import pytest import numpy as np from .constants import RESOURCES_DIR From a3d07bc2c8145a08257e4d2121ccf57de0f664df Mon Sep 17 00:00:00 2001 From: Simone Date: Wed, 17 Dec 2025 16:18:18 +0100 Subject: [PATCH 16/16] Resampling method added (#71) * resampling method added --- packages/scratch-core/src/conversion/resample.py | 6 +++--- packages/scratch-core/tests/conversion/test_resample.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/scratch-core/src/conversion/resample.py b/packages/scratch-core/src/conversion/resample.py index 5aed5b21..3463296e 100644 --- a/packages/scratch-core/src/conversion/resample.py +++ b/packages/scratch-core/src/conversion/resample.py @@ -1,11 +1,11 @@ from typing import Optional import numpy as np -from numpydantic import NDArray +from numpy.typing import NDArray from scipy import ndimage -from image_generation.data_formats import ScanImage -from utils.array_definitions import MaskArray +from container_models.scan_image import ScanImage +from container_models.base import MaskArray def resample_image_and_mask( diff --git a/packages/scratch-core/tests/conversion/test_resample.py b/packages/scratch-core/tests/conversion/test_resample.py index bfd2a2ae..ab31424a 100644 --- a/packages/scratch-core/tests/conversion/test_resample.py +++ b/packages/scratch-core/tests/conversion/test_resample.py @@ -6,8 +6,8 @@ resample_image_and_mask, clip_resample_factors, ) -from image_generation.data_formats import ScanImage -from utils.array_definitions import MaskArray +from container_models.scan_image import ScanImage +from container_models.base import MaskArray class TestGetResamplingFactors: