From 2091d42738a9dcf7e2146fd1c5d23dc6e5b28010 Mon Sep 17 00:00:00 2001 From: Chain-Frost Date: Mon, 10 Nov 2025 17:45:56 +0800 Subject: [PATCH 01/11] Clarify POMM peak report workflow and metadata --- .../TUFLOW-python/POMM-mean-max-aep-dur.py | 4 +- .../TUFLOW-python/POMM-med-max-aep-dur.py | 4 +- ryan_library/classes/column_definitions.py | 13 +++- ryan_library/functions/pandas/median_calc.py | 16 ++-- ryan_library/scripts/pomm_max_items.py | 73 +++++++++++++++---- tests/functions/test_median_calc.py | 8 +- 6 files changed, 86 insertions(+), 32 deletions(-) diff --git a/ryan-scripts/TUFLOW-python/POMM-mean-max-aep-dur.py b/ryan-scripts/TUFLOW-python/POMM-mean-max-aep-dur.py index 69efaa1a..749eab16 100644 --- a/ryan-scripts/TUFLOW-python/POMM-mean-max-aep-dur.py +++ b/ryan-scripts/TUFLOW-python/POMM-mean-max-aep-dur.py @@ -3,7 +3,7 @@ from pathlib import Path import os -from ryan_library.scripts.pomm_max_items import run_mean_peak_report +from ryan_library.scripts.pomm_max_items import export_mean_peak_report from ryan_library.scripts.wrapper_utils import ( change_working_directory, print_library_version, @@ -28,7 +28,7 @@ def main() -> None: if not change_working_directory(target_dir=script_directory): return - run_mean_peak_report( + export_mean_peak_report( script_directory=script_directory, log_level=console_log_level, include_pomm=INCLUDE_POMM, diff --git a/ryan-scripts/TUFLOW-python/POMM-med-max-aep-dur.py b/ryan-scripts/TUFLOW-python/POMM-med-max-aep-dur.py index 8154bf71..3b4e300d 100644 --- a/ryan-scripts/TUFLOW-python/POMM-med-max-aep-dur.py +++ b/ryan-scripts/TUFLOW-python/POMM-med-max-aep-dur.py @@ -3,7 +3,7 @@ from pathlib import Path import os -from ryan_library.scripts.pomm_max_items import run_median_peak_report +from ryan_library.scripts.pomm_max_items import export_median_peak_report from ryan_library.scripts.wrapper_utils import ( change_working_directory, print_library_version, @@ -32,7 +32,7 @@ def main() -> None: if not change_working_directory(target_dir=script_directory): return - run_median_peak_report( + export_median_peak_report( script_directory=script_directory, log_level=console_log_level, include_pomm=INCLUDE_POMM, diff --git a/ryan_library/classes/column_definitions.py b/ryan_library/classes/column_definitions.py index 3f91b59d..34c5cede 100644 --- a/ryan_library/classes/column_definitions.py +++ b/ryan_library/classes/column_definitions.py @@ -225,17 +225,24 @@ def default(cls) -> "ColumnMetadataRegistry": ), "mean_PeakFlow": ColumnDefinition( name="mean_PeakFlow", - description="Peak flow corresponding to the mean storm for the group.", + description=( + "Peak flow from the event whose statistic is nearest to the group's arithmetic mean; " + "not an averaged peak flow." + ), value_type="float", ), "mean_Duration": ColumnDefinition( name="mean_Duration", - description="Duration associated with the mean storm for the group.", + description=( + "Duration taken from the same nearest-to-mean event used for mean_PeakFlow (not an averaged value)." + ), value_type="string", ), "mean_TP": ColumnDefinition( name="mean_TP", - description="Temporal pattern associated with the mean storm for the group.", + description=( + "Temporal pattern taken from the same nearest-to-mean event used for mean_PeakFlow." + ), value_type="string", ), "low": ColumnDefinition( diff --git a/ryan_library/functions/pandas/median_calc.py b/ryan_library/functions/pandas/median_calc.py index 9a21bae0..87b87293 100644 --- a/ryan_library/functions/pandas/median_calc.py +++ b/ryan_library/functions/pandas/median_calc.py @@ -1,13 +1,15 @@ # ryan_library\functions\pandas\median_calc.py -"""Utilities for computing median statistics for grouped data.""" +"""Utilities for summarising grouped statistics for POMM reports.""" import pandas as pd from pandas import DataFrame from typing import Any -def _median_stats_for_group(durgrp: pd.DataFrame, stat_col: str, tp_col: str, dur_col: str) -> dict[str, Any]: - """Return statistics for a single duration group.""" +def _summarise_group_statistics( + durgrp: pd.DataFrame, stat_col: str, tp_col: str, dur_col: str +) -> dict[str, Any]: + """Return median and mean-adjacent statistics for a single duration group.""" ensemblestat: DataFrame = durgrp.sort_values(stat_col, ascending=True, na_position="first") r: int = len(ensemblestat.index) @@ -17,8 +19,8 @@ def _median_stats_for_group(durgrp: pd.DataFrame, stat_col: str, tp_col: str, du mean_including_zeroes = float(stat_series.mean()) mean_excluding_zeroes = float(ensemblestat[ensemblestat[stat_col] != 0][stat_col].mean()) - mean_duration = ensemblestat[dur_col].iloc[medianpos] - mean_tp = ensemblestat[tp_col].iloc[medianpos] + mean_duration: Any = pd.NA + mean_tp: Any = pd.NA mean_peak_flow = float("nan") if stat_series.notna().any(): closest_idx = (stat_series - mean_including_zeroes).abs().idxmin() @@ -74,7 +76,7 @@ def median_stats( count_bin: int = 0 for _, durgrp in thinned_df.groupby(by=dur_col): - stats_dict: dict[str, Any] = _median_stats_for_group( + stats_dict: dict[str, Any] = _summarise_group_statistics( durgrp=durgrp, stat_col=stat_col, tp_col=tp_col, dur_col=dur_col ) @@ -98,6 +100,6 @@ def median_stats( def median_calc( thinned_df: pd.DataFrame, statcol: str, tpcol: str, durcol: str ) -> tuple[dict[str, Any], list[dict[str, Any]]]: - """Compatibility wrapper for previous function name.""" + """Compatibility wrapper retaining the legacy public function name.""" return median_stats(thinned_df=thinned_df, stat_col=statcol, tp_col=tpcol, dur_col=durcol) diff --git a/ryan_library/scripts/pomm_max_items.py b/ryan_library/scripts/pomm_max_items.py index c739c6a4..cdf55f62 100644 --- a/ryan_library/scripts/pomm_max_items.py +++ b/ryan_library/scripts/pomm_max_items.py @@ -1,6 +1,7 @@ # ryan_library/scripts/pomm_max_items.py from collections.abc import Collection, Callable +import warnings from loguru import logger from pathlib import Path from datetime import datetime @@ -16,21 +17,23 @@ def run_peak_report(script_directory: Path | None = None) -> None: - """Run the peak report generation workflow.""" + """Legacy entry point retained for backwards compatibility.""" + print() print("You are using an old wrapper") print() - run_median_peak_report() + export_median_peak_report(script_directory=script_directory) -def _run_peak_report( +def generate_peak_report( + *, script_directory: Path | None = None, log_level: str = "INFO", include_pomm: bool = True, locations_to_include: Collection[str] | None = None, - save_report: Callable[..., None] | None = None, + report_saver: Callable[..., None], ) -> None: - """Core implementation for running a peak report workflow.""" + """Run the shared peak report workflow and delegate saving to ``report_saver``.""" setup_logger(console_log_level=log_level) logger.info(f"Current Working Directory: {Path.cwd()}") @@ -55,9 +58,7 @@ def _run_peak_report( return timestamp: str = datetime.now().strftime(format="%Y%m%d-%H%M") - if save_report is None: - save_report = save_peak_report_median - save_report( + report_saver( aggregated_df=aggregated_df, script_directory=script_directory, timestamp=timestamp, @@ -65,7 +66,8 @@ def _run_peak_report( ) -def run_median_peak_report( +def export_median_peak_report( + *, script_directory: Path | None = None, log_level: str = "INFO", include_pomm: bool = True, @@ -73,16 +75,17 @@ def run_median_peak_report( ) -> None: """Locate and process POMM files and export median-based peak values.""" - _run_peak_report( + generate_peak_report( script_directory=script_directory, log_level=log_level, include_pomm=include_pomm, locations_to_include=locations_to_include, - save_report=save_peak_report_median, + report_saver=save_peak_report_median, ) -def run_mean_peak_report( +def export_mean_peak_report( + *, script_directory: Path | None = None, log_level: str = "INFO", include_pomm: bool = True, @@ -90,10 +93,52 @@ def run_mean_peak_report( ) -> None: """Locate and process POMM files and export mean-based peak values.""" - _run_peak_report( + generate_peak_report( + script_directory=script_directory, + log_level=log_level, + include_pomm=include_pomm, + locations_to_include=locations_to_include, + report_saver=save_peak_report_mean, + ) + + +def run_median_peak_report( + script_directory: Path | None = None, + log_level: str = "INFO", + include_pomm: bool = True, + locations_to_include: Collection[str] | None = None, +) -> None: + """Deprecated wrapper around :func:`export_median_peak_report`.""" + + warnings.warn( + "run_median_peak_report is deprecated; use export_median_peak_report instead.", + DeprecationWarning, + stacklevel=2, + ) + export_median_peak_report( + script_directory=script_directory, + log_level=log_level, + include_pomm=include_pomm, + locations_to_include=locations_to_include, + ) + + +def run_mean_peak_report( + script_directory: Path | None = None, + log_level: str = "INFO", + include_pomm: bool = True, + locations_to_include: Collection[str] | None = None, +) -> None: + """Deprecated wrapper around :func:`export_mean_peak_report`.""" + + warnings.warn( + "run_mean_peak_report is deprecated; use export_mean_peak_report instead.", + DeprecationWarning, + stacklevel=2, + ) + export_mean_peak_report( script_directory=script_directory, log_level=log_level, include_pomm=include_pomm, locations_to_include=locations_to_include, - save_report=save_peak_report_mean, ) diff --git a/tests/functions/test_median_calc.py b/tests/functions/test_median_calc.py index 32933692..9406b5b6 100644 --- a/tests/functions/test_median_calc.py +++ b/tests/functions/test_median_calc.py @@ -3,7 +3,7 @@ from ryan_library.functions.pandas.median_calc import ( - _median_stats_for_group, + _summarise_group_statistics, median_calc, ) @@ -20,7 +20,7 @@ def make_df(values, tps, dur) -> pd.DataFrame: def test_median_stats_for_group_odd() -> None: df: pd.DataFrame = make_df([1, 2, 3], ["A", "B", "C"], ["5", "5", "5"]) - stats: dict = _median_stats_for_group(df, "val", "tp", "dur") + stats: dict = _summarise_group_statistics(df, "val", "tp", "dur") assert stats["median"] == 2 assert stats["low"] == 1 assert stats["high"] == 3 @@ -32,7 +32,7 @@ def test_median_stats_for_group_odd() -> None: def test_median_stats_for_group_even() -> None: df: pd.DataFrame = make_df([4, 1, 3, 2], ["A", "B", "C", "D"], ["1", "1", "1", "1"]) - stats: dict = _median_stats_for_group(df, "val", "tp", "dur") + stats: dict = _summarise_group_statistics(df, "val", "tp", "dur") # sorted values [1,2,3,4], median index 2 -> value 3 assert stats["median"] == 3 assert stats["low"] == 1 @@ -45,7 +45,7 @@ def test_median_stats_for_group_even() -> None: def test_median_stats_for_group_zeros() -> None: df: pd.DataFrame = make_df([0, 0, 0], ["A", "B", "C"], ["d", "d", "d"]) - stats = _median_stats_for_group(df, "val", "tp", "dur") + stats = _summarise_group_statistics(df, "val", "tp", "dur") assert stats["median"] == 0 assert stats["low"] == 0 assert stats["high"] == 0 From b2321e8346f161efe4cdbe38a5c6f3609f7a508e Mon Sep 17 00:00:00 2001 From: Chain Frost Date: Mon, 10 Nov 2025 20:22:10 +0800 Subject: [PATCH 02/11] local changes --- ryan_library/functions/pandas/median_calc.py | 45 ++++-------- ryan_library/scripts/pomm_max_items.py | 75 ++++++++++---------- tests/functions/test_median_calc.py | 11 ++- 3 files changed, 57 insertions(+), 74 deletions(-) diff --git a/ryan_library/functions/pandas/median_calc.py b/ryan_library/functions/pandas/median_calc.py index 87b87293..eb62c1b1 100644 --- a/ryan_library/functions/pandas/median_calc.py +++ b/ryan_library/functions/pandas/median_calc.py @@ -1,14 +1,15 @@ # ryan_library\functions\pandas\median_calc.py """Utilities for summarising grouped statistics for POMM reports.""" +from __future__ import annotations + +from typing import Any + import pandas as pd from pandas import DataFrame -from typing import Any -def _summarise_group_statistics( - durgrp: pd.DataFrame, stat_col: str, tp_col: str, dur_col: str -) -> dict[str, Any]: +def summarise_duration_statistics(durgrp: pd.DataFrame, stat_col: str, tp_col: str, dur_col: str) -> dict[str, Any]: """Return median and mean-adjacent statistics for a single duration group.""" ensemblestat: DataFrame = durgrp.sort_values(stat_col, ascending=True, na_position="first") @@ -43,32 +44,10 @@ def _summarise_group_statistics( } -def median_stats( +def calculate_median_statistics( thinned_df: pd.DataFrame, stat_col: str, tp_col: str, dur_col: str ) -> tuple[dict[str, Any], list[dict[str, Any]]]: - """Return median statistics for each duration group and the maximum median. - The logic mirrors the ``stats`` function in ``TUFLOW_2023_max_med_from POMM_v9.py``. - For each duration group the DataFrame is sorted by ``statcol``. The median - value is selected, along with the associated temporal pattern. The group with - the highest median is returned separately. - - Parameters - ---------- - thinned_df: - Data for a single AEP across multiple temporal patterns and durations. - statcol: - Column containing the numeric statistic to rank by (e.g. ``"AbsMax"``). - tpcol: - Column holding the temporal pattern identifier. - durcol: - Column holding the duration identifier. - - Returns - ------- - tuple[dict[str, Any], list[dict[str, Any]]] - A tuple containing the stats for the duration with the largest median and - a list of stats for each duration group. - """ + """Return per-duration stats and the record with the largest median.""" max_stats_dict: dict[str, Any] = {} bin_stats_list: list[dict[str, Any]] = [] @@ -76,7 +55,7 @@ def median_stats( count_bin: int = 0 for _, durgrp in thinned_df.groupby(by=dur_col): - stats_dict: dict[str, Any] = _summarise_group_statistics( + stats_dict: dict[str, Any] = summarise_duration_statistics( durgrp=durgrp, stat_col=stat_col, tp_col=tp_col, dur_col=dur_col ) @@ -88,7 +67,6 @@ def median_stats( count_bin += stats_dict["count"] max_stats_dict["count_bin"] = count_bin - # override low/high with the true min/max over all groups: if not thinned_df.empty: global_low = float(thinned_df[stat_col].min()) global_high = float(thinned_df[stat_col].max()) @@ -102,4 +80,9 @@ def median_calc( ) -> tuple[dict[str, Any], list[dict[str, Any]]]: """Compatibility wrapper retaining the legacy public function name.""" - return median_stats(thinned_df=thinned_df, stat_col=statcol, tp_col=tpcol, dur_col=durcol) + return calculate_median_statistics(thinned_df=thinned_df, stat_col=statcol, tp_col=tpcol, dur_col=durcol) + + +# Backwards compatibility for older imports +median_stats = calculate_median_statistics +_summarise_group_statistics = summarise_duration_statistics diff --git a/ryan_library/scripts/pomm_max_items.py b/ryan_library/scripts/pomm_max_items.py index cdf55f62..e4e1da99 100644 --- a/ryan_library/scripts/pomm_max_items.py +++ b/ryan_library/scripts/pomm_max_items.py @@ -1,19 +1,20 @@ # ryan_library/scripts/pomm_max_items.py -from collections.abc import Collection, Callable -import warnings -from loguru import logger -from pathlib import Path +from collections.abc import Callable, Collection from datetime import datetime +from pathlib import Path +import warnings + import pandas as pd +from loguru import logger +from ryan_library.functions.loguru_helpers import setup_logger +from ryan_library.processors.tuflow.base_processor import BaseProcessor from ryan_library.scripts.pomm_utils import ( aggregated_from_paths, save_peak_report_mean, save_peak_report_median, ) -from ryan_library.functions.loguru_helpers import setup_logger -from ryan_library.processors.tuflow.base_processor import BaseProcessor def run_peak_report(script_directory: Path | None = None) -> None: @@ -25,45 +26,45 @@ def run_peak_report(script_directory: Path | None = None) -> None: export_median_peak_report(script_directory=script_directory) -def generate_peak_report( +def run_peak_report_workflow( *, script_directory: Path | None = None, log_level: str = "INFO", include_pomm: bool = True, locations_to_include: Collection[str] | None = None, - report_saver: Callable[..., None], + exporter: Callable[..., None], ) -> None: - """Run the shared peak report workflow and delegate saving to ``report_saver``.""" - - setup_logger(console_log_level=log_level) - logger.info(f"Current Working Directory: {Path.cwd()}") - if script_directory is None: - script_directory = Path.cwd() + """Coordinate loading peak data and exporting via ``exporter``.""" + script_directory = script_directory or Path.cwd() normalized_locations: frozenset[str] = BaseProcessor.normalize_locations(locations_to_include) + location_filter: frozenset[str] | None = normalized_locations if normalized_locations else None - if locations_to_include and not normalized_locations: - logger.warning("Location filter provided but no valid values found. All locations will be included.") + with setup_logger(console_log_level=log_level): + logger.info(f"Current Working Directory: {Path.cwd()}") - aggregated_df: pd.DataFrame = aggregated_from_paths( - paths=[script_directory], - locations_to_include=normalized_locations if normalized_locations else None, - ) + if locations_to_include and not normalized_locations: + logger.warning("Location filter provided but no valid values found. All locations will be included.") - if aggregated_df.empty: - if normalized_locations: - logger.warning("No rows remain after applying the Location filter. Exiting.") - else: - logger.warning("No POMM CSV files found. Exiting.") - return + aggregated_df: pd.DataFrame = aggregated_from_paths( + paths=[script_directory], + locations_to_include=location_filter, + ) - timestamp: str = datetime.now().strftime(format="%Y%m%d-%H%M") - report_saver( - aggregated_df=aggregated_df, - script_directory=script_directory, - timestamp=timestamp, - include_pomm=include_pomm, - ) + if aggregated_df.empty: + if location_filter: + logger.warning("No rows remain after applying the Location filter. Exiting.") + else: + logger.warning("No POMM CSV files found. Exiting.") + return + + timestamp: str = datetime.now().strftime(format="%Y%m%d-%H%M") + exporter( + aggregated_df=aggregated_df, + script_directory=script_directory, + timestamp=timestamp, + include_pomm=include_pomm, + ) def export_median_peak_report( @@ -75,12 +76,12 @@ def export_median_peak_report( ) -> None: """Locate and process POMM files and export median-based peak values.""" - generate_peak_report( + run_peak_report_workflow( script_directory=script_directory, log_level=log_level, include_pomm=include_pomm, locations_to_include=locations_to_include, - report_saver=save_peak_report_median, + exporter=save_peak_report_median, ) @@ -93,12 +94,12 @@ def export_mean_peak_report( ) -> None: """Locate and process POMM files and export mean-based peak values.""" - generate_peak_report( + run_peak_report_workflow( script_directory=script_directory, log_level=log_level, include_pomm=include_pomm, locations_to_include=locations_to_include, - report_saver=save_peak_report_mean, + exporter=save_peak_report_mean, ) diff --git a/tests/functions/test_median_calc.py b/tests/functions/test_median_calc.py index 9406b5b6..bea65e64 100644 --- a/tests/functions/test_median_calc.py +++ b/tests/functions/test_median_calc.py @@ -1,10 +1,9 @@ -import pandas as pd import numpy as np - +import pandas as pd from ryan_library.functions.pandas.median_calc import ( - _summarise_group_statistics, median_calc, + summarise_duration_statistics, ) @@ -20,7 +19,7 @@ def make_df(values, tps, dur) -> pd.DataFrame: def test_median_stats_for_group_odd() -> None: df: pd.DataFrame = make_df([1, 2, 3], ["A", "B", "C"], ["5", "5", "5"]) - stats: dict = _summarise_group_statistics(df, "val", "tp", "dur") + stats: dict = summarise_duration_statistics(df, "val", "tp", "dur") assert stats["median"] == 2 assert stats["low"] == 1 assert stats["high"] == 3 @@ -32,7 +31,7 @@ def test_median_stats_for_group_odd() -> None: def test_median_stats_for_group_even() -> None: df: pd.DataFrame = make_df([4, 1, 3, 2], ["A", "B", "C", "D"], ["1", "1", "1", "1"]) - stats: dict = _summarise_group_statistics(df, "val", "tp", "dur") + stats: dict = summarise_duration_statistics(df, "val", "tp", "dur") # sorted values [1,2,3,4], median index 2 -> value 3 assert stats["median"] == 3 assert stats["low"] == 1 @@ -45,7 +44,7 @@ def test_median_stats_for_group_even() -> None: def test_median_stats_for_group_zeros() -> None: df: pd.DataFrame = make_df([0, 0, 0], ["A", "B", "C"], ["d", "d", "d"]) - stats = _summarise_group_statistics(df, "val", "tp", "dur") + stats = summarise_duration_statistics(df, "val", "tp", "dur") assert stats["median"] == 0 assert stats["low"] == 0 assert stats["high"] == 0 From 71fed5cdef4ae0bad7319eed48555d813d41846d Mon Sep 17 00:00:00 2001 From: Chain Frost Date: Mon, 10 Nov 2025 21:05:41 +0800 Subject: [PATCH 03/11] 2d_po locations to python list --- .../generate_2d_po_label_list.py | 213 ++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 ryan-scripts/TUFLOW-python/generate_2d_po_label_list.py diff --git a/ryan-scripts/TUFLOW-python/generate_2d_po_label_list.py b/ryan-scripts/TUFLOW-python/generate_2d_po_label_list.py new file mode 100644 index 00000000..f127da00 --- /dev/null +++ b/ryan-scripts/TUFLOW-python/generate_2d_po_label_list.py @@ -0,0 +1,213 @@ +# ryan-scripts\TUFLOW-python\generate_2d_po_label_list.py +"""Utility to convert a 2d_po GeoPackage into a Python list of PO labels. + +Example: + python generate_2d_po_label_list.py path/to/2d_po_file.gpkg +""" + + +import argparse +import sqlite3 +from dataclasses import dataclass +from collections.abc import Iterable, Sequence +from pathlib import Path +from typing import Any + +# ------------------------------------------------------------------------------ +# Configuration section +# ------------------------------------------------------------------------------ +# If you frequently run this script against the same GeoPackage you can hardcode +# the file path below (either as a Path instance or a raw string). Leave it as +# None to rely exclusively on argparse. When both a hardcoded path and a CLI +# argument are supplied, the CLI argument wins. +HARDCODED_GPKG_PATH: Path | str | None = r"E:\Library\Automation\ryan-tools\2d_po_PilaruWest_01_L.gpkg" + + +@dataclass(slots=True) +class RuntimeOptions: + """Container for the fully-resolved runtime options.""" + + gpkg_path: Path + layer: str | None + + +def parse_and_resolve_options(argv: Sequence[str] | None = None) -> RuntimeOptions: + """Parse CLI arguments (if provided) and merge them with the hardcoded defaults.""" + + parser = argparse.ArgumentParser( + description=( + "Read a 2d_po GeoPackage and emit a Python list of PO labels, " "including inline comments when provided." + ) + ) + parser.add_argument( + "gpkg_path", + nargs="?", + type=Path, + help="Path to the 2d_po GeoPackage (e.g., 2d_po_Example_L.gpkg).", + ) + parser.add_argument( + "--layer", + help=( + "Optional layer/table name inside the GeoPackage. " + "If omitted the script auto-detects the first 2d_po layer." + ), + ) + args: argparse.Namespace = parser.parse_args(argv) + + gpkg_candidate: Path | str | None + if args.gpkg_path is not None: + gpkg_candidate = args.gpkg_path + else: + gpkg_candidate = HARDCODED_GPKG_PATH + + if gpkg_candidate is None: + parser.error("No GeoPackage supplied. Provide a path argument or set HARDCODED_GPKG_PATH.") + + gpkg_path: Path = Path(gpkg_candidate).expanduser().resolve() + return RuntimeOptions(gpkg_path=gpkg_path, layer=args.layer) + + +def main() -> None: + """Entry point used by both CLI execution and IDE run configurations.""" + + options: RuntimeOptions = parse_and_resolve_options() + if not options.gpkg_path.exists(): + raise SystemExit(f"GeoPackage not found: {options.gpkg_path}") + + try: + entries: list[tuple[str, str | None]] = load_label_entries( + gpkg_path=options.gpkg_path, preferred_layer=options.layer + ) + except ValueError as exc: + raise SystemExit(str(exc)) from exc + + output: str = format_as_python_list(entries) + output_path: Path = options.gpkg_path.with_suffix(".txt") + write_output_file(output_path=output_path, contents=output) + print(output) + print(f"\nList written to {output_path}") + + +def load_label_entries(gpkg_path: Path, preferred_layer: str | None = None) -> list[tuple[str, str | None]]: + """Open the GeoPackage, determine the relevant layer, and read label/comment pairs.""" + + # Connect directly to the GeoPackage; it's just a SQLite database under the hood. + with sqlite3.connect(database=gpkg_path.as_posix()) as connection: + cursor: sqlite3.Cursor = connection.cursor() + table_name, column_lookup = resolve_table_and_columns(cursor=cursor, preferred_layer=preferred_layer) + + label_column: str | None = column_lookup.get("label") + if label_column is None: + raise ValueError(f'Table "{table_name}" does not contain a "Label" column.') + comment_column: str | None = column_lookup.get("comment") + order_column: str = column_lookup.get("fid", label_column) + + quoted_table: str = quote_identifier(table_name) + quoted_label: str = quote_identifier(label_column) + quoted_comment: str = quote_identifier(comment_column) if comment_column is not None else "NULL" + quoted_order: str = quote_identifier(order_column) + + # Build a read-only query that orders the labels by fid (or label fallback). + sql: str = ( + f"SELECT {quoted_label} AS label_value, " + f"{quoted_comment} AS comment_value " + f"FROM {quoted_table} ORDER BY {quoted_order};" + ) + rows: list[Any] = cursor.execute(sql).fetchall() + + entries: list[tuple[str, str | None]] = [] + for label_value, comment_value in rows: + if label_value is None: + continue + + # Clean up whitespace so the output list reads nicely. + label_text: str = str(label_value).strip() + if not label_text: + continue + + comment_text: str | None + if comment_value is None: + comment_text = None + else: + comment_text = sanitize_comment(str(comment_value)) + + entries.append((label_text, comment_text)) + + if not entries: + raise ValueError(f'No label values found in table "{table_name}".') + return entries + + +def resolve_table_and_columns(cursor: sqlite3.Cursor, preferred_layer: str | None) -> tuple[str, dict[str, str]]: + """Pick a layer that exposes a Label column and collect its schema metadata.""" + + tables_to_check: list[str] = build_table_priority_list(cursor=cursor, preferred_layer=preferred_layer) + for table in tables_to_check: + column_lookup: dict[str, str] = get_column_lookup(cursor=cursor, table_name=table) + if column_lookup.get("label"): + return table, column_lookup + raise ValueError("Could not locate a layer with a 'Label' column.") + + +def build_table_priority_list(cursor: sqlite3.Cursor, preferred_layer: str | None) -> list[str]: + """Return candidate layers, prioritizing those that look like 2d_po layers.""" + + layer_rows: list[Any] = cursor.execute( + "SELECT table_name FROM gpkg_contents WHERE data_type = 'features';" + ).fetchall() + discovered_layers: list[Any] = [row[0] for row in layer_rows] + + if preferred_layer: + return [preferred_layer] + + def sort_key(name: str) -> tuple[int, str]: + lowered: str = name.lower() + return (0 if lowered.startswith("2d_po") else 1, lowered) + + return sorted(discovered_layers, key=sort_key) + + +def get_column_lookup(cursor: sqlite3.Cursor, table_name: str) -> dict[str, str]: + """Fetch column names for the table and normalize them for case-insensitive use.""" + + quoted_table: str = quote_identifier(table_name) + pragma_rows: list[Any] = cursor.execute(f"PRAGMA table_info({quoted_table});").fetchall() + return {row[1].lower(): row[1] for row in pragma_rows} + + +def sanitize_comment(comment: str) -> str | None: + """Normalize whitespace so comments become single-line Python-friendly strings.""" + + normalized: str = comment.replace("\r\n", "\n").replace("\r", " ").replace("\n", " ").strip() + return normalized or None + + +def format_as_python_list(entries: Iterable[tuple[str, str | None]]) -> str: + """Format the label/comment pairs as a copy/paste-friendly Python list literal.""" + + lines: list[str] = ["["] + for label, comment in entries: + # Represent labels with repr() so any embedded quotes are automatically escaped. + line: str = f" {repr(label)}," + if comment: + line += f" # {comment}" + lines.append(line) + lines.append("]") + return "\n".join(lines) + + +def quote_identifier(identifier: str) -> str: + """Quote SQLite identifiers to avoid syntax errors or SQL injection surprises.""" + + escaped = identifier.replace('"', '""') + return f'"{escaped}"' + + +def write_output_file(*, output_path: Path, contents: str) -> None: + """Persist the rendered list next to the source GeoPackage for reuse.""" + + output_path.write_text(f"{contents}\n", encoding="utf-8") + + +if __name__ == "__main__": + main() From 56513333d344864d443663b25bfb658ad214350c Mon Sep 17 00:00:00 2001 From: Chain Frost Date: Mon, 10 Nov 2025 22:02:46 +0800 Subject: [PATCH 04/11] checking if closure durations still work. --- ryan_library/classes/column_definitions.py | 12 +++---- ryan_library/functions/pandas/median_calc.py | 35 +++++++++++++++---- .../scripts/RORB/closure_durations.py | 3 +- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/ryan_library/classes/column_definitions.py b/ryan_library/classes/column_definitions.py index 34c5cede..03f75df8 100644 --- a/ryan_library/classes/column_definitions.py +++ b/ryan_library/classes/column_definitions.py @@ -227,22 +227,18 @@ def default(cls) -> "ColumnMetadataRegistry": name="mean_PeakFlow", description=( "Peak flow from the event whose statistic is nearest to the group's arithmetic mean; " - "not an averaged peak flow." + "not an averaged peak flow. Uses mean_including_zeroes." ), value_type="float", ), "mean_Duration": ColumnDefinition( name="mean_Duration", - description=( - "Duration taken from the same nearest-to-mean event used for mean_PeakFlow (not an averaged value)." - ), + description=("Duration taken from the same nearest-to-mean event used for mean_PeakFlow."), value_type="string", ), "mean_TP": ColumnDefinition( name="mean_TP", - description=( - "Temporal pattern taken from the same nearest-to-mean event used for mean_PeakFlow." - ), + description=("Temporal pattern taken from the same nearest-to-mean event used for mean_PeakFlow."), value_type="string", ), "low": ColumnDefinition( @@ -257,7 +253,7 @@ def default(cls) -> "ColumnMetadataRegistry": ), "count": ColumnDefinition( name="count", - description="Number of rows contributing to the median statistics for the selected duration.", + description="Number of rows contributing to the mean/median statistics for the selected duration.", value_type="int", ), "count_bin": ColumnDefinition( diff --git a/ryan_library/functions/pandas/median_calc.py b/ryan_library/functions/pandas/median_calc.py index eb62c1b1..4d4fe4e9 100644 --- a/ryan_library/functions/pandas/median_calc.py +++ b/ryan_library/functions/pandas/median_calc.py @@ -1,9 +1,8 @@ # ryan_library\functions\pandas\median_calc.py """Utilities for summarising grouped statistics for POMM reports.""" -from __future__ import annotations - from typing import Any +from collections.abc import Callable import pandas as pd from pandas import DataFrame @@ -24,7 +23,7 @@ def summarise_duration_statistics(durgrp: pd.DataFrame, stat_col: str, tp_col: s mean_tp: Any = pd.NA mean_peak_flow = float("nan") if stat_series.notna().any(): - closest_idx = (stat_series - mean_including_zeroes).abs().idxmin() + closest_idx: int | str = (stat_series - mean_including_zeroes).abs().idxmin() mean_duration = ensemblestat.loc[closest_idx, dur_col] mean_tp = ensemblestat.loc[closest_idx, tp_col] mean_peak_flow = float(ensemblestat.loc[closest_idx, stat_col]) @@ -47,14 +46,36 @@ def summarise_duration_statistics(durgrp: pd.DataFrame, stat_col: str, tp_col: s def calculate_median_statistics( thinned_df: pd.DataFrame, stat_col: str, tp_col: str, dur_col: str ) -> tuple[dict[str, Any], list[dict[str, Any]]]: - """Return per-duration stats and the record with the largest median.""" + """Return per-duration stats and the record with the largest median. + The logic is based on the ``stats`` function in ``TUFLOW_2023_max_med_from POMM_v9.py``. + For each duration group the DataFrame is sorted by ``statcol``. The median + value is selected, along with the associated temporal pattern. The group with + the highest median is returned separately. + + Parameters + ---------- + thinned_df: + Data for a single AEP across multiple temporal patterns and durations. + statcol: + Column containing the numeric statistic to rank by (e.g. ``"AbsMax"``). + tpcol: + Column holding the temporal pattern identifier. + durcol: + Column holding the duration identifier. + + Returns + ------- + tuple[dict[str, Any], list[dict[str, Any]]] + A tuple containing the stats for the duration with the largest median and + a list of stats for each duration group. + """ max_stats_dict: dict[str, Any] = {} bin_stats_list: list[dict[str, Any]] = [] tracking_median: float = float("-inf") count_bin: int = 0 - for _, durgrp in thinned_df.groupby(by=dur_col): + for _, durgrp in thinned_df.groupby(by=dur_col): # type: ignore stats_dict: dict[str, Any] = summarise_duration_statistics( durgrp=durgrp, stat_col=stat_col, tp_col=tp_col, dur_col=dur_col ) @@ -67,6 +88,7 @@ def calculate_median_statistics( count_bin += stats_dict["count"] max_stats_dict["count_bin"] = count_bin + # override low/high with the true min/max over all groups: if not thinned_df.empty: global_low = float(thinned_df[stat_col].min()) global_high = float(thinned_df[stat_col].max()) @@ -84,5 +106,4 @@ def median_calc( # Backwards compatibility for older imports -median_stats = calculate_median_statistics -_summarise_group_statistics = summarise_duration_statistics +median_stats: Callable[..., tuple[dict[str, Any], list[dict[str, Any]]]] = calculate_median_statistics diff --git a/ryan_library/scripts/RORB/closure_durations.py b/ryan_library/scripts/RORB/closure_durations.py index 7dc44a06..68a9310c 100644 --- a/ryan_library/scripts/RORB/closure_durations.py +++ b/ryan_library/scripts/RORB/closure_durations.py @@ -1,8 +1,9 @@ +# ryan_library\scripts\RORB\closure_durations.py + from datetime import datetime from pathlib import Path from collections.abc import Iterable -from typing import TYPE_CHECKING import pandas as pd from loguru import logger From d39698e68c51be7a66f936e4893ca5b9bb21f94f Mon Sep 17 00:00:00 2001 From: Chain Frost Date: Tue, 11 Nov 2025 00:32:07 +0800 Subject: [PATCH 05/11] parquet option for POMM --- ryan-scripts/TUFLOW-python/POMM_combine.py | 3 + ryan_library/functions/misc_functions.py | 146 +++++++++++++++--- ryan_library/functions/tuflow/pomm_combine.py | 11 +- 3 files changed, 133 insertions(+), 27 deletions(-) diff --git a/ryan-scripts/TUFLOW-python/POMM_combine.py b/ryan-scripts/TUFLOW-python/POMM_combine.py index b7441c24..293a3dc8 100644 --- a/ryan-scripts/TUFLOW-python/POMM_combine.py +++ b/ryan-scripts/TUFLOW-python/POMM_combine.py @@ -12,6 +12,8 @@ # Update this tuple to restrict processing to specific PO/Location values. # Leave empty to include every location found in the POMM files. LOCATIONS_TO_INCLUDE: tuple[str, ...] = () +# Toggle to emit a Parquet copy of the combined output. +EXPORT_PARQUET: bool = False def main() -> None: @@ -35,6 +37,7 @@ def main() -> None: include_data_types=["POMM"], console_log_level=console_log_level, locations_to_include=locations_to_include, + export_parquet=EXPORT_PARQUET, ) print() print_library_version() diff --git a/ryan_library/functions/misc_functions.py b/ryan_library/functions/misc_functions.py index 6dbfe39e..dba56268 100644 --- a/ryan_library/functions/misc_functions.py +++ b/ryan_library/functions/misc_functions.py @@ -112,6 +112,9 @@ def export_dataframes( column_widths: dict[str, dict[str, float]] | None = None, auto_adjust_width: bool = True, file_name: str | None = None, + *, + force_parquet: bool = False, + parquet_compression: str = "gzip", ) -> None: """Export multiple DataFrames to Excel files with optional column widths. Args: @@ -138,6 +141,12 @@ def export_dataframes( ``export_dict``. When provided, the auto-generated timestamp prefix is skipped and ``file_name`` is written exactly (``.xlsx`` appended when missing). + force_parquet (bool, optional): + If True, skip Excel generation and export Parquet files using the standard + naming convention regardless of DataFrame size. + parquet_compression (str, optional): + Compression codec passed to pandas when writing Parquet files while + ``force_parquet`` is True. Defaults to ``"gzip"``. Raises: ValueError: If the number of DataFrames doesn't match the number of sheets. InvalidFileException: If there's an issue with writing the Excel file. @@ -169,16 +178,29 @@ def export_dataframes( f"For file '{file_label}', the number of dataframes ({len(dataframes)}) and sheets ({len(sheets)}) must match." ) + export_stem: str = self._resolve_export_stem( + datetime_string=datetime_string, export_key=export_key, file_name=file_name + ) + + if force_parquet: + self._export_as_parquet_only( + export_stem=export_stem, + dataframes=dataframes, + sheets=sheets, + output_directory=output_directory, + compression=parquet_compression, + ) + continue + if self._exceeds_excel_limits(dataframes=dataframes): logging.warning( "Data for '%s' exceeds Excel size limits. Exporting to Parquet and CSV instead.", - file_name, + export_stem, ) self._export_as_parquet_and_csv( - file_name=file_name, + export_stem=export_stem, dataframes=dataframes, sheets=sheets, - datetime_string=datetime_string, output_directory=output_directory, ) continue @@ -187,7 +209,7 @@ def export_dataframes( if file_name is not None: export_filename = file_name if file_name.lower().endswith(".xlsx") else f"{file_name}.xlsx" else: - export_filename = f"{datetime_string}_{export_key}.xlsx" + export_filename = f"{export_stem}.xlsx" export_path: Path = ( (output_directory / export_filename) if output_directory else Path(export_filename) # Defaults to CWD ) @@ -260,13 +282,86 @@ def _exceeds_excel_limits(self, dataframes: list[pd.DataFrame]) -> bool: return True return False + def _resolve_export_stem(self, *, datetime_string: str, export_key: str, file_name: str | None) -> str: + """Return the base filename (without extension) for the current export.""" + + if file_name: + return file_name[:-5] if file_name.lower().endswith(".xlsx") else file_name + return f"{datetime_string}_{export_key}" + + def _build_parquet_filename(self, base_filename: str, compression: str | None) -> str: + """Return a parquet filename with an optional compression suffix.""" + + if compression: + suffix = compression.lower() + if suffix == "gzip": + return f"{base_filename}.parquet.gzip" + return f"{base_filename}.parquet.{suffix}" + return f"{base_filename}.parquet" + + def _write_parquet( + self, + *, + df: pd.DataFrame, + sheet: str, + parquet_path: Path, + export_label: str, + compression: str | None, + ) -> None: + """Write a DataFrame to Parquet with consistent logging and error handling.""" + + try: + df.to_parquet(path=parquet_path, index=False, compression=compression) + logging.info("Exported Parquet to %s", parquet_path) + except (ImportError, ValueError) as exc: + message: str = ( + "Unable to export Parquet for " + f"'{export_label}' sheet '{sheet}': {exc}. Install pyarrow or fastparquet." + ) + logging.error(message) + print(message) + except Exception as exc: # pragma: no cover - unforeseen errors should be logged + logging.exception("Unexpected error during Parquet export for '%s' sheet '%s': %s", export_label, sheet, exc) + + def _export_as_parquet_only( + self, + *, + export_stem: str, + dataframes: list[pd.DataFrame], + sheets: list[str], + output_directory: Path | None, + compression: str | None, + ) -> None: + """Export each DataFrame to a Parquet file sharing the Excel naming scheme.""" + + export_targets: list[tuple[pd.DataFrame, str, Path]] = [] + for df, sheet in zip(dataframes, sheets): + sanitized_sheet: str = self._sanitize_name(sheet) + base_filename: str = f"{export_stem}_{sanitized_sheet}" + parquet_filename: str = self._build_parquet_filename(base_filename, compression) + parquet_path: Path = self._build_output_path( + base_filename=parquet_filename, output_directory=output_directory + ) + parquet_path.parent.mkdir(parents=True, exist_ok=True) + export_targets.append((df, sheet, parquet_path)) + + for df, sheet, parquet_path in export_targets: + self._write_parquet( + df=df, + sheet=sheet, + parquet_path=parquet_path, + export_label=export_stem, + compression=compression, + ) + def _export_as_parquet_and_csv( self, - file_name: str, + *, + export_stem: str, dataframes: list[pd.DataFrame], sheets: list[str], - datetime_string: str, output_directory: Path | None, + compression: str | None = None, ) -> None: """Export dataframes to Parquet and CSV files when Excel limits are exceeded.""" @@ -274,10 +369,11 @@ def _export_as_parquet_and_csv( for df, sheet in zip(dataframes, sheets): sanitized_sheet: str = self._sanitize_name(sheet) - base_filename: str = f"{datetime_string}_{file_name}_{sanitized_sheet}" + base_filename: str = f"{export_stem}_{sanitized_sheet}" + parquet_filename: str = self._build_parquet_filename(base_filename, compression) parquet_path: Path = self._build_output_path( - base_filename=f"{base_filename}.parquet", output_directory=output_directory + base_filename=parquet_filename, output_directory=output_directory ) csv_path: Path = self._build_output_path( base_filename=f"{base_filename}.csv", output_directory=output_directory @@ -287,20 +383,13 @@ def _export_as_parquet_and_csv( export_targets.append((df, sheet, parquet_path, csv_path)) for df, sheet, parquet_path, _ in export_targets: - try: - df.to_parquet(path=parquet_path, index=False) - logging.info("Exported Parquet to %s", parquet_path) - except (ImportError, ValueError) as exc: - message: str = ( - "Unable to export Parquet for " - f"'{file_name}' sheet '{sheet}': {exc}. Install pyarrow or fastparquet." - ) - logging.error(message) - print(message) - except Exception as exc: # pragma: no cover - unforeseen errors should be logged - logging.exception( - "Unexpected error during Parquet export for '%s' sheet '%s': %s", file_name, sheet, exc - ) + self._write_parquet( + df=df, + sheet=sheet, + parquet_path=parquet_path, + export_label=export_stem, + compression=compression, + ) for df, sheet, _, csv_path in export_targets: df.to_csv(path_or_buf=csv_path, index=False) @@ -328,6 +417,9 @@ def save_to_excel( column_widths: dict[str, float] | None = None, auto_adjust_width: bool = True, file_name: str | None = None, + *, + force_parquet: bool = False, + parquet_compression: str = "gzip", ) -> None: """Export a single DataFrame to an Excel file with a single sheet and optional column widths. @@ -348,7 +440,13 @@ def save_to_excel( file_name (str | None, optional): Explicit file name to use for the exported workbook. When provided the timestamp-based prefix is skipped and ``file_name`` is written exactly as - supplied (``.xlsx`` is appended automatically when missing).""" + supplied (``.xlsx`` is appended automatically when missing). + force_parquet (bool, optional): + If True, bypass Excel generation and export the data as Parquet files + mirroring the Excel naming scheme. + parquet_compression (str, optional): + Compression codec to use when ``force_parquet`` is enabled. Defaults to + ``"gzip"``.""" export_dict: dict[str, ExportContent] = {file_name_prefix: {"dataframes": [data_frame], "sheets": [sheet_name]}} # Prepare column_widths in the required format @@ -362,6 +460,8 @@ def save_to_excel( column_widths=prepared_column_widths, auto_adjust_width=auto_adjust_width, file_name=file_name, + force_parquet=force_parquet, + parquet_compression=parquet_compression, ) def calculate_column_widths(self, df: pd.DataFrame) -> dict[str, float]: diff --git a/ryan_library/functions/tuflow/pomm_combine.py b/ryan_library/functions/tuflow/pomm_combine.py index 6a29c826..60b07c46 100644 --- a/ryan_library/functions/tuflow/pomm_combine.py +++ b/ryan_library/functions/tuflow/pomm_combine.py @@ -23,6 +23,7 @@ def main_processing( include_data_types: list[str] | None = None, console_log_level: str = "INFO", locations_to_include: Collection[str] | None = None, + export_parquet: bool = False, ) -> None: """Generate merged culvert data and export the results.""" @@ -50,26 +51,28 @@ def main_processing( if normalized_locations: results_set.filter_locations(normalized_locations) - export_results(results=results_set) + export_results(results=results_set, export_parquet=export_parquet) logger.info("End of POMM results combination processing") -def export_results(results: ProcessorCollection) -> None: - """Export combined DataFrames to Excel.""" +def export_results(*, results: ProcessorCollection, export_parquet: bool = False) -> None: + """Export combined DataFrames to either Excel or Parquet based on user preference.""" if not results.processors: logger.warning("No results to export.") return - exporter = ExcelExporter() combined_df: pd.DataFrame = results.pomm_combine() if combined_df.empty: logger.warning("No combined data found. Skipping export.") return ensure_output_directory(output_dir=Path.cwd()) + exporter = ExcelExporter() exporter.save_to_excel( data_frame=combined_df, file_name_prefix="combined_POMM", sheet_name="combined_POMM", output_directory=Path.cwd(), + force_parquet=export_parquet, + parquet_compression="gzip", ) From f035996176dc1195be8262dce45af4e658bd69a3 Mon Sep 17 00:00:00 2001 From: Chain Frost Date: Tue, 11 Nov 2025 00:43:55 +0800 Subject: [PATCH 06/11] pomm parquet options. file_utils search improvement. remember to test it in logsummary etc. --- ryan-scripts/TUFLOW-python/POMM_combine.py | 7 +- ryan_library/functions/file_utils.py | 161 ++++++++++-------- ryan_library/functions/misc_functions.py | 46 +++-- ryan_library/functions/tuflow/pomm_combine.py | 13 +- 4 files changed, 131 insertions(+), 96 deletions(-) diff --git a/ryan-scripts/TUFLOW-python/POMM_combine.py b/ryan-scripts/TUFLOW-python/POMM_combine.py index 293a3dc8..cb15835e 100644 --- a/ryan-scripts/TUFLOW-python/POMM_combine.py +++ b/ryan-scripts/TUFLOW-python/POMM_combine.py @@ -1,6 +1,7 @@ # ryan-scripts\TUFLOW-python\POMM_combine.py import os from pathlib import Path +from typing import Literal from ryan_library.scripts.tuflow.pomm_combine import main_processing from ryan_library.scripts.wrapper_utils import ( change_working_directory, @@ -12,8 +13,8 @@ # Update this tuple to restrict processing to specific PO/Location values. # Leave empty to include every location found in the POMM files. LOCATIONS_TO_INCLUDE: tuple[str, ...] = () -# Toggle to emit a Parquet copy of the combined output. -EXPORT_PARQUET: bool = False +# Choose output format: "excel", "parquet", or "both". +EXPORT_MODE: Literal["excel", "parquet", "both"] = "excel" def main() -> None: @@ -37,7 +38,7 @@ def main() -> None: include_data_types=["POMM"], console_log_level=console_log_level, locations_to_include=locations_to_include, - export_parquet=EXPORT_PARQUET, + export_mode=EXPORT_MODE, ) print() print_library_version() diff --git a/ryan_library/functions/file_utils.py b/ryan_library/functions/file_utils.py index 8db61bc9..1b44c261 100644 --- a/ryan_library/functions/file_utils.py +++ b/ryan_library/functions/file_utils.py @@ -2,11 +2,11 @@ from collections.abc import Generator from pathlib import Path -from concurrent.futures import ThreadPoolExecutor import fnmatch +import re from loguru import logger import threading -from queue import Queue +from queue import Empty, Queue def find_files_parallel( @@ -43,18 +43,35 @@ def find_files_parallel( match any of the exclusion patterns. """ - # Normalize 'patterns' and 'excludes' to lists for consistent processing - patterns = [patterns] if isinstance(patterns, str) else patterns - excludes = [excludes] if isinstance(excludes, str) else excludes or [] + def _normalize_globs(globs: str | list[str] | None) -> list[str]: + """Ensure downstream logic always receives an iterable of glob strings.""" + if globs is None: + return [] + if isinstance(globs, str): + return [globs] + return list(globs) - # Convert all patterns to lowercase for case-insensitive matching - patterns = [p.lower() for p in patterns] - excludes = [e.lower() for e in excludes] + def _compile_patterns(globs: list[str]) -> list[re.Pattern[str]]: + """Translate glob syntax into compiled regex objects once up front.""" + compiled: list[re.Pattern[str]] = [] + for raw_pattern in globs: + compiled.append(re.compile(fnmatch.translate(raw_pattern), re.IGNORECASE)) + return compiled + + def _matches_any(name: str, compiled: list[re.Pattern[str]]) -> bool: + """Return True when the provided filename matches any compiled glob.""" + return any(pattern.match(name) for pattern in compiled) + + include_patterns: list[str] = _normalize_globs(patterns) + exclude_patterns: list[str] = _normalize_globs(excludes) + + compiled_includes: list[re.Pattern[str]] = _compile_patterns(include_patterns) + compiled_excludes: list[re.Pattern[str]] = _compile_patterns(exclude_patterns) logger.info(f"Root directories: {root_dirs}") - logger.info(f"Search patterns: {patterns}") - if excludes: - logger.info(f"Exclude patterns: {excludes}") + logger.info(f"Search patterns: {include_patterns}") + if exclude_patterns: + logger.info(f"Exclude patterns: {exclude_patterns}") # Obtain the current working directory to calculate relative paths later # ``absolute`` preserves drive-letter vs UNC style while ensuring an @@ -71,8 +88,9 @@ def find_files_parallel( visited_lock = threading.Lock() visited_dirs: set[Path] = set() - # Queue for directories to process + # Queue for directories to process; the stop event lets workers exit once traversal finishes. dir_queue: Queue[tuple[Path, Path]] = Queue() + stop_event = threading.Event() # Initialize the queue with absolute root directories without converting # between drive letters and UNC paths. @@ -87,39 +105,32 @@ def find_files_parallel( dir_queue.put((abs_root, abs_root)) # (current_path, root_dir) except FileNotFoundError: logger.error(f"Root directory does not exist: {root_dir}") - except Exception as e: - logger.error(f"Error resolving root directory {root_dir}: {e}") + except Exception as exc: + logger.error(f"Error resolving root directory {root_dir}: {exc}") def worker() -> None: - while True: + """Continuously pull directories off the queue, scanning files and enqueueing child folders.""" + while not stop_event.is_set(): try: - current_path, root_dir = dir_queue.get(timeout=1) - # Timeout to allow graceful exit - except: - # Queue is empty or timeout reached - return + current_path, root_dir = dir_queue.get(timeout=0.2) + except Empty: + continue try: + # Keep per-thread results local so we only touch global locks when we have matches. local_matched: list[Path] = [] local_folders_with_matches: set[Path] = set() - files_searched = 0 - folders_searched = 0 - # Use non-recursive glob to avoid overlapping traversals try: iterator: Generator[Path, None, None] = current_path.iterdir() except PermissionError: logger.error(f"Permission denied accessing directory: {current_path}") - dir_queue.task_done() continue - except Exception as e: - logger.error(f"Error accessing directory {current_path}: {e}") - dir_queue.task_done() + except Exception as exc: + logger.error(f"Error accessing directory {current_path}: {exc}") continue for subpath in iterator: if subpath.is_dir(): - folders_searched += 1 - if recursive_search and report_level: try: relative_path = subpath.relative_to(root_dir) @@ -132,7 +143,7 @@ def worker() -> None: display_path: Path = subpath.relative_to(current_dir) except ValueError: display_path = subpath.absolute() - logger.info(f"Searching (depth {depth}): {display_path}") + logger.debug(f"Searching (depth {depth}): {display_path}") if recursive_search: try: @@ -145,10 +156,12 @@ def worker() -> None: except PermissionError: logger.error(f"Permission denied accessing subdirectory: {subpath}") continue - except Exception as e: - logger.error(f"Error resolving subdirectory {subpath}: {e}") + except Exception as exc: + logger.error(f"Error resolving subdirectory {subpath}: {exc}") continue + # Record directories we've seen so we do not process the same path twice + # when multiple roots overlap or symlinks point back to an ancestor. with visited_lock: if resolved_subpath in visited_dirs: continue @@ -157,31 +170,32 @@ def worker() -> None: dir_queue.put((resolved_subpath, root_dir)) continue - files_searched += 1 - filename = subpath.name.lower() + filename: str = subpath.name - # Inclusion Check - if any(fnmatch.fnmatch(filename, pattern) for pattern in patterns): - # Exclusion Check - if not any(fnmatch.fnmatch(filename, exclude) for exclude in excludes): - try: - matched_file: Path = subpath.absolute() - display_path = matched_file - try: - display_path = matched_file.relative_to(current_dir) - except ValueError: - pass - logger.debug(f"Matched file: {display_path}") - if not matched_file.exists(): - raise FileNotFoundError - local_matched.append(matched_file) - local_folders_with_matches.add(matched_file.parent) - except FileNotFoundError: - logger.warning(f"File does not exist (might have been moved): {subpath}") - except PermissionError: - logger.error(f"Permission denied accessing file: {subpath}") - except Exception as e: - logger.error(f"Error resolving file {subpath}: {e}") + if not _matches_any(name=filename, compiled=compiled_includes): + continue + + if compiled_excludes and _matches_any(name=filename, compiled=compiled_excludes): + continue + + try: + matched_file: Path = subpath.absolute() + display_path = matched_file + try: + display_path = matched_file.relative_to(current_dir) + except ValueError: + pass + logger.debug(f"Matched file: {display_path}") + if not matched_file.exists(): + raise FileNotFoundError + local_matched.append(matched_file) + local_folders_with_matches.add(matched_file.parent) + except FileNotFoundError: + logger.warning(f"File does not exist (might have been moved): {subpath}") + except PermissionError: + logger.error(f"Permission denied accessing file: {subpath}") + except Exception as exc: + logger.error(f"Error resolving file {subpath}: {exc}") # Safely update the global matched_files list if local_matched: @@ -192,28 +206,27 @@ def worker() -> None: with folders_with_matches_lock: folders_with_matches.update(local_folders_with_matches) - if len(local_matched) > 0: - logger.info(f"Found {len(local_matched)} files in {current_path}") - except Exception as e: - logger.error(f"Unexpected error processing {current_path}: {e}") + if local_matched: + logger.debug(f"Found {len(local_matched)} files in {current_path}") + except Exception as exc: + logger.error(f"Unexpected error processing {current_path}: {exc}") finally: dir_queue.task_done() logger.info(f"Starting search in {len(root_dirs)} root directory(ies).") - - # Determine the number of worker threads; adjust as needed - num_workers: int = min(32, (len(root_dirs) * 4) or 4) - - with ThreadPoolExecutor(max_workers=num_workers) as executor: - # Launch worker threads - futures = [executor.submit(worker) for _ in range(num_workers)] - - # Wait for all tasks in the queue to be processed - dir_queue.join() - - # Optionally, wait for all worker threads to complete - for future in futures: - future.result() + num_workers: int = min(32, max(len(root_dirs) * 4, 4)) + threads: list[threading.Thread] = [ + threading.Thread(target=worker, name=f"find-files-worker-{i}", daemon=True) for i in range(num_workers) + ] + for thread in threads: + thread.start() + + # Wait until every directory queued for processing has been handled. + dir_queue.join() + stop_event.set() + + for thread in threads: + thread.join() # Log folders with matched files if print_found_folder: diff --git a/ryan_library/functions/misc_functions.py b/ryan_library/functions/misc_functions.py index dba56268..8605e6fc 100644 --- a/ryan_library/functions/misc_functions.py +++ b/ryan_library/functions/misc_functions.py @@ -4,7 +4,7 @@ import multiprocessing import pandas as pd import logging -from typing import TypedDict +from typing import Literal, TypedDict from pathlib import Path from importlib import metadata import re @@ -113,7 +113,7 @@ def export_dataframes( auto_adjust_width: bool = True, file_name: str | None = None, *, - force_parquet: bool = False, + export_mode: Literal["excel", "parquet", "both"] = "excel", parquet_compression: str = "gzip", ) -> None: """Export multiple DataFrames to Excel files with optional column widths. @@ -141,12 +141,17 @@ def export_dataframes( ``export_dict``. When provided, the auto-generated timestamp prefix is skipped and ``file_name`` is written exactly (``.xlsx`` appended when missing). - force_parquet (bool, optional): - If True, skip Excel generation and export Parquet files using the standard - naming convention regardless of DataFrame size. + export_mode (Literal["excel", "parquet", "both"], optional): + Controls which artefacts are produced: + * "excel" (default) writes only Excel files (with automatic Parquet/CSV + fallback when Excel limits are exceeded). + * "parquet" skips Excel entirely and writes Parquet files matching the + Excel naming scheme. + * "both" writes Excel files (subject to the standard fallback) and also + emits companion Parquet files. parquet_compression (str, optional): - Compression codec passed to pandas when writing Parquet files while - ``force_parquet`` is True. Defaults to ``"gzip"``. + Compression codec passed to pandas whenever Parquet files are written. + Defaults to ``"gzip"``. Raises: ValueError: If the number of DataFrames doesn't match the number of sheets. InvalidFileException: If there's an issue with writing the Excel file. @@ -164,6 +169,10 @@ def export_dataframes( ExcelExporter().export_dataframes(export_dict, output_directory=Path("exports")) """ datetime_string: str = datetime.now().strftime(format="%Y%m%d-%H%M") + normalized_mode: str = export_mode.lower() + valid_modes: set[str] = {"excel", "parquet", "both"} + if normalized_mode not in valid_modes: + raise ValueError(f"Invalid export_mode '{export_mode}'. Expected one of {sorted(valid_modes)}.") if file_name is not None and len(export_dict) != 1: raise ValueError("'file_name' can only be provided when exporting a single workbook.") @@ -182,7 +191,7 @@ def export_dataframes( datetime_string=datetime_string, export_key=export_key, file_name=file_name ) - if force_parquet: + if normalized_mode == "parquet": self._export_as_parquet_only( export_stem=export_stem, dataframes=dataframes, @@ -202,6 +211,7 @@ def export_dataframes( dataframes=dataframes, sheets=sheets, output_directory=output_directory, + compression=parquet_compression, ) continue @@ -261,6 +271,15 @@ def export_dataframes( logging.error(f"Failed to write to '{export_path}': {e}") raise + if normalized_mode == "both": + self._export_as_parquet_only( + export_stem=export_stem, + dataframes=dataframes, + sheets=sheets, + output_directory=output_directory, + compression=parquet_compression, + ) + def _exceeds_excel_limits(self, dataframes: list[pd.DataFrame]) -> bool: """Return True if any dataframe exceeds Excel's size limits.""" @@ -418,7 +437,7 @@ def save_to_excel( auto_adjust_width: bool = True, file_name: str | None = None, *, - force_parquet: bool = False, + export_mode: Literal["excel", "parquet", "both"] = "excel", parquet_compression: str = "gzip", ) -> None: """Export a single DataFrame to an Excel file with a single sheet and optional column widths. @@ -441,11 +460,10 @@ def save_to_excel( Explicit file name to use for the exported workbook. When provided the timestamp-based prefix is skipped and ``file_name`` is written exactly as supplied (``.xlsx`` is appended automatically when missing). - force_parquet (bool, optional): - If True, bypass Excel generation and export the data as Parquet files - mirroring the Excel naming scheme. + export_mode (Literal["excel", "parquet", "both"], optional): + See :meth:`export_dataframes` for details. parquet_compression (str, optional): - Compression codec to use when ``force_parquet`` is enabled. Defaults to + Compression codec to use when Parquet outputs are requested. Defaults to ``"gzip"``.""" export_dict: dict[str, ExportContent] = {file_name_prefix: {"dataframes": [data_frame], "sheets": [sheet_name]}} @@ -460,7 +478,7 @@ def save_to_excel( column_widths=prepared_column_widths, auto_adjust_width=auto_adjust_width, file_name=file_name, - force_parquet=force_parquet, + export_mode=export_mode, parquet_compression=parquet_compression, ) diff --git a/ryan_library/functions/tuflow/pomm_combine.py b/ryan_library/functions/tuflow/pomm_combine.py index 60b07c46..9f0657e4 100644 --- a/ryan_library/functions/tuflow/pomm_combine.py +++ b/ryan_library/functions/tuflow/pomm_combine.py @@ -2,6 +2,7 @@ from collections.abc import Collection from pathlib import Path +from typing import Literal import pandas as pd from loguru import logger @@ -23,7 +24,7 @@ def main_processing( include_data_types: list[str] | None = None, console_log_level: str = "INFO", locations_to_include: Collection[str] | None = None, - export_parquet: bool = False, + export_mode: Literal["excel", "parquet", "both"] = "excel", ) -> None: """Generate merged culvert data and export the results.""" @@ -51,12 +52,14 @@ def main_processing( if normalized_locations: results_set.filter_locations(normalized_locations) - export_results(results=results_set, export_parquet=export_parquet) + export_results(results=results_set, export_mode=export_mode) logger.info("End of POMM results combination processing") -def export_results(*, results: ProcessorCollection, export_parquet: bool = False) -> None: - """Export combined DataFrames to either Excel or Parquet based on user preference.""" +def export_results( + *, results: ProcessorCollection, export_mode: Literal["excel", "parquet", "both"] = "excel" +) -> None: + """Export combined DataFrames according to the requested mode.""" if not results.processors: logger.warning("No results to export.") return @@ -73,6 +76,6 @@ def export_results(*, results: ProcessorCollection, export_parquet: bool = False file_name_prefix="combined_POMM", sheet_name="combined_POMM", output_directory=Path.cwd(), - force_parquet=export_parquet, + export_mode=export_mode, parquet_compression="gzip", ) From 38bead506582df4f608ecfe28f58a4a925782054 Mon Sep 17 00:00:00 2001 From: Chain Frost Date: Tue, 11 Nov 2025 07:16:10 +0800 Subject: [PATCH 07/11] tests --- tests/functions/test_file_utils_small.py | 77 +++++++++++++++++++++ tests/functions/test_pomm_combine.py | 85 ++++++++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 tests/functions/test_file_utils_small.py create mode 100644 tests/functions/test_pomm_combine.py diff --git a/tests/functions/test_file_utils_small.py b/tests/functions/test_file_utils_small.py new file mode 100644 index 00000000..726606f3 --- /dev/null +++ b/tests/functions/test_file_utils_small.py @@ -0,0 +1,77 @@ +from pathlib import Path + + +from ryan_library.functions.file_utils import ( + ensure_output_directory, + find_files_parallel, + is_non_zero_file, +) + + +def _write_file(path: Path, content: str = "data") -> None: + path.write_text(content, encoding="utf-8") + + +def test_find_files_parallel_non_recursive_respects_excludes(tmp_path: Path) -> None: + root = tmp_path / "root" + sub = root / "nested" + sub.mkdir(parents=True) + keep = root / "keep.txt" + skip = root / "skip.txt" + nested = sub / "nested.txt" + _write_file(keep) + _write_file(skip) + _write_file(nested) + + results = find_files_parallel( + root_dirs=[root], + patterns="*.txt", + excludes=["skip.txt"], + recursive_search=False, + report_level=None, + print_found_folder=False, + ) + + assert results == [keep.resolve()] + + +def test_find_files_parallel_deduplicates_overlapping_roots(tmp_path: Path) -> None: + root = tmp_path / "root" + nested = root / "nested" + nested.mkdir(parents=True) + target = nested / "match.csv" + _write_file(target) + + results = find_files_parallel( + root_dirs=[root, nested], + patterns="*.csv", + recursive_search=True, + report_level=None, + print_found_folder=False, + ) + + assert results == [target.resolve()] + + +def test_is_non_zero_file_variants(tmp_path: Path) -> None: + data_file = tmp_path / "data.txt" + empty_file = tmp_path / "empty.txt" + directory = tmp_path / "dir" + directory.mkdir() + _write_file(data_file) + empty_file.touch() + + assert is_non_zero_file(data_file) + assert is_non_zero_file(str(data_file)) + assert not is_non_zero_file(empty_file) + assert not is_non_zero_file(directory) + assert not is_non_zero_file(tmp_path / "missing.txt") + + +def test_ensure_output_directory_creates_missing(tmp_path: Path) -> None: + out_dir = tmp_path / "exports" + ensure_output_directory(out_dir) + assert out_dir.exists() and out_dir.is_dir() + + # second invocation should be a no-op + ensure_output_directory(out_dir) diff --git a/tests/functions/test_pomm_combine.py b/tests/functions/test_pomm_combine.py new file mode 100644 index 00000000..2bc293a9 --- /dev/null +++ b/tests/functions/test_pomm_combine.py @@ -0,0 +1,85 @@ +from datetime import datetime +from pathlib import Path + +import pandas as pd +from pandas import DataFrame +import pytest + +import ryan_library.functions.misc_functions as misc_functions +from ryan_library.functions.tuflow import pomm_combine as pomm_module + + +class _FixedDateTime(datetime): + @classmethod + def now(cls, tz=None): # type: ignore[override] + return cls(2024, 1, 2, 3, 4) + + +class _DummyResults: + def __init__(self, df: pd.DataFrame, processors: list[object] | None = None) -> None: + self._df: DataFrame = df + self.processors: list[object] = processors if processors is not None else [object()] + + def pomm_combine(self) -> pd.DataFrame: + return self._df + + +@pytest.fixture(autouse=True) +def _fixed_time(monkeypatch: pytest.MonkeyPatch) -> str: + monkeypatch.setattr(misc_functions, "datetime", _FixedDateTime) + return "20240102-0304" + + +@pytest.fixture(autouse=True) +def _stub_parquet(monkeypatch: pytest.MonkeyPatch) -> None: + def _fake_to_parquet(self, path, *args, **kwargs): + Path(path).write_text("parquet", encoding="utf-8") + + monkeypatch.setattr(pd.DataFrame, "to_parquet", _fake_to_parquet, raising=False) + + +@pytest.fixture() +def temp_cwd(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: + monkeypatch.chdir(tmp_path) + return tmp_path + + +def _list_outputs(directory: Path) -> list[Path]: + return sorted(directory.iterdir()) + + +def test_export_results_excel_only(temp_cwd: Path) -> None: + df = pd.DataFrame({"A": [1]}) + pomm_module.export_results(results=_DummyResults(df), export_mode="excel") + + outputs: list[Path] = _list_outputs(temp_cwd) + assert len(outputs) == 1 + assert outputs[0].suffix == ".xlsx" + assert outputs[0].name == "20240102-0304_combined_POMM.xlsx" + + +def test_export_results_parquet_only(temp_cwd: Path) -> None: + df = pd.DataFrame({"A": [1]}) + pomm_module.export_results(results=_DummyResults(df), export_mode="parquet") + + outputs: list[Path] = _list_outputs(temp_cwd) + assert len(outputs) == 1 + assert outputs[0].suffixes == [".parquet", ".gzip"] + assert outputs[0].name == "20240102-0304_combined_POMM_combined_POMM.parquet.gzip" + + +def test_export_results_both_formats(temp_cwd: Path) -> None: + df = pd.DataFrame({"B": [1]}) + pomm_module.export_results(results=_DummyResults(df), export_mode="both") + + outputs = _list_outputs(temp_cwd) + assert len(outputs) == 2 + assert {p.suffix for p in outputs} == {".xlsx", ".gzip"} + + +def test_export_results_no_processors_produces_no_files(temp_cwd: Path) -> None: + df = pd.DataFrame({"B": [1]}) + results = _DummyResults(df, processors=[]) + pomm_module.export_results(results=results, export_mode="excel") + + assert list(temp_cwd.iterdir()) == [] From 1fcc0699affa9cd61eb0db56a4c36da4c238ddf8 Mon Sep 17 00:00:00 2001 From: Chain Frost Date: Tue, 11 Nov 2025 20:44:19 +0800 Subject: [PATCH 08/11] _RLL_Qmx.csv to culvert maximums --- ...yan_functions-25.11.11.2-py3-none-any.whl} | Bin 148519 -> 141947 bytes .../TUFLOW-python/TUFLOW_Culvert_Maximums.py | 7 +- ryan_library/classes/column_definitions.py | 27 ++++ ...flow_results_validation_and_datatypes.json | 25 ++++ .../processors/tuflow/RLLQmxProcessor.py | 60 +++++++++ .../processors/tuflow/processor_collection.py | 119 ++++++++++-------- setup.py | 2 +- 7 files changed, 181 insertions(+), 59 deletions(-) rename dist/{ryan_functions-25.11.7.4-py3-none-any.whl => ryan_functions-25.11.11.2-py3-none-any.whl} (55%) create mode 100644 ryan_library/processors/tuflow/RLLQmxProcessor.py diff --git a/dist/ryan_functions-25.11.7.4-py3-none-any.whl b/dist/ryan_functions-25.11.11.2-py3-none-any.whl similarity index 55% rename from dist/ryan_functions-25.11.7.4-py3-none-any.whl rename to dist/ryan_functions-25.11.11.2-py3-none-any.whl index c51cfd83de4848f18ad185efe7d15c1f63851e32..08c14b347373a19324c3f02c37f091dc3e7c6202 100644 GIT binary patch delta 48424 zcmZ^}Q*@xg(k`52V%xTD+nU(6?c|*}nb@`_p4hf0$;7s8pV{C3|BG*}b8dQdR~J@y zUp!A$S6Kwi+h1q|Ww~$Q=pY~<&>+I@K?w-#-^>HG>cn;M5dST;57ZU1mm&V`q9#@0 zF#%_eYn;fvJy^$I`PRL+LrC9|bZdxuPfH~Ni%$~W#8}`#8NPFZ%BacED!eXTNq1pP z$l$Z5#Ci%5Ud6IARc;CD**o?C0$c&-<;>jyzQB#QZ8kyi69MR+*=t*9B zvI%ZFpk>ZZwudaok0>LqiW;pDBT3B$|f*qm+!Q+_2mw>gj z*B&et7*z&rM#6SD9meUczx`^dhzfil;*v#(DX8Fw2|dZq(S18-2l;KUFbM^=)-7DY z+d%9==UmtL0bFv{!h@ckGd=lEymLH?12_w2kR*FAEp*zmNJj=MMi4_3j;43Nvq$fh zGb~$7!b(elHHifd;x9kFtn1GCao`thJBXoaVMqqC_b8Zz1BT?zUxtzLQ6mcL>*a9= z(G)|?>`QAMIT6D75ZStyewF&nRHkazoRFF;nZ&#>dQP^jtU9x*>FnjSiA`69^?5{gA?! z9_DJkt#~kivhraUHCG-24Hqy}yXH|-W-%MIi31ME^M}eoUZ98-ZuhTD#LD+YSz5>+ zlSqw>DN0vX8@ENKTo4-8Php?0Uo3q61AHBt`K{knF8=}$YL3F6m9UmG&eA02`VC2C z@`fe+7*z@3lA}$M%||iMK)};9t~T1*X#nylzo&7*&}8OYO>lM_o9F^dVUEOr)WY`U zX&DE@yIaO%WroOg18GRIX4!@dbc&SN5GxB|2;WehV`UB^^;oh>^J7f7g)rm^?~Cxvh6WxZ;jtsgnkFMfpfl72cPoP_c{KKBs-7IR`b{o=* zePCCAm(T3a*-FY3?MgljH7SG|INf@Ae$ADfGikD_nX#H|*+ey6;hecwYQrpG(4~!Y z7<#<){Wh#&rVE#0DTO8gG$lA7yP!v-u4myTM!9w}j zZyAxgB*{ymA`?gY9erno#w^duTlgHL8WQVd##*moAD6Rno0?SFw9MH6u1u3JJ0XHz zZ}bZW-=@!vGYpP0b8VD2!=NK9Wyl1n-w0}TR0yKTT5XS$N_wcpBd_>z3}R&f<0(uU zrbAv0D>DiVu*Q8zD^>h8S`;Bj?L92>k_=aGPNQ{1j|HJ1CC?#QK3LyPi-A@2bA4;t zIu}_nI4*i%XfD5Z*;pia=WIL@PoW0~c4s_w`&kjWmEE)zRU;fufh4xE$Fn_xb&jn8 z4h=!9d$7A(%qg{uWbn0S!>$Wig;VT+#oI{es-C+R5LjWs>RDoF%T>8> zmW#JqLT2w@r0aReg?cqs>is2cx)J!uk=etG9@l~0U$xj#r!jwcaFLbr8HLFIj`Y94 zKs!r;Z^H;51muJg6omL62qcY=kOE(>Ypr!(0&FitTJEd1TWky}h20xt8P4a=j8{Wg zhUe=|^CwoqV2RJ73H*t-X^kISTVTY}%H-y$7WXLMNqYS4@A_{{TKs;*uBa$mBum#nv)$ah+C-mD zlGGmgMLlbJp8Fn>(i5}_(+_-NOo9Lp(up z!rOoEYND(uG`K+19#X~Y@PKEkoKNR1Ue5c<(GPgi;Ov^backTYCB=Q#O7QsWc!kdA zm^iyQqf9W^`Nk7`vzQerFR!S`yjIt=LvM4%j?g3a8K`<#2Z^wE5`SL><$d}g+Y=gp zq@XQPcSD@{n&;8M<3ksDw0hWJXzx|p-gS^s=f%n#B{8s|`IgGOnRhTrIDxrwxe=)pg2)J$r7{B`PGYJ1g5wc!B0jl;t}cp+!>gwCQsvM+>NQ^`boZ-aI8x&@u7y4{rGc+`cT|yTzKx$I{GQW| ztg5nWF#Jg9@H9biC5AKRs|N>cXAZT;=qiUbQonZF9?79hW-{?+;;(9YzuMZ5ME2vg z8>5$#_S$ynVJwVrn3>oFvCvp0uZ=P5!c1{R&Ehh!y?0P$9!Kq1{m-oMS;2UKP zKU()mVt9<(Hv#&fs&UQitEi^e=eDa$sGC-uy)Vf+)MiWwZ4yGj@GA z)`vLo^}zP}TjG!PN*oW+@jiKD$n9zjg_K{-C5@~*V)vTG!`tU*YI(E%xDZZrQ03dP z`AQ8YOebPQs4vW1YiY;4VGa=pJPnJG5}Rn}g+s8!f01E1dnuv%`UpUb>EeGYVksCI zr|pPZV`n2!=(kjdr>Zghy-0!6mFm!*_o3+=9h%)H&86i{=nU_B z)`<0)z&v6k+>mcd4amb2`o5zYkb38Z>f3w%db$WJUx;EJF|Tj~p1K;`=xEB86!e!# z{b01y@QJB?((+~(#=Iq)6iltsT0I$xICdUXmj z?8Q~wFMT&by63_F|gqROO1dXy_NFz)lNW(ZEuB-@b7h1JmqxS2HqVzTWMGH zNq%!7#x8VeSD5@1=~6(=CM-j#9UVBT-CM`+3ro9dy@7|O>O|@5CfwT#)ozVp&-k0B z(@I^6#BDyS(WOGaaGWjUVNb1@7C@G~&vQBcD*l$h(oYI!%UNKHHUWI=yI8mBr3TKP z^RSnS<2G#u_@1-r$m%wK0)Kn=w7Lq+ViNA+yhFj7>`|!-lgF~zf-)XQ?v7;4r&q*C zsp-4`F64XgQM-U}SS-Dz9r8S($;5X&j=z!!P2Y=hVsL}|67JvRJ#6k_(Sg5$G4>Ct zZ|r{s5t@{W>J%3QV~()xd9Ww5U?+vdn1$+p8*hU`-p7K>>WYcARLdx}j2Q_MV{)S8 z(lOWec7okq1ioxWe9``Q0Xz90>ANQ6q3{2gJ1$d-4t#1!nc5b9OKoxQa74+8Pq2@B z{UGeu3iO6J!~oJA5hk;eSl-l)0;0Om)3;1&(v=S*6=- zeu^ndw&4+7-~dwPDf!KIlk9>3`n!?Ae=MdF`T{W0w@jEI4qFnH|0}~t|ElioEa`n` z1HW4)X=Wk3cMDR0HwxfinjMsUH46%#ZUvOW>~k+Q{;RK0Ir4w^@h%+rVhMdXf|eC+ zAAW2$*WhM*e1BiZBKnH_#9(TLa5yc#p;q&s+h7RtZkYLhI8pu|Cy@(m)t{|pS);h8 znDzF6KWiSc*Et$LA%c{ZDEhx+8w9>A_P(cos4E9VHK8Z}yATm?hy#pdP36y)pj$GC zgFKRCBKc?;q~D!_BxHrU_weT->0y`1^Ow58Fq!i3|JCj0pccZO?SUP4tV<2L)s>;9 zuFdZZx`iG8(ohbl%714Ixy2?MVTA{(bp6|hAdv|C=eR~*msxxt^Ofu@1yz&{{u6br zBh_W{llK3&rTaJe_qGRAu;W5v)IyDKl=&~Yn^Z}W4lz`*4r^$-c}U@8rp$zR0&*M7 z{}b;*#JfTiV!vfs7WAc>96erB*Z|ZUV*lyWk>Kmajt#Ja$Py$}*>biDT& z%qA;byBpJLd9^T#YU(`ux!YwxhHH4UQ}zFY%5y^WaHxNGiGvp`2&yUfEx{ST(T~HJzTh#(v|~ z8$Ek`Aw1v=T(TN%OZGFzL>>L=kI9K3*F|Xi&Z%06jUc?m75Cf^8EIu4HRmjy{hEmP z$#!9ZIW4bPhH$y=jy&ZllW6{9WpEoo*#4>-Ngs|m1s;*GfwXs|4W4;c8J>uRMv-CL zHS>%FJU#gjx(p@K%3udHX}YjYqoF+MKA4S`ALVypz=WkJq=hO+C_S_00~CH@Jj95a z3zFreq#U&mdDfaj8aGXy@~n__3*Dj|#pld5OeH{BJ=9`wf?E1#o9Ppqu|J+th6fq; zzO*>$NArlPNn$f#kD(lzlfRZx(3O{bA`WJcid&;v{O%EB5$V=|A}Fq z6)M&NSfP}khzTrT6EiJ*3P-bBa~&Xj`b*=rr~}(TDQ&=0|xj?CkjK@W95P z=KmYMSnh6NqnS!>EjjVP+Ei#vAsI_D#1nfbw7zBe^zpWpkAknDrb8dw83jcK<-Lop z522Pq6p8RU_d241gemeST_@S5@A`oQhCC;FvoZuq?}pb-+e1!tttStwv1nY{s^58j zyYF#5F*`=RG)|#68bT3D_5Sej_robx9c`22epfboL4@{l2bf?sRD(A^;;Z-{_Y)wf<2sIIj?nw?kwO`pRxLUCrYe}U6C-hFpT#oGal|hz zN53WcF>=MfiTBz+gHw>UFt+&uAJ!% zIQF=UC_Ue99!IwOANXnB_Qt2~X60F~?!0%;f+lVnU4Ind-It7ffrcyj51q&G9jsO> zzsnSTbtae)-{bg2H|=>gE{rtl7M0_fL>5b}Q%t4IrZ!3EdGje5I-sopl8DP}T5noW zY1HJk1w--2SiGcmSjKFg1$;(K<{4W3P$Bv)q(ox2z)l@$rm*ef(yplnks>uh8@eiy zrfz|gK|Gu?rhJ8dZibIx%u`tjEF%R=e8nJ5nOQHT)Gim5>&M+w@6VV3)ELT#BAv}Y z7Htmnn%`_XS4OZerK&@o6tg~Kg>8W?GYF=sss(Ipw;aCbWo(J zIm~i$ze*rm*u@y>4JG8ySQosLerX7tu1U;#ua6t@BYx09C-vtn3R~LS-v!H7pZh&f| zcTz|T)KMj&SS&rn?h?RAeUNW)Gjj5{OtXQ^B?#LOGac$1xQ+ehYAbffNx(sn%M}F2 zZ*PjmB*zc`a|#p@aD*~p4-YanL=DqtjsbX(H8#vw>|*CN%6jYvMjc^7KV6mOtut2< z<8Oj3!%gPAU81myek|fHDj_7zSf*!t+;L}Q<1?WkUb27Y;nN06VOFQ)TA!N{ zcPW`JtYn21|K)P}WMD)FD2-=_6-4`y5h$gBX%7^F zWQC|^D3H}AMttCm49MJAYM~z`E3OyV0m}Pz>I9+4l%Gf&9%?OB=o4^Ex4A8TDVC-O z;a6Vse|TX6B9)(GpQG6KBbf!Ud`WpGO~*Xr&+H;eF$Xc_u*%%dkVo(cU8L39{^*X> zjbhBE#H!Q)nr;lH28v%rX?`P%DU7oM8+=2dozR8zYBqI+;2bZ`UG>@#IWB<-f z`UXLJ&qTTdjNwR90w9<*2ddOXra`R4ZNK>S@i#ZsG8mVmkKvVFsWmc=RTR-%pBdJ+ zNj22w2v2Dv^l3NVBHVGgh+-W!3A#R=?q4@%!;3rtDXyjKn^!T#{WSCK1h))&?=!=j z4OZ=*MD@TaDc8U`42o6y6BEpKAzi5!a8=cMkLO&*hhqV+7Glt~#utGb7~5wH0F%co)6jOhxAJuQC!y-KuF;N03HBuD~Y`x=) z>CN{65*y0Pr;`qay4E3_82WqyNeARQM0@4iu%u}-FrHSJ`gTJvD-^besGr3G`rkc+ zKy`=hS&@I63spdes1kpj;TnH8kK$36@2z;@4c9h5Z4rc6=wu~?1%w43(<=>TG54sh zym`5HJQ6tX6XFne!dNHe>zV{fx{S!!G?DFr>x{PI(s9qlxdq3t*#8s{!n&zs=0ez&<9wu?849N?p^PnFFOpoK8(rT)aP6S|F{@QhmQL&l2wZ}2(YgcB z@%0UNmz9<}H&U{s$0ngjWGZ}&bv-VY>-~i{e7Wtp(+T;LQz7TtjomUY(|)b1*L6_l zyAn4T3A+04bGv;5_Z;!N338u~3!gc);9r@B`JSz^T51wq>q7Cxw_Aj|!2^H4k_chH z$kkVVG+UZN(~AgJThKo*Jg&HCvhy8ihlqxpJ@!hpA1PM2c2gqCZ~a9YGV(NveWH=U zpROst1$}m-#^yg3vX{s6O@tolt^@*{hm!M4KD?DZCHxb%(eQiunT6GT&U(8S%C%y- zod^_v7d^*^Rx14hEpR;kx45v48{@^SrmZ9OMYS~M8ScqEuccf9@zELV#W)B^R>gg! zTjiOAiy%jVrC(jgfce74jWyR9{fmO5+isqTMiBE#wt<@=W@%(RAUfPLj=3^qBB^bD zum{zFtxP$MUm&_@}>e<8eg%7(}Q#~Z4@{Uemz14v%c^Pc6yqY-pu3nL%}%U zKiQ7fdpTz1)}u%`%!=eT3kZ0l`DAL45o}u}*qp=^C_7-Y-8_ZNguVNQ+Kbm^41h5Nn?v5-bHKbZ@PN>q1TQ zwg<)^yqOKqNBVF}G1ufYYrQRt_kZjusvJ~q;8e%O)vP!OZF`|W&;5{T z9OJB*2nw=aOhvJ4s*m0^b+3+iDhU)giN_EkNy)829Vve%yF!+`>0{{YS^Y6PPKk~7 z9u= z_s>Wyq^ZIF>RCvqDOGkcgV}s?Nd9Q5`t1Ozop)y5?YJg+>h3atDWH_haFTaBWo>uS z;p7VQ@YfYp1;_trr`Q!2WpEL`<^@mahe23vY{q_13=2BVbxQ)bAggqFm!z|`$Zo7< z0joLIAE(c@XAU{0@3d=1o7#a>L1aUHeKVQU)QYqcVAt`*Ay_c9vj+ZOH50t zHA16MEv)LW{jQGy*&_2GFLDphLGc|{z+`n&MpYP>H0>It0iHXo+irZb8g{)=^8 ztFEFO=PzllI7q4BpnV6ANkyCb3?^GUPCaf)X(c|DOL=49K3J7KK^{`gNq@EQwuC9b zx)Vi);CafGzyn2|DCVd+vQ7~i=WNl8dvPShsLL{bw)b*Pz_ZmY=c$X(J+L6Scg*0#wYxH%@+&tPWbRDNc|6`OHM+~q zhg*A^Qs#ugH7hQA=dvg0TRSscW+pt&9N_InD|Q1psfhGhcob80ofGwq;VB9q&s{{K zrxo_PYuP4BmR9@VP`EvATxS;1rMe|npR#t&2k0gYq?OyL*|lPE2AZ+~@J{VK-_1J5 zYZa#~##F)QHWM2toCVE&hszNC`QhH%I<%zbD9Yw9A?Mk_2T9o{)#!PtNI6%`Q15&d zA9MlH7W&eMJV`S|L&pkskrbyh6I9TU589P2e3c zalIv|vt1L8#C6+L*)HnB_1L=Wqg)q<`)p#Y+MXEv9Vf?h=pV!S2{vKyhY4!Dig0si%d!RLVGkzwAt+7a>F5*7CqR*&qgBjY-?Vs66u z$q_4OI_ZprbGQ5mp@fxwejxq!1#I$a^<*AWe-_1LJL=1Fi*~W5Dmk7DC)|AthLOOB z1x#hB_+p;}Cbk_-{G)A_L>Py}+@%@Yn-v_?sbbd4h%Sb%`ajMtdb^VN5YlH%Ne;A4 zutWA{>p}d;ED7C=7Zgg?i=dkSSxn7`Riq{AQg~=i&Sj%V?OD9>(@E1tkJ7U==O?tM zjh;03Uq!w2CQr3H2XnKQ3OaDM%S0-L`3$8)&69?({y)B6R{%VVt|uEWg(lftN_m@B zTxuG=qakb}-lYtw=eQWO6V^tO=q2zQ3K{vbg9>J65=y$4|B_gYP*lX@4Hk9Yp|9)Y z7tuVE%Z)2AlT?B42YGeNb`%i*{MQ>U5_Rg{uI%kB6aCihECWM#V7`HlGl%L()5_8? zOm~qwFI&F=0zh$4aRpogRJN`qPAONfv24B>MsG-BEl>(^3edP&p?U1RK)g z1=P~ip1rz!Rfp!q24&6#ooCC{(2id*6Z9F6|O0?w2*~n?I`w0$uR9QX*c2>n{YU0$n%chLKKSj$OA0MbGrcWTknM1nH8z zt8+u?&$kw`0(r%biSet8pyI{D%|a=i4+PS|Tq7Tm`xkf2xP^U}N)nCR1N%k{7s_XZ z(&7&HZ^}F7bCPLktd+8Lj;@oLF@G&e7Bdj^)Acz*_?xGw&$tS=fmkXtc5hFzk~)Wq z!h3c_FrSp~o314<*4S|dW5pDKQlx{P+1h8kTs$XMYTLy&wNL=lq<_Kh$Z zcKY40P+lXl9+iCac}pGcPFA@K3O)N%4E2lgX!2)Qf#LXAgi79g5RWCXRcp2}RmwSo z8k;Z7f)OMnZ<*k45jQs4_|hl1p{-Jk9H~CpH}{?45lti^fMP_bL}W$FRg=VRb5-d* z<0sf&Y_m`L_Z5pq6QG|FtS;_ZKu}7@$8LLO%jK$&Zge$LS^yJn9C&2|5Moos1EF}h zGxplf_8Lmgc7#g!;~J+AzHRVN6izFjUjcx+2a<#3cqolM0V@{92sW&Bh7>qTg#1hd z{d4tq@HymRqE}m8Q~ zx5y`A|43`UU{S-YZN6!v`D;?=3O;4UcB6pTvsw(W8)NJk3`3JAtB)PyL#H|?*GMpf zsl{N!vS=+qg{R=8j}P1rdokZ{7DGYA_N4C3R~~o=gW{ZI46Y87DE1bfM@2o}Dj(IT zJH`@E%MQB;=0EEAV5IJP2T|2IWfYJZ&6{I78mBU>Wo63m7~?doXQcz53%zAQ!xr`~ zJl-KU+Q)ploK>}7>FXBVoxqxB9D=^y`2zi4KbJ{Ff_y0d>0g2O7J3eX1_4o}1OZ`4 zs>Q=hL!t)51uk^#9oM)~Kf8NBp>nWV1GxEoZhZH^my5>=gzLwKt)qr^iIq{!?`R`+ z5@mM@FMGD)eVQ1wZT5D5_a3T~hsTAn745JkV*$RNo-fa9A1RL`Kbc^ufrc%PFBfBN+Y`_29Y@%w#u#Iq!3p>n5h_)+>0 z98eOu=%ZJMB!Q5GkH+0x5jZg{Xxoh{JsnybfSjEPNVkM@EjBGwo^pJz6;slyI}LWE zh5&;o3H!lTt^peZ0i&Z1U9xcpJM?clU))PlJ~|LMfgD4vhig%+KYK|aa(s*_+H|q1 z_EVNlL|GycKR(Qml@CRIr;_zmbHEP5)CzJBA<3H|nr@#l6Lfy_D?(D&-(&J*4GB1VNrmwy8?l{h&ZT>^zcRLGCo@cd?dai4 zqkR)wX1teSqhs7KArC*(L&Zk~D_L_K}C#Zp6>XW{=we)LnH# zK9g)hF7`O&nA_Db1+2n1U6Op1RYFi%j6MT+@>A;6RWX6o*~dUb(;jU@}R_ToqUH>(blOc+F+x-b~`{UCzTvC8LdP3Hb0 zr+!LZx1G>-(y$cPADS!gqK>=3i#Mo~LjI4BM=%iLpdQ#CUz(uXIH^4NJYGMhUc8ub zV~)PCb%^;qOR6kHDiMV4H?hPCaYf?9 zzMUvFlv|Uwt1d@0=ergE)<6rNayF1MZBgdMIYs8G)txt7Gst3yZN0bv&K=;oxaH;7 zXf9TS2yC10rrZ6|uS<)5Dz!|&?FSgJWQuRb{B7|ucv9znibO;14uA^No&#^*L87;U zw3VggcKdx~e1&$Sq2;szF(l45DfLnd`m>m79*;m8)b0{uG*t@Ar`jJYeu_}0v+0KZ zzL5Rc|3GWioopQ^qZ!-b02c}ZHUNJ zILx(TON;~xiY{*%tTvu?gJK;&L1?-UV;w0Ow_ zF!SaXq#tr)hMP2oU}B!&{wD=wu;i~FZeRQ zr}z0x$Y+rqu~L~gS{3Eb$UJ>6(?vRyZ; zC56+N#As-mM-Y=ZTCmM7LjyrQ0RvD42r#^Lw)4dM(9D!YY#UT7(SGvxbhdA%c6I>5 zLPy8X=;(-x>e4?*Acd78=QPV=&$#~Yh~1Wh z<;QH5BknvWsh#_B+s0ApylQ{_`y%X|7l>Tz#Bc6F4%U#l+XqWaZKuX>bt&_M!zSk* zTH`Pwh#`p_f=%-IJz_LFhqLMYr->89BN z`TsEBz5MDq;88F4;gc|8o{a%x49+*g{sTE%yU<&e|7HXtb8r?C#4_~=oV6RNUmN2h z{wl*ur;iOG)H0@HPkp;oY~ab3G>TqoQM2;2A$VMp)IrW8GGqyYnIevMoJ zCSM4W{(xb0cV8L8ZXss6iynWpNJ5Uo%H;)-^@a)uIy-V_=a-~-WzB!%e@j}sA5KbI zfZDAio}h87q-i%5g)@Cffp4fnobg>0EqBxYDjLi!G7*_=*MN?&B39yG$z?4&9%=tv zp9D1!Xftz9GiO^47e!yj|K=)OSbAJ7YL?1)s;=Tdhoh{ozmTm%cc(5vvPJn3$_PxHyS6h+>Mk7a0xF?(D3}Rg0w$IpKZ&GYChZ%A|UL_e3oV&-_Wl z?80pNgUYCqcVkBY<5O)Q`F1*+{%8X+zN#q#U_N5xhfC}jN79E{7ZdaaBYoLxNjA{#;snYyQCKasu+Iyv2gK4ohl@HatE5$&R zP9NRd#s8K~*?ez=2=i3T#OX%pfAJUX+x*eFPDl_C7V-oooHQG1Fob`cMfYF#EqVaa zpRfEE3M4tw5$m8TRd{x5HuQ)r`b!9ZRIA8TZq;I06~%1B9@D_uK&L=iHZXiuYum3W5?LlLn_ zl!7Zz6XtSzROig{Z%53y2Gu9T#Im3oq$6qStUPxSyKZ|gdjjY0v%ihg{J3GISg;~= zP@d)=OiNk(?)Zns;iOii5&0;TmH}bT0eILDaauy_FwQ}4f1i0F`p@eAHk6+mAt(p9mJKAYR)2r| zn&lj%!3HXSt}AS;8DOse=%`KM61gG=3nbLt~b8UxyE*<9TbCq{(?cp)9H}(r| zXc$Oph#KfSA>lu?THR>dhH71Q6LXS%#~tUCe~6p0#f}@)%({$Faithkgk3AjxLjg= zGK$Vz4(){*?qUE}ma0)AW-vikCqY?_D%Mbbq5vAzxApfmrP#`MkhHq{K3Kq{3p`Q# z$RycXE-7%Hk^AzAS1{QVw!`E{ou4q#Rh526qT?lIu`fhdw;6jZrpqo*-Fr_nSCYds z6jQRLFU$pDjvtgvFG74_!rS9DEFf=*=5!a+#CuxSiM1Nc*xD)^%(#gi+h@cNEo*c1 zHUsgDQghl2(aktwxJ;H!Mk~Xeu|FGhr1h9`4tiTOsPX6=@%-I2zH<;)Q|KY^ZD?Mb zIBEB!ry|#bc39^xr|t|p{f3#qDC@~RX{zzR_-hWe>0ce=;A8ER2HPR#-Kv_>C6d#X zCfmV)?_S<)uj-(QsA={PJy^GDQQE0uc zbK;t5=S{6xQ!Do^&(=I``%<^|^6^Ojtc}Q|vN;O|9Zvh+%!iGg&9ry!!L2N|BB|Vj z7Kdo7V<^uK-tUh0%a^vD6K;ceqS_Mp3TW4AnO~_-Xr7!~%sdG#MH)f!GSBK1C=qLUAB;dsN9S zx34L=rs-_4VxQ$ULMOc_E@`Rv9Nk+M2^7;Lu$vw5Pdy_JVPFv3*JOlZ>lTC=G=q;a z-YhKKj`)otEgt{lFRGm^Y2p4gnI-nR%zwC)`uMf&C`|zK^R~cTH!Q~A#nb8|3NY`e z9pCafonb#tlTkx28$-ly7~x{M(0#wGOZH-ZT(? z#3h+I5O`QI$LMbv9D?6bbJpn#CSG8oB^$?9VqFCy@m&WsHCXb9C)5F*6NA#|2kgFy zM^5DCKF&aQKmDIa=eeakA#`*cW!Z=Pn00cHKw-Ef?fpC#bau}ZbhCdA+SI1T^_&6b z%`aoyh3BuTMw4@bzAJN67NDw*%c>OibtS{F5MEl%j+0}Uqb`w!ZdfN|c7}&%CC5y< z%s*-KTNmohhkyx%av=^OHbQlSd5Fdgd!%Qr#`|_{`!lX<}b3Je5$*Q8LD=<@!|B^u*iSjD8Fxh+APZlIF{2K|rp||E=*zN9ipD~W z7zpFI#2_3#UZ(QGh%aMB<9lC}B;l+fcn?ks+_tO)8!_%j+#K}F2OP~Wdm5>{%P(a@ zMPXS`3bPI}Lh^uG?D1{biB6V)Zw;uS{WNmwk_>P`x$NR_Kk)!Kyp{a2CTTORyLEZloV?46u|h)w>qa>(G~q@I>C%y6@S=bODCWlJ6ITRa&Xa zSV-06vfgWd-Rl@E>dT-%AC$Dy$6oOl4r3UNEX4Hae{0M9LLVA)e*GIMA`161_)S&D zt;g7Dz=Rnxzbr$%24v;8KB`u>&NGk@d>|`fN8J$UH?nWC56MS!05PIOKTnD=(G+3oq(UA7py%PCBt&aKz$vx`samjGa4=}rBc zG|x$nkeQ)vYg5AseO>2v3__KpC2`L&9zpY(z~)H3%2%ZKPq4&$Johr}ua2#`=HZrM z77sR$uY%sn;G3yWZ_d1&sXPI+FE)$azYW{i`BfK3f$f7d&L^bv_q*PaBj238){w*t zT7x8*{1L2525=8SnyDiguCoB6HGOT6CQBahLQOZkKPkptM3X)+$ zMsJB%aiGM;8*o8T(cL-+VsqhaAusXrZsxe5PVh2urU-{F7yg~oEKXfz`4L=`ABf{h3DwC*t3++Q-qu)zG! z&NquwJ*mBJT7i)6-UTT@Dq&%uTl@HstxM*;D5)c|^;KH1jW6e2hm7Yu#k6I#9%`)N ztTREe#(vyPm!>+zCKNC;UR+SDFC5q1Z4X9Q|B=Cmss5=Zfl;s)AdzxvWvh@8oUtHy z%&VwonP$o6RMxN}ub|#xRvt_~E%mbgude0ydQSgRCagfV*~Yv*FUGh5`|a*1-0u|w zN>;BOM7_ex>b#sM!|J{DSn5^+-RHXCgy^thXLsz!6@bi4hz*$g8MRYsU}Qml6A-B! z-n?7g?ikO<<#Hl@CknbgCnBRJ>g^zc#y(gRDv zzzH49yrJ4?1VBco)fY{a6Z+}&UVCn@ze?O4B_1R%@A%Amuf}o-_i=q97U|L7B^>n| z#d<8L;ZOt)B8s^6HTdji58B6zn;%DpQuI}_&$9Q?){$9D`W5w{ub|}b)UU8Hj~2+N zF{>-m8nNt=m`IY$(=urc=RRfV6Ov#vY&K{Cg4gY`7~otEn!S7`aDzt+%L1f~Ti9~` zHZMc=a9b+WD>X11735RR$eT>jui(0qNAa5(T6fwKt9NUv=X44@Y05pBSO9jfhre&_ zuXVCdhV6;b)Z`$CG_R2GiLGC8J3nL~$1li(wF6eZV{OTOI^~kP!eSI}yr zWW`fiCjx#6`RSU+7o$;n+=(WBXg5#ypnq}=CR?3LuCq5r`jkH~g$aC&+ zM;sHTXmqDT)y7-773ff88+uVGO9q*j){z|B;|Lq7N6u532)TATgjb7D0Q?9~n<1jQ zlfThR@|V0Vdw8hnsR%DV7;aW$whkSDYPa#1KCpI+Al&FUiKJ%yYpmariLry@C@V?* zgT9jmX-TOcuW^|bs#bF|Ka8)j8qp+|!RO&YUdQu{1LbV3))AyucfGEDHkLv}nwK2kOS-*-XLz%U94~?mkZjh2c zXTalaR@!|+tGN@4L-cwInv^+IQJTg^3DVNgtGb?3RK zfp33PkKL03MQG$LOoB;y^+J(mi)Dmq^0T&>22$v&F@9^~8`c-Z|E3@*=;yOq|B~$~ zG!PIjkfh&u7)ezc3;^gYQL}EopxT_^eX}{}O(nC@NbCVQ^w0kMRC*STZJoh;4mrt&*@$EOE!Ddp-qdo8I zO1`>Tl!?shKU$eecG*zKbj>DaIQ{EcJh~TpN06iehSX{?ot~iLe_|3^*~%8j-)bQ` zMsQB1cjsGo)PR6Vba5Cg@o8bJ3NrFo{cPl$&Z09`12Y{8kvCv zn)rD#h=ck>*-Z`uzf{OIT9?#9QF3(P&Q90@MlK&O-*p2-gFY<(ZI5IgfJ+mkPd2(~ zcdK+aS#YYR&0HQX;fD{jIcWCo;T1y*?#Fz))CMVZk@!LW9d$Q6Ot0 zFi8kJtjUa=czeIK=ob1dI@>YGQ(+XjO5>UihJfO*Sbdmg3uhV$?@`jy{da*l&K&m1`dEgmAhcE?GBszGrzXuN4;n+4JS^J*4oa z^;@tBM^o?aEgZL3z63DJa*&10x^+z$Qot~tQxvCnNjUu)ZAJ2=y{r4hllhi!(N!|uy{0-asbv;s&ohvw>(ODvI}4KJ zhInBp>h5IKCa`s(^D6r*R(WQ|b*bRv&WKxYE}usOPj$YEQ8ZksU;uVCoJOBx=@8;r zS?nWsEoAZip0Y2U(Nn@ERU{TU5@-?o*f0Fe`7vh0YV9zM^}7h3PvRV+TSzU2xB8da z??V4}KKF`pf3b(`K6{vprGjm!Mxhmq)tUTn4Jx^Nntw$6OEiREKMsu@LJ{jza3A>* zs^h7Dy|m#fcI6~wn#Y@pzq;?YimGf8wSH_Qn8x5gKt@zapg~}`$gf+*zXERhy|_fd zaLTZrF^z6GxO8`@%y=rLbX7U`^GpDeo>@^Um+y)q_f}uod$+g-CEQe337W!^!Qq)bE-X)6fomJC<_>K`?HRoQT zK5|@FVO$uXLPg(@SVxyP@S-%n?gegqbNH@=7V)-Q! z&n@BWuS1g#&ss*kHHj;`i-zHpyLTT_aAJ9@r9gzo+_hWsC@-YGiMF6b7GZQHhO z+fK)}-N_qvY}>YNJL%ZA?d0_L|7VQ7_g#%z&$BLTRn?p|OT1h?ELDW^FIG{Jr$kds zGljC)J*7JZ%WSZWOQq6IkrpWpot#`=l;S1st`Gcv^E%OpxbAO$K7h=ZtkPSgOulDQ zuQFxwsRU*sMX{Qp$)Ir+Z8G)uA;uvmFcK$~=3PqW@TI(Vg!+iAy0tnxBbxSLmvwIa z9Gbm+bcN2WKs`|mD8J@crRtLBhq<|5&R{Iy{{^eHPq%p8euDr389)L7;rtu3b@no{ zH!yD$(#rq=@kmPCG%yDh66;SCH)H|J6cx}()#n7pPi3M7flACX#6*xYu+hc;*PZ2p z0RpP#Pn}l=M*ZKzT|-QOp5g%?MvvsapjLOjRB_+bhjnpHhqv~GB@OVPh{rQX{w{^R zP5hAF#-54!N$(B!j7LTwDwU{xUifT%?#b`hRVSY88D`k#B7u0@ndB5jQwlPt5JGU` zyEJEV18W8magt#^5k2Z8l1nG*B8@MF(#G!QZuUeHq8QdlSF&`#0TRKkiO8UmrFBuG z2Eo$eQ@M&<@pOZ(H0oE=;U1r_ca&m}YWg#X=tH_q5|8V7$@EBuG|!WEY}P#agh}j% zF2HP}lju7rWH3A`FaAD6tG1XBnP8>b}d6r7ZAu@4KG zRb(702@ua_2yFzQCZxg~xB>>2ILHO3f%Uj4CuD{M8kHaa8Ec2a#?BCA4&2|JNh^Um zWvN#e7euICE81c<)p{aceM=3n(g5(1!JcZep9g1FYW8 z_VYsgzFP=DbagT=DXh~%#W@yRCQ|X;l#eu@YIEYLUJ{BCimc(Wd=g{eC5o@Pj|S4(@?NK?@~gy2(j5H(v%A$c-NnY|V9Q8R;1;PLdX^wo}I zlwDZPQy81wFMIvV`9iHibEo8VV-=0fMgj!zJ{Cmf-|KQp97so7V(BDj{fqIhm`7}K z#4+$jP-s>;dTj08=gbtQ+6wz-b^A*DzRTFyRa~m6L3r=(J(Ql5zzOl|1NSh%ol{607Q!|lm)vuDS8BWqk?(P6kYe|?ju{WBJ`i+b8+V4x(w>0S1k@4$ z!Yt7Ni!3c=qm;%+*Vf5Y7MO4pd?At;oo8cDPx>I^N-k;-w^i;qozY4?AU88X9Wv@T zUE}C|hbj$%Zd;a*5X8f#FAaLGnA3=6I^t^{f_{u7CA(}Gi^)cIpp3t4kyoP9of=@g zfRwi^%=?&iyT>qW4qpwiMpHMEcDW1q#X@(&d{=r*QHp0>$>W(?S}G>yjlFmO8NFxL z%{ii-^c&l;n}Rjig&6ewflvuzk-Kiq-dn3KZmKwJDn=D?#n?e0QSpNDV`yv?lK-iHaDKJ$MZD)M~MosF-|Qf zuJr0>Aw<%&?dziXwvKe9i5qse>Wjx~T5lR2i8O!5>f!$w-CIvJ1mC)k2G0FPoD*UP zM2Wo#Q~Of;$6vS)mT#uk&#ywYS_3E>Z+nX^m2vs z4^MK_5I@#3YF(ZT{__%4X9^2|`cv<4;O&J#C<+2}n0{4?;i3yA1d#zVGbn)79MJLRxde(>Trj*7`_1}9U;ULJx^ z0+wzfh2xTL+tOJ1Z9y1m93mFRR(C~7{?f1myZnCQhcML#Y9|TaQy_ixMSV`um4p`- zS)|VhWMhfuU{?mgZ-Ka?5RyLTwevS~)<{&38SDc!YW&IAAJTLL9K#c&PKAVDQS0Z5 z1RStD*gKFexUXx|exjdAP|O$=oT!{KtkwEa`dECmGJ7-F26>r6KbfVjf?=mZhRRb5 zAy*bn75^2h_lm=_rd}e&?Y}SQPEmBkdlWuvFmCd+rf{=(6)}_8yZ?UnES%Ifp-tWnF zgh&N{m*s`y)nHm!!NhCZk|TnLl-;0iQAu;U+%PJXop7*qSg;9$V&9&E+0)*1_@ zpR1iWmk9LUm%p%W&gl9vfKtE56Bm&n4@#gZM}+6*^_)S3g2mz($p6ZA%{fDi1qq-& z<^Pw{?zPE%aDISAEb{yYM33;;-+31sNmMU5DX)$&d2uX1BBbcP#Q-z3q4vBqt%@#% z8%<4$W~Iph{N`>}TJ&f+poM9U>6su3c#V2i76*2gXB>!8V{4~LvkQP~PBpu23W!e_p z9>okMC#vNfdsK>P)s9!Fjbg$uXjJ>MpzOtrd-}2<@W*(d+s^~TvpirMyAHlfbi#V# z?I?6X5e~(eTrjB;|5jQ*m8V-Ri{3yJw@c%e;H34IOF(H@D3PSZ{5ad}$R)T~8x^Jb zxbx-!{G-O9@`e?YgFC?dY$^TF#aaOa~^l^y7VTG@85Np%KRG88hTU4b0!bz=sCC8851%O-eCt*?@h+m za9DKJQCjO45l`^89RErS^tvP4HO_CVW3oES`bmdaH_n+J`>WPo8)&wk{NWJ z8Xp^BbsAvu;>^j4203Gq#i1Bm$oh)c8=Hsv%p~kk*TPF^v+cW;2k(TvgXfV4h{%&Q z+uN;=I>h7pI1*zSXe(n#yE{rp4;ken@9gU|7Hq*-VpK8?53KU5T`X^2i%1pBVv6{& ziLA?J}ik1b3pl6dI0ZS{M%)4UX5}guxh)k=kA%=d;c_AD9%OKH9 zbY@7v5PBWp_EvSxXqSD^GNZf3b2Ge5DBBRQpQzKt>2gA1-qzl;LHeT;OGnl89JO5X zD(4-_A&;%dW(oBY{5Zl)hCV5W7S_3qy2Da#6p3Ge{nj-gmRmdyp%xJ;Cw9XI?D3E` zFQyKN*}9+&YJ_W7r*ffm!+e3bhrhf^A^7!)%T-v^uM<|_Q-v0_Sb!qOk&bY%)Ugla zWmGZMgd_h)`Ys1(f_Yp14lUL!1-a;s`jR$q74FuA5qnsIpvfuSuAhX4oTq$AVh2V} zAIFQY9|8wm5Q3yfphV@Y_!TfnFi8`P0&Eo_@se@%HQw`H!G0k>$huoZ!C9=3FArBP`JF6I5L!a+9Yo zBt0Y8an}VnzUywOo+fj8Lq(yjW}Q@HT2YoQR&;t*&Q0C@+ul_|ZtUHI?(Kumu0;le z8zByB`LK_~Md#cfto1tZ^*YEVz0brioL;1351+=L|4Z|($XTMX*~i^ih%9lh6eqDY4jxdgyA`|Hf(+>G z4QvxdwyQ4%v(I5@1Nq~l5Z$6+&3h(_RMn2sT$+`lCVlqI5%cph11BB6xN?oXB>GGj zKez*T5;-`Iz97lKsouWyi?EkLC!M`G;rODUYN*6+Vxs8jZogp8dX`09eZWugjl<-q zN29}N2bP-6A*rR%dK<6_%K!5u`Faxa(HZBnZMr4cT*pe&Y~yx8+1KkX^1(dajlsuw zG=uQvK&!JSxqWqU+-CkW;(>{Y*-oXUJn=hdb-Ls@19Y3F-SF13I743F+GVifFIz8B zmBGp*G!UnV?5i=(^mpCPp5lu(pP71Q?fKPJ=Rnjf4xCs7v$M#i%j{%OOI zzfoZQQpZHX=GO7a;^}U9tT|R@hGM$xW_r*NAc6229l=(`ruxBW;L6{Q1~|v+!R#4= zFm{Gym+{sfFSh_%fBq+}V-m`M(8uMehCm@6Td|h*lW1^hH21!iOJag+qr45UtjVMK zJN7h5Q8!;dyjpMV;xeMtRD{1w1Gs4=<)-T3Npdo?#Tb3v8C2dK(Fc*4 zLM4+r90v_s3o?O)X8ceL4U?p2M5-(VUp&0^*Y+fabCZsPeLD_U1hPsKEYORV!~dPojvIF>o5M9U~=g zC#;5!ut#s~A zc!~JzRy2GTiO-rH6-a3tn={S~IfzTQViNZx`YRyNoDS?>K*(`?Cy%g1kcq&KY~4lu zn}LIOPB(E5a3UgovvOxPiyqgNSOV@cvm`vg*-vd9NPDsrKq&19GW6$E8`|&{g9{?B zEo;Fs@f|8a(W)2QRkQ6;eyEC~y}`f_el*pPt3>$3v|WpIZ|PAC=k{WFm%YLKqy_}p z%o<=|(y}*vYPR@Mpg^ZnB)G2pNQ^NLIbB@tNs6~3Id@1I)gRGLKDT!>gsQr+BXk6} zszITpTeuS^PevfPXYQ>RHWXmXlx5|23zz57L?>2oN2aR3B618p=UR)Bidh_eLIgcl zz3k89`|!0vxV_cw{q}9&j1#e>iePFjzYMSyGC&=F31qYPji5(i%djQWv@tbx$5J!J zJDG2B1V15%OP8W_ND?K}g2Z;>{#MD5qS7Rr5Q@r)QN_gc)}6arUH+_7PaDnV zU6UkfZIWzo!kX$bk3#nj^FdF%WSx4DMre0pvb)bpX;6}Odb5v1-KS^iapMRocNzR(x?=#X+~tUo$B%REX1si#{RZZTjJP1ket&t3YK6@x54t$gZ`DpkMq2+p=n4oD)Z`! z+Fd=(flsXXXSptBJ73gldyW~B0|o&2dl^Srxi~AuGb5lh;$i1WJcqXdC)a<}$1v1U zC|leBNwl2p`xh_}Y06H3OO{rDD%?`}>=ahPIho4YTl+3cPZEy@8 z7i8aeXbWT|PWU@;zYr;MCV!`0;D`Pt=1o%80l9$CB-XKU?vzeS-xMrH$QNKi%wA!R zL3ip;{51e4#g`ln4|DA}NXw_30&!^cf>&mL3g>N-`d(}mfH}}mGEM1Htcbu>(_C*8 zQnj_0N$`TJ%|YHy2x1xA-iF`3AC3C^wMs{34anx8ae{WtIWq`IP+n)IDrKV!;wdYN ztosxs)dlL*{zKH+Vmw-4#0UU)YwP%{^xjW}CdXQ<5F9nKSuhJ;9~7lB25JiUi+!9Z zGEb;qmUPu-?!L9cE1LT%ds#;EKuRe8IcmHw(2rRMyDdu1ip{9ZmU{MKC{aru;UCTP3smEzpYK%?wWAu|zm`H+Zn&@D`_F};&`v2nK6rt*pfW5Ex0YLoRQ@o^MAc*dMHxMkdY#=}KQa6$cTOkx07iJ4((Z!fgo{sMjrGWsK650E$euq;Fnni)5L0k3hz>TN9=Q&D`0b)*(fw5Pu5)o!=GwNam+CZ7QCL7+EIT_m zpUlyVHfr1k1FL3$QUF2BPsYi6V*2@u=>0S6D7_sZGaa3d&m<-IjsB}y-W1P8EsICO zHXCzdmb!7sGiWfscLHi98^)9PMmU919jD4SwDB6ovkc?Uc`i&fr+KKR%7(?;pIl&oavd#XpT(Lt}~AS3Koa!s#(ou<56{e|pXP%U)ccKGo!L$^7HL)#EW zhs}Du&BxKZ>f4+1v@^rA%=SKeU3V${*@XXOwiE~g9RmQlc&nntWXu>JNCsY}wigEQ zhRBG!Eb^|rhYnAG*jW_MK}vWSvs)kSw)$dd>e)}QvslzCTq4jBB^~5^=*5JO9eST@ zy}~{d%s_K$14^sY;AtJ$sK--xZu<4;%J12LWnz_`x{?(?3Fk~H=}VCq34hRhZt0b}_H($U_(4xOBw z%nf0PEWEjb<2if(gySfL_L%MmVqQL7 z`p5F*?WXZzq|Z8f7-@0X1G42A+OP|6Rp9Vl_2#N6p7aOjla*m zx|>ogm~COnoEbUzFA@|cIn)6snBgB}Q_q4MC&xL?uJ@%bh}SI!@+slc_J`T@d}m-UhwJFVV9Kjhs$}eq6px z-l6^qN<9~Hfz-#_;O}!J6Xiq#X(l2@5mxfgg_p;{oGCqoFOuHM2~`6?%Bur?n5la~ z8oEu!@VPa$83RhwMp^6&QVjO-Ze)__ixOn}Qle9)YOiPUZWki-&lCt0$^wA(4>*dVQ-kd#v@ea%lxHk=qjJYDWM-VT( zIV_mTR%g#s*h*LSqkXJa46T9 zAdIKf7|I*z3SA#Z^aT7xZ<~Q*px`vfqxZzoM#$xl4#1db@lmg+I=$zjbBB~*6Lr_r)Kp)otmKW=0_Zk*sTw?r13sG8 znGe& z)E|+Q%bnvJ(n5NFqQ+7Slc$$>tBmcgbW?m zL5P>6e1tVzgwvdWPFVghlDNA}as$+Q_0Zb&i-Ph=PPajW%G8G1>82@1X%yDp{ppMs zoVmWyX`WQZ_;323G+xB`c0|IYE)tL^y%o4Ob|Ij$vkomkDuy>`+>so5EfJFo=0t4X zZxn-UzTbVjiDcs*KZ5lrUUx=c}UE@<5S7=&{Gxs4sh!*0r)!{URir?mc2 zU)5ltx%z#oRa0ojX7Gc{n7Biv4w2{vQf;jBno5h0K9t)%S0kKTCG-X_zAK@FWSP&R z8@{%v!ikcX=msypp+)q)gVorOf?c+YLFhg~m^+TAp_Pom_QzM&G#z$+^K#nJ7}n?A z{M5DTP3DsWzT@dNhUNX7c<+UUy}mEU6B&=A_>E`RI04B2Xt>Ty-zerB74EsRK$!t2 zzML4O6MwK^O25&La{r@7GD#Q2OaK7fGMNvt6SNmWQ%lGtF2#-Nv`GZ9! z|51yODuSGlpG6%{7J6101d*=|`R4H0A$f*<8ul&)ST)VTbaQfbL7hV+_4cdPkYqPx{SYKjlm2@ZZU zU@0s*O+|-Qaz{f;K_CX&=(Pd1BSCtD8=f!t?H>7giF)D83UrcN`bWihKMvSHg%TEGqro8f&D!-=SfMOz%O(wPdSwMK1gMdyH-BMZ0cm0bg%hswt3~2qUUGI;I zF_MJ+4Mpao!zT(uFk}lYv+Fc*AU?DJu^Hs6$3X2-3e>??<7NJ~VCY`Y0Xs9;Z8o^S zTsW4o8!V*k0(^)3fVNQL&R~j4fS0z@sq^sOf^`GW2@Ak@z{!+SD9|PY=Umtmmvo`V z8mRvSboU7N-kGfg`bU>P_bidkWme>oo?DWvcTDN`H*&tp!LWFB1d$&ye3Cl=hKM$u z@eG(;LoV)8!p)_$6spG(k@kWJ!onotf~O~vE&+q1l{HOXbpD4N+s3Zgl|XAIhT#wR zZ(D}G;#xq6lXo&!iWyc;)BR>M^INuTyMg%ub-+8AHFvnCLL@p_Ob2*WqkX_eS)ljc z^kVY1g+k@%lc{d%jQm`tJaR$+6CiZlG<)T5JJw*Jy(Jhv3LyfZNDkilLUppY=lzq< zr&&e=KsMmzO7W(-F;nnkA@^bS=L@!Xe$t`$doQ!KWDE1n0`Q#}7!tv->unuhP<|ZX z>De)>4Q^55)r##Toy`8)VFH;CBc7LYVZVBcGUm^@Q&B=Y(%481#nBFcj=IRcaBqX5 zLWl@K=~zlM+^(z%W?)$rke7-{#1uO$ zZKz=}VYR)a(ynOn#E0c?MJ^FIj(sY({2&9D)c^~uQP;jYFdapS@y-!M-5>GD4)D9> z+wPv3zx8jN&93f>#Z0bdB(r(tqG-)~#qos}2W^AB(E zdc@x!muno`{XTe9q$vd~>+)~4RQ!Ra#L?nHziFAF=EE{!Jkd$Nr^ZM0#rN5t~xKuKQ3_pIq>#XmrZ9*Mlu z`eA``a&?Vl-?l|A9piY^p%+uS&K3 zKBVaaf-4V0Q@B+dJtasXJ7|;0Sn&gGoYRh=4#E}0Ekfe}a5j;N6}0b}hO`E8mw~^a zQ{xICh0fQV=6!WASla%jTEHpB(jeLs(+R1}U+-dTR7zNZ7pxgnbdxn9((4hlZEp(WwPMPU7DERH%6*j@UqvWCbXQ z;pDq5T_|e+2m*0gAdsE}WG($W^xQsws9FgpDrQ@TZwMK!a?7ZW$pXfwB#U5!c{n{Z za0M|T$~;Tv>5Hj8af*HBxrPl=gpxZHFk5It2sMPXGL!(|`(T5I?Lq<;2^eoFG+oB5 zT-Z=>i~#!=J!R3Rqk;$042XC>iQ!}OT{^^)%hx#oL3E(6KC97~-f!bGKX#GX*g1y& z0{p&iqVi#G>HIn_D*C%h@Ar?a)-c4mf2t9!pj zoq#iO@Y{$Wz#`nZ_A0G;TLPwCs9GDRb1r@$<~?0R_{Z4C*-+gX9c&?dT{TKO1KxwPtHswkfth7&K+PeY<1a&i2k3pVTy=o?2Ari4sl}84mgrCv2!%|? z?Xvk!)E6#Tnh%qF3|ki|)V8jXJwqGEexiNAJu%*=+&yXO;a?Ro+QW6MDOd0Demjpq zh*in}v((>mSK_P|r53D^YH_1?K9eD2-aL8%15%QpIb&h5V!8iD3}HBi!6{UN9d-Bs z8OB(}=kVnqdBCOnE&N`fXr*!2r!%|t{24wS8&jdh9@}8819A~><15Y$x7H!!NGy=r zD7(AJiZd2K&onBAe@Q0$Vce(LXR9Ihg87>3Z~bQJo#}0f7r^vS9S|<#2ERcJ=%OT` z-e^mRB5N%K2+h8CYP+qdZ z=Inx0Zong!RiB3X9TTy{v^xqdoNkiJ%GsIw(!FE6MC~6VH-new6^;(ZyV@p?O7X@S zFUpmuj3J*jorv?s-2jTN0QyPVD5Z-Eet*=`Q3$Z;4G>m-5(r_%u&*bn1sV(-sv5f@T= zV8j;24cTNh%55-cQE^$tG5MUwO5lzPvu411xeL3H^oDcje?LwvR3Y-4ciV?X%e90% zRq^5V_S2Vz3Ty;Uxt=cSfQ<MyWK==KT{zV2&x_6p{1qtc`#{i*&G(8Bx@^!S_|e=pb$=SuFR>B(~9CjcgD+ zMT9yoy@0_;;`_ZzOlZ7}J&c|fY%_<53!UjI+o$jXH6iK?i-V6MiBk{&tbBf6!>NQ& zizU@RJ$ni`P93CcdU2sNs8^v?1kS)lqmHl2SqFzuSN5`va#P>v1=Qv~u9ARO$x9e4 zC3_~!KOf2*&)7aIj#CLP#G~R1ZPF@mQ{XdeVBt#^Egx=V=AP){(B^f(lVDxtD4(E|2Dmy!fWvp z)~#jS4Qo{pTN36}(H&u~;2KXx$7+LreJ$OmC|6%}M!UQ~{l&czbo;F*r6p2eTe;aj zHPGF<^~m7B4Br}M`x2fFb2Or8dtGAIOjXH$uK+~xVyG%V9P$m|c)8VS6MU%$tWYq| z{aa$g3w?HdbV>w`S@=(}q;E4Q`VtgPJBsGR2bmYa(qmRViqHZC_9Qjwj$g>%CUK^l zZOY-i1wyMh*>x@*^(fsR1+vojFzu>v=!x2l+>TB(9D{2=v)vtniTYf^)W(Wk@(&;I ztWjog51rLZ(0~G9Lg0(`n2*Vv$qnVsGeL&z*)+jYld$zCOV3yPCpw3;eC~PK-$``5 zupgR&`R?qncLL@qC?tp#v5Iuq!)wQP@hK-MuAHU8r!lYHHMMKxzP!}EmdN?28+6mT zO{YSE;>!Zq+@QgN1j_Tcy?aZG4S^2Q=Q*4|t2V|6Zh;W+hbY{=(yWebVR+D{sKQJn zyCB7SoRK3HH6y7@s9(q!i=@F*ZfS7RTyz-nR#q-Rf2PQ)_3^tMvm7c_vzp9wbzQ{=|H+|`@&qu<39kh|bq+Fn$E_bSs7t~!}Q6LNh z{EQVh-YgQJpHpLx?6Z%R9YA2L<{0sADp9$>aD;TwLAyEw_n?I96tml$xk_Jyf?rHc;V- znJIb5Kt&*reyh!HXXZ9wi)*$$HU@%So=d%*`CCZ|-@={rGNY#q?~=x?un0mbNEM;t zzL*So5@C5+$cOK}3?n5uACf#GNLiLQFkl=l6q`gveX-!*ezM_qR74*Apr@^5+|lBs za)MjHA=)`pPD7Z!HHgxr>)iD3KPsQ-LsSij*=4{ zGs8?&`)Q@gS4tr&3~!sSBPZ3JKoA(~U@LI}F>4T9z*YNg>0u@TsNm=A)K0a?@w;Yt zjNk&`?@rAIG((k}O5N=wg+&!JKeE>SE2#Z;nHek#A&9B0Hb>VFZIr2Xy-#NJ*V)@; zt&3Oklc13vfF+pJ;VLzc7FM~L6OU~yT@R->^EGj!0asp7 zUKo1}@as#^)wtrc-4CT7u0p!sWF*Z5-$D2~LnD{BV7|7oq7!lyGH-v?U;|34Ec~X> z?aY_(sf=8~|AcWGRWC^vsSZG8-?JD4=5~vtgKefto;jpwuMjpV>2@|vrWnjHE_uF5 z&sk4g`xKhZ)%nFZyv^`mXDY@FDp!9!JtLOyd>8Kup@d%SfK@Vi1C5P;Ieq*r^|;8# zfe#oK6qnk%v3A#T0Z-)W?oD3zYCRec>ak+M6A)g)2@cWI@gsgf1+K zy?k=uGhvfr;$N-`6cv6xhVK9X^wC~E-tXAY*UNt@WV0*NB;$$ge6h5MOUB>~Y51PZ zuwi+j{e~)d-O{!+TqlyHo6r+ziQ<_u4yRFC3<^P~s-7bHsI!@4c8RDhZ_zVc|2hqT z<)G+0(LQk-^vg-xJZq{hEZ52s1m1I_frS_eDJ^T*@yp|!5iCQ^NdRa}fy@*iYYaLC#6^5XI>_M6WELQluLSi#m)x=>Yk&_{HGw7v3&S zF@ztxWq)R%I)vxFGcz|p%d8ocxC5YXyyqpWF%ccot~QqM-16#Io5#NE+>NvClt6xY zfmXhmSkt+=TY_?>#YS~KM68mm zN!-2_VY!i$P7&DlU-zx2Q`v>;rhWr9tpBVsL}WZG-PWvR@vT?5L=Ak zO?Ha*Cbrd@Ilm)di0=Y67^FXP8Q$B1ASjb&p`*(vx9&s;y3357?K!q8(5C7O74`+F|Z%MIJ${vdRD8s>WOC+suidmc=ywgS}hgVyH$7} zlCcO>l}DMJrU5ziiCs(2g}GuGW?fLn-A>)W-k;Gm_e=raaJR_&TLtzvEW%9ZMs9D* zx~pp!Kd%%Z{FPI_{NvYRO}0mEf~*SqB~jiojHlY|u0aV}96ubEU+t?Xg8Fc5)6hC6_ql1QJ<^i<0=;U8 znJHe^Y98U$BqDhcw;l$M1**8WE0W~px$L*rc^MB8fAB}$mLq`6Q5 z`=s;U#BqHii?RsEU-R-P8OtfOJAwI?pmjnQ+Vo3x5hU?qtHJUBzZB>a1~8YGWC4w$ zelbh{^+bSJu?NJbJBPS152Uz%O90upF=F?W81NIDeTi+Mj2vZzK~decuosA=wgbNS zHMlfUl$Rphyy=5BUWq2NA5K)1hoWbxIJ9$e`we-p zx#3>*8OH1{#gpF#XMa3R?3|{zf(v|R%NOo;$(r>oUOGBPq^mC#OSLd_J8^YYq7|-8 z!dHAa>%|)Jd9@1qg*P+{&QJlTui~G8DA#X7cSpLVw@ka8&-Loe9r;&)&sr>W`DA#= z8&599^8f%Tx)t;I;&;X`qZnrr5d$q^=crsJOzNK>c;ID?YvqwgM8Z$T|3lnlFfeY! z8MQ_I4_;^tU#9{6AERYU0txh5At3SRitm3M0@pf_|6^o689;#k_tV!0Gc0Jh_jM`{ zCNNcMEi(vGqFgZCe^j|vT6kc%{~^!)cP94VoKkB5CU6MEe;2eu696ZHg8lR9q~;R? z3xR?VUZv(z{=1@^6$B-ffCLy0nbgr*7kPtJAn)IjSv=%_1iSypb^o11rv4%UMg(Z> z{I_z}k8}4Yz(wD7N~7`83gj1&Y-*_+{2w$QN4WF^%S>8v7_ud?i}&6eg(NW<_XPVi zE-YdbiyUDp{FH5|@80-$tf&qPypWoSQ zGP3(3GX4~wMb6?6f>t9%_@U7#a219{g=hO4;z+{jY*qeYdFGeC=ABUFh@yss z8BFBlr{D9-2!1U2aB~Wt--pikoXfaGf1HRbADF?~7J;a!F!A^v>HwfC5@+GT6C43s zJ&UcRzPddXX9`nVcWf+-g^$?hs)bwW@B0B37Nks(vy$fA8h9(prCk3;ByBTHF(s6+ z7qu~)!wN;ejMj1j+In$mTLXNwG1+#!4uZ0Bpg1Vjo3i=3f`T_i1bE^Fg!7Ah{JyEU zb5cS5B>lL03hLgC@&NJWGCE50rNrLgr(_}qz6Ybk8qn6-eIZZUC<*6P3Ath5Fwqm8{M2 zlU&358+ZD5dN~S-#sfCM>{2LmYy~$O!!g~-CRfK8pa~CcQ!twuNr*eW(!Q(UWVA9?Wl25E_QEk7r07ol zr`*JjTk!C0gn!#6pBO!Y&2_LvBV@D?*d0aI$QxmE!at*iQ0`ATWz<4~)unVM$ZMoe zQ>tJrAJYxc!_>u8-=GipEar&=EoW;~A94hy-8;o$9VQ!hCCTp*yrvRw@!U~*PUKpN z`rg!&dZ-}Q6^4^vm~-Kh2KNZ zupoFD66C@?cb1^Rd(TIesB-PL;PWcKmnD&Qn*4O+8x^@+@HSTArF$i++BQ9W#1>dl zndYi(Q}KCSNwr&OiE}&+W|R+U4w_57O<;K8kj^X?fRIH9c9}8j`Zyd7 z+zomPFjZbIAaY3ye)^**qk%+sR3C!h_Gp3<%0M^VfN4{&W` zzvZ+%VTtOndVt3y5OnGso9*}gWJ9Les_;%RTL1|Ao=7ysB+#kcjQH2fn-13j`h%xt z;_qxqcptGg(B9YKe2g1aWC2Y2h!_+31I|f;y{ce0qdQHa!L&n$L9c=sVYuo-FUQn9 zxhsdtCdpQhRHw0l_?8-p4Kmxwlt*4a058%kM{{ZW;T4|15P#_>4*#ESc{|oa-RKSp_@C^ULe6hxc^&uCMFXtg) zaPDRQ1)}Axz?7{ycA6>316H8Y#*uw}!(sfP0?*W_!`Z%WD=(cbRFfW!da^<`k8?%| zVg)e$_(E(bq)MgzEcw4E&!WZ9c4RogCZAZeR59pAoaa7SV)~JBTcf$2z?jJ0_B2HR zz7spC_=-%n8z?!o9F?ea`l-_{V{`R`xd|63rr^w373(06l00J z=Tw0}-A~WPf{FrCT&JEGc7r!zzTCms7Au^LB*8eJ*O2eXn{p?0;Na)}XiNCU&yQ+- ziTEW-k_V^(zDvW<5+sN_(z0N<0#=b-#ZwP*gwjaEI zQAn1Tyv--97P=@m2H1Newe{QiYtrKNo+>&T4i(Hdarb%puI+g=?G*G;%S;9nIJHh? zBpSSCF^(!^MeCn?M`}Ab^e)pSDxX2ss!z)K{KXxk%o_gC8C*`ENM6>cJ?|%7&;bu6 zOlp_`&I(B=FC|1j)1@mRiel9Zf2UaWY**DI>cmt-V<<93;IG?3~=bNa!B`vNMJWGTJ|)vE2e-Tw_4n_ z>IBoKkf_k-JRDY3zO0HZ{oxh-qqX(bm{Z~fSX_$)`8}mL+~(8RIQ$dW46hFBp;(2g ze#MBFSexM{C9AO_!+E^`QjV<#4~iS9&(Mn53jQHEoymC6E{t5NSyJ4oun80eWpwFC zWcpQ*A9E|+MJi&|WaaS+ud0B)-(He+jY`gA{-(8LYyqw3FiVKRCcF#2HESyE_dVzX z_;Gu3islr6YFJa^Zl}Fip_qICf9cb6uZ;D#?f!-14npbK6SS7JHK~1JScWl5SK=l( zijt~K4g6FkL&*uqoXRyfYMDLi$0$wDwwIf@L`pxy2@&5TqtY%8_1GSj`?po@ z|A`Sg5TMFjWdHI$eT3F#W?*CR|8mAu98O@N|4kYHjk)uI{l|c5&EW)=0R3O{lm~bP z?0h!$5h&^ zk&;;q$E;W1=NyGp6XcB;_L`NM&MP;w63P#3An(IYn-0RlKbY9XW=W}s&vzg&#PMm^ z(-(N$RWG|OP_3tvFwFtUGwG;0{W4TAbZZJp|6gHW0aexZ{Lj5ecXvrjcXu~PBZ7c* zgQUa-K~z8*E=Wjsr=)auOQ&>7i6FlNKHvBG-hZwCUh8sLb3QXW&W>~T%&T}MHKj`sP~3zX zWj{;jNe+ix);>o&j(-oCcnw9)ll50y`J$`3}4zFm)kT_?cdq3lVVCD-+>a=h2U0(6K6%!%&`p~}A z3EPYU#RD$Gh649QE@l0CCn6sUH4_EpY{Qs~o~*R7nJI|&mjKU@5P-&EyVHI zH^L}nWR{@Cnok+#GqRg(Cj|Rv4+K90%51F`A;Yj&HPqiET6s1x{Vg+Km*|YsyC#Jz ze27*k8I!*wQ_%5TfqN@vQ+E85VnC?y+fV*Fy_=71#*1U8JUK-9YuN0KRAD_mdU9WU zXD5=Cc=I#gR!hojO0hzC(O6=?h=#4-dAqyA7~Q7*SeuQvv3;L76_)q;1=(wdj|ZyA z^K$<@rs8fGLX%K|b0>gth7DEY2cz770s{10Obrkz`iWG}6Q<%ADMH{JC2!oj5%~)V zCjiE%)o=hkg#R@lJzYUxL0C?J<6z;5PlG8M3`~V63=Hr6fP}USfC(V`2G1PlyRaa9 z%ZFuY{#)pgiGt#fmqSp}@Jq6q;_85pwKZhueXHp@E+k?mZ$8edG$Wn01m*7Bji#n^XccxV z%<>s_7#-ZzJ_`Fre|rg0Qnn=-*H#R%*L)!{6l@_n(5#YvSC!vi^!hjqCdPh{XtW>1h=YyEP90v)$az_A zZmJ23^!VZgv)lv@!gp?ur}-f#%{Z1MxCTurv{Bwf@Y!<%CjKYjc}jT0Q^4tmUO_|a zwS2UGf1i(Ps;`1k;HM{!PdwO6q$T=Sz2V!iYV12$v+QK*KfXC-PGXhUuUc)>Kc&@= z4plNB{$SpfvKJMCJ6+z zN{mMZb3G5+`pP;&DL-cP?Iz9zeiTMN(FZ<=FdP->#Ot-`d*EA&3Xn=mP1M$3IfE;M zf+Ck<&V5I@w;*4ZIc#1KvblN*?ON&UeYNdlN(-LF0&5S^XwxiecS*is804?SCTQW% z-b%mmx@;s&ut$=|0qlBV_azCUE@Vf}pynIhLH&VQ!uE0JHR9dqZLc zlMTTLMkx-)sg)3fDu7ev)Z1*5u@H3-0l=UihUtXzT0?UN*wgvVzd`KCdVb()OST8YU9H0sppf{1`p76~&t>mbCW z&r@0ZGr0v3#l0ndvFqcU*#+8pkehqe)Dfj)2ANHru-U?;f-U>g$#SHDFgb53>1o4= z+MyS ziIsw2%>FnZp0z84AdhO!WR#W}%WQzS5Kit!wyl^9S8}gWoGQJzjH*>UN#dzIPlMw8 z6LEqVK0-Qidqd<(a~H2nn&!9{*;Td9uAD23A?utFGdahQjQXm$)%TH&@$vCResa5M zbN)&T^m($56Fx4PNM97GreDwWVD{Dcj}o#J8}C>XzLc9S_wf_JpCH*Q+Jj-}K+M;i zUH6KA3)ay0Qs+ptis_X4uq)fbm4cGQobc3Z)1LCPC2{_`eFZ5hInpv{z0H&&^t*cgJbDA=4Q3vu zUye(CZ5*4pVcn>u)3#I*vFXGH(xa)ePD0WZ&U2IR;d93xqAU7d-2B-aI-{&^3o=3;T&*{*Z%0;fbNO(>)le$ja1=LlCG?NlZ! z*p0bn<>7NtP+n&-!Q{p^?@&OBUQ=kt00`Sr?hBZjSllI#SL}b{)d~u-|4b!~U@V zh;xMI27LSK3{8lehj%qcxPyh1Tw;x5$QHfJJ6o2~NcyC{+;ZvRXLaQ=>x0*vOI2$c zi!*ttvpjuYFf9uu!u9tpWG%lXlz?(;Po=mc;`kKyKB#0)P7B&7$|FCd_8%vw;c5#G zfGF?wp(e>|h##2F*^AkPoG6Y9cVVwMe@LB8?5hjr+CO_Kw8RAH6?2xPZi`WKo@LX( zhWa>>bLl{6)cRmWF}rnD=TQ)NGUSpA|YMog;^B6SBsqqsw8HMKw`cqWB%$t zpZ)Cn=9cp3w7u*I=G$R?KMsL@ssI*YU5KefTS3iQj&ec*r~Zs)I@bm8)ep3XV2}%1m3U`o;DD{yxT@&35CCq`2ct@pVnpX|9I`E=v7{@dongi z&6-SW1e|9znrM)YHWp@Goium|KO?RnQ%}YELl&B7Hu@kS`yj~Bjn6ObdNhCrZwUg1 z!_&5Ug(-r9@_w?h?wRWcH3um*Z3qUo+eQ68X?nL(iLLkpVwQmdhDS@0lj-R#6T6IB z(-3*H5B$XPM;+^kd`S4M;VfcaBS@{smCw$$DVIYpCV%*HUQaq$$NNd{;L)t<@%Tve z$CE{$@(p7piEO;AHfasDZ);#I=K2abEP7i(hqV_-=w_Dk(v#u<52w;j?dV7Fi>zsK ztE+E$8>N}?-)eg;CN7~))L#0A9HRIHFPrt@yocyF8yU9i$*O-w;q9u3QxrWxk$Xtr zZQVTMC|adHX>U@RGcNmqm@;RxK51sxt{IH^Qf)dQwbpzgEQx6#Ynan9`$qQy5+qf5 zC>S?n?)9;HW^Q1Qk_ls56BEkjFe=^bU&&o5i@}84gA&A?d*KD0<;x6UW)scZGx5S| zddJ!XVhIc)v;7{uJf!}kEz^CuTsU$|3UH;9!Jz1GuV)3hq~4x;ocjx@?R%Kp?+7#o z+MckBIY-|HW<7{nddKFQN)s~y(SZj&LaGm(dQ-*NVkq?T=J~^ua2GPSdX4;1W}Jp# z7U~(JmgnH1_t-}4s9*YpJ#OA_mLK-5^E@uwQNs>wtTSArh?s%f$_kj(fz~Ti| zqaQ71W8@=-sGr%)96~p_-&~J8FBCO1iC9O}Z*vn@EyXCjO>{wSj(a`{l4-JttaW*j zm*)=aDV!$B$6oJYjy{!veFZU!mI!e!HnS0k$j4M^ia4Ix86nY|4h)fuUIZgE0XQmdqxrWV`*sg5TQ}$VLCPPJp8+-r~!j00_S*@3m8vX7kVC6)Rl9VwF#o0 zv_Xf_Zha3+lcLPNDuob0c+MlcVKZZQV3v^#`M~IS4d*4q-$@+wYO=q6Kb_l`lO~`< zc1DfVHySq#ji6HZoaZ*wUOuP&niv*{DKqy>GBMFDmEpDW4mx|^wF|qYr^uHlYn)l3 zi5}vo;F70hErLZ(Hg*W}DuPgql1b_k4yxSF?%;XW=K2aeS>2AVo5Yu)%AidC2z z^lU_|f0pm}&}Gx{=h;|{m6C&Z=Qn*{{-U-w>Zga22a_Ha%M*3;!8d$+e0EAZsy)0< zI9aoZ>*Jm215Xai&ZjwBx2<%MtG;YK!VY@h-h^JL9vm-yybO_n-tfBwjrD6Ar@&sn zk^h`kY**Pd-tShG%c8ERtnDumM$i}?>@p&Ux*qKpDTGIAA<<|KhU@pDEmXny1exWEyFh%kWG?oOpg4vl;T zL?*f4hN*uA%7uf~_GGIydkb;~VU?TO7&IDo3bF#bFrL8iFLcnTF%ThC?L7z^qPt?d z#*2GP-g8SS4DOY8D~li}x7!X!D`{%$3eUuqD__^riVvgV8?^tibYW&tvpVC^=PTNlPxwC$TGr{n^~GW` z1KYiUy{uA=3~@D9QbCkLty5Ox5TYvbSUn4`*2a8_+J4&di?XI?O#v%DQ|Rc@N~{-M z`-`e^(i*0i*YQ>FDJPo8;xJ$`MaB)ASB{;GdU?;f=$kAv%w|2GwcBauh_=;1^0m9d z9?u#ZY)FzFg%ifRkBbhev3sx4cY~|>*N8X!Tt>6Yl7}&`BX5ivM|W>sldq3~#0JbA49lLhGJbxE8VL;h{? zNGHbGn@?XDLNYuejoqb{pZH2=uZ}$&WL}AW91BY*6fFW1IN@)F%n!5V+`5kySlHho z^N7cjMR$erDvwc`TPhB;g9V9_M+!mz5Zj_vMST2RtyMwHyUdKA15NRz`c>=WdF+#7 zgi9j5>YTJptui?}Q?sJr>aFSB+QUruI6%{f+XF`Gu*!{v30-oV^tQ~Jva!*gCGKwB zuYaB3k@k?6cUs{+ zP0e{aOj|3JvQYenUPz9P=q+Fa5B83xw~(=pfZYP0Hg&0@bsaT9bmR*I1M2m~7o=%C zD5bJ-d+Y zBeuEZdpNqsN!e4AgnCb=atuSjM-yotf3 zr0vZUMma|7vMW+#F$2`L-m|(VG28T0v4TdWyhcr&U9KzWBKn+0sids>NOkqd`_HzI zd^|+pIuCy6G^P|q(>u!D8kRa}gKkIppS~rLZbUT+u!|EM5#i7KkuUj7 zs?wtjagfT}D(lp6*m(GyJKz>Fn5u9*iFCZwf5aqxNg;oqzwP1Ya#pn$ksy4+ zUt7Ug`5*zXxK(XdJ7^LFe)kgU-3gLH=K^d84Dn;7m4H1bTI{<$r}zaBBGgM1OcdV* zLWE#M-iqfTQWlp)p4-D$r5lh`wM<#aup0wb1Mv(=g~~QGc-;%*yjD#0eG*e zPr|z!);zi+hTjURM@{<_=Lyd5N*;G@{ZOM2yK#Lsp-~&MVP2TGqOnya@YLw3k(T0t zyaF1RPdDNck4B>3UMCul0sE|iqsbQS>mrD)4!^gnp)2trnx?BE*ZlHHn;NfHtQOAY zQmmgB3tzEKgEspt`{bc@b<-9a&#CMv*{&fvwMwCmCq4U=7RQsXiUn>)#~;pQSNl=G zw~VZPJ}N5ZZmWq&4tZk(!~Ij-#m8VqE8okull^opmrSclIEj-g64o-G{+;0bMG$0A zd2T(EeYnVDG*?K+C6b`(Kyx>@D!uC~C+VhD%SNLaUhmEJW!kd3b(mcK<_(DDh#89H zbqq_YIBV&3@DL-Ko1SO7D?etB=FO0U1q7$|Q&GK4lm62>{=pY^2uJqz<|>km`SMbq z5JmLPEk+gf^=U6*pYV4xJ*1(mRr~@e;Xo)Yrd(4rwtI~W$5a0FHlJ(cBUUpO_6`Va z67eO)M*kJ>2yP({NjA0Wt1MnceC1sjbis3^lCPQOT&6W4+r>rwta#ci`2tCfoyhqw z7t~uVhPs2CH@Py_{dzEi23O=wRJ-5Req|YYvkIclH4aCx4|Sh1o6RFsX33a_I67vU zbCTG)l9{P{uUV?Psl+&&Bu|m~Z)#F?XQB*v8|AZ^L9f;KID^{~( zT;1>W^Ssus$ojcL9oGj z!vG#X_Eod&xrQ%%OJuav1+NBKj~|M6u@2$3&Ey=BG6hkl*|3rWk5tV-oh$YT&>WPDTnz@JAgs%RvV?7xc1nv^ z1fP?mpwme4e*9kVD~}-GcC=nqo)bXf25F99g{wV7C`i-2TT;J1)hAzJI}}EJoB-D) zNV-?|CESYp)H2%F`oqVTySt~Kn`FKQV-WqHb!<2Q&?mbN%>{tlW49Mbzf3ur; z(SH^B9oouQR!T{LGfy^5aLtrJrqWCahK~G5WeI;Dfu_QC%Xr}DJ>s!f?uqTy>`<3y zHgDLL;ML)WHcc=2^B)l!@AQ;U2SAedMsDc?pe4Bb1Gv$3de=Sb$x`lUZAZ`e@(9}@@Bs%0D<_m59ZU}qmX^m)oY}1|uaQfLn$~aepLv*MYT4sZ-;hsaTJ9h988H$@ zBJ~@=cN~aUX!7&Jwq&LzaAoKZceU!a-}Bi$LD!BhnezKPR8^phI$ie^bFdg`dPK6RupCl z8pT-Y3Zm+bLXv9~XTLY?OWNJ><>~6H3F}-RY!>EDzR!Vh{-;oD46r(=99oY7 zz6NaDpi0tdtbK zT6XF(x{;h)QP{cj;WXLY^N1b66VbgCPZ`z}Uh}fgorgB33-IUPI-(4qIz$qbUr`sI z)}4k>LM5@mas)MKfok9k%OwNe1P&@1befU~y1CbOjq+3Q~ne;eeeW zy#_I4S;Sw8yl3G2zp(i|%swt2m{c+#=)1Y5@RR+l;a1A2+Jk>hOY?kXc_l$_AYUHw zEH+9F%A+fmp`|IBPO;25v^z6SC#}aEza@&$I~bgcqTN@5UK)X*OQ}p3ywx#%g#2+W zWtW0&$$m;#*U^EukYc*DWYJu+O{3Quvbuz$C~TN;5}6V5DmmnAM1nozyTM36{bQv0 zk%Bz6Ac@9Sz@uYePS@n#W5HzhB5{z6%B9pxn&n6#B?7c zbJ}a~k_4H?{zsSuJ#46qa8nopwqThayN%kBkAf;rNm`|pEop<#DSaF1M{(T;OD}R6 zZV)>9^v0+iESC}L;ohP2JtPV7%Zsy1NcM!6Z};udI2M%V2re1%p_QQ(u^qg{%96)s zE$H}AKTJ5~&$;Quju;un^+40l8Y0F`l+|-V7l8x2;QvYX6I^eMlAN{QOVYLn{XJ=s z^PCe2^rjuGs+pOqSlw2(3>f7Psp?Hoj8UssW8t+c*N{F;A$2_@Dx=TXXzvlQr!rpU z?X43 z6V4!LYQvDQ%%nfvrDjfBx*W++ojHk0qu^a7Kd1oI)7A!l&xS{j)gBVF#he`@L>vQ5 zm0q&n9`#pw8p^sW;B15iwm>GIoOC>tNmoiGXh-DGX7&caWgNPs-#Cr}!Ce+(hu!%2F=iI>-UIUsdg5`*5%MB9ZVEOGmQyfcu|pbDZDJj_Nxy*`h1|Pyco9J)I zc}mhaA9|__8VeA6iF5UR(9D_>7gs3Vm zHHh=e&6G4!SbUa!80utxb{B~w?ix>aFZMN9k#3@;Uu>TABp$cwcvGtqhrpR9;U=9;<<7ng0}Zfl3K== zNiipIxi z%LmKBAjx+j%MB40e0M2g-i454cOY0$*)vELmw+ADhTru`GH1kjBe*0WO$zlrHIrVmev%esPkD;iN9`Loo;#<~cAkjCboon^2X7@5R`ZWs=H*m~ zY88JJWa=(VxKqn;&h}qA+c`R&fQFgB>O#{zIn&VR#4F%3ZjJdl_qc5Wc#Nk%Mk) z{k^I2%#6mb-?TgRyZh_C*f=Tr-lC0mbwqQkv7V2 zW)w8F+&*%Q`to=fAhsff`Jh`jFfbw1!mB{5-_LpLW4~l0xPtCfFMftB8))J$ja-n2 z$G8eVy(pP4;lR?%dCrZJohh7bH7qODT}zf3G1%%V(d0TmIu}yB$s?Y|*Z) zBKqB2fRH6|tCzvg)E5ui39j(6=pm{Nl7bYKgr*G1o=V@X2X4i{%MAyi_Vcv5up`|4 zXyog@(;HE5HIslVFbRxyZ98_|kV|;qT2pjP|YKe)n zO$0A9w8}O^fcmCbq1tw!t~#U6qDhE056TmLNQHY-cOCQXBc~yW)dFV6vj+k1*6X~=F^b-Es=O2(<2zp(YIL%6F^gy+LUIUH+#F+FtJ#t$eE_Y1&Z z2IclxI6N?FMAg)r%gd1Wsq8{2*H>GYI>f#P3Fujx0|iUf^Vl1-Q6Xei*%+x%B^GFyTlk>`j#bh` z->MaDgW{mB5*&^7*LLJ%goYuHQT&N&DDE&*N5QVMq+^!5T8Qaz9sCE5E%}RGGkd3^ z_DG|36YoxrmzI!TqrLVcyYVbNaWNY9nRDoqv`?@_3YME+F{<6*cD>q5y0ln&Q(l06 zguNC?$RO~A|E?|;I!`P4O!aVTA?Y$~SGu@r{A%7N)-SoOFUs;6x26fci5q?H?php2 zh@Y;-Wq;av`fV}WO9p-*gz*9z_PJyRhyS&v z`z8(j)HPaPN2cLNC(;b{^>$8zH%8}xAu8j~lYTgvj}zumff>I3?9-ZXYDAXVDc&fj z{A!5KyXA|W#cUT|p-*+CR8Y}MogYcJQB1!(Ozs@-oU_p#?wWUXZ4G_m6~%d>N+K0> z*2rtU`B|2A6V!d|%r&V90N=V!=RKfkb_p*5_tV!tQMIF?+ z)5+snc%^cavO1GQwPDnx3!OXS+nTk9?XF1B^V4qHwO#2`YY)#jjEahFo8@4`{U%4j zG#WPrI^0E)GG&5oEOadv+fC_bBolT>@r9(9lw?SSkN~HkEWdz)K4FhQ&Wo7Ua-Qix z={Px@=bc3|WyyjYW4y&Fwt;!6ZWi@sRhvG{XP3BA3@W~a<)yLH5-uz2=Ewet-;tBu z>Ct%0!rlrq%`ztU7Q3>`3knzFQ4>4&zaxk2JT2)Vpy~V?iA{O zD%7=Ff{6O^zob!HpfZC?2w!lGZ?H`t_Tn6}e%0=u=bk*7;!?rMwbFToc%`H&D%wu! z@x9!TATcmrdd1neA}>ZeBgxkcnfv7#@l(X$#PQQ6bQ4^eYvfGP?upcGUYAef8NP>e zToqr{7;^SJmh0nh9zOXp$_pdpCfCLslqt5CBnC0AYWKSh8;|?$Lo{GFJ%3DQW8S#U zMQEBoy;g4?nApszg^tPkCPE2Vc!uZ1Tuv<`^*+?cT3GmK4>Ph6>z9cb(V7T*;JJ5t zeq)}i8Ah&15yTvd{OmgT`0?qc#f#{4`|p*R`A**GoOq74Xqt&M8EE?ZCk*t=qc>;epYKItlEl9mg1Y*c4k>l zvMY5duA%u}QFuLOA<-hrrDr17Ol}n&woTQdz3Qqz?oLkREoY~vMX*Jpio<|}@WXsb z^7z@6*oEwcbD$4wW|a`dqR?cz(DoE`tC0X=-zdD$L{_Be0;K^n@^pY@bsb}O? zRNuxd{^m#3srZZUD)jk1z2&Sw#`9CHYT%{>m$JoOP~XI5d`+mHqg-IJ*NNh z$|^iA5cH(KVAa`b$589B&&x)h0qm{C6q>^azK}gAn+tg#}L`QzUP`#lOm@op4L^LAAJbN?P z7l)l2Ic=+^4UWKNt?Cj?ax%dJ!qUbwy;9VCOP8>@`ms#6G3w_R!fZnEb-0!KJp2IX z1?Q;F7rV#FbS~~`lCrFpkL61ESd6+DPHx60`3C6i@Eo%~4*`8SE*+)od|zV}1R1A6 zoaD-5P5RX0TII92EuA%kY)*rV^o-QHK+b=%ZA@qB^T)zzmX0Hi1MF0hV^2D!*xD>pUoMHnzXV+!T=MlulK+v zvXPmPR9DxCu}@U+ts`58aQ&)oiB}*!dg{kQ-w!_(v%~RKkHlx1=!a~ ze_6)7@@w*YbID8fxjFrlK;D|1j^sz>LY%iv0fLtTE=gNu3QRRADi0@h2{X9B;tj7dZKzSsji+nmMkLM&gll@7jcD6+hVl$y?LK&)Q-L zzTV7^vyZ%glmyz?>JKo!;;*}Y*PxInKW_UKT$=1Qgw;~DoFH8l8wf(DVsD%;M3B*& z8kYRdxl}j~%Cf?aGjRE^r5N)NQv5Ze^&zp4Nbrjk{3F#iJYsLdai4>vO#J~%mO$i!3hk%)v+oGK-7?=wEi zIQiIQTp?^*aqFf4W})%*mQMWU#~;G2Tc&F`nqdDr1LX4z?sCL>?`vNMEJV8HGm?PP ztF&vr;(@1c^W|kZ?L<^_)TS%dZNl`who>w?kVuIfb)=J^R0bq0jXU=D4T@t_uV9YM~emV7hzcNm|cXBcKZ7pn>^U9#7=%kv0rqqI;wh-_0i_K<17d z3=GL1GPP9ik)IgAxcA6EL*xDrS`#p%^H-o;8m50Y%fI6p{${HQcnwv51b%e4f9d@Q zObQk-j@f%g47Gm*My0s(621chp=9n8-e1?dOHDvJbny|Gh631l{R2e#2WSdP#Rz7j z{4*dT@S*w#2;bx`Mh!vCvN0YsnGuY7Z`uGYVgwU|<86Q461Tx;{%4*38|LPJ5^Dl1 zJny*FC;@M1gG9b;4R#EEDfL6gjkn2~74YisNH3Aj{6z&?t5=QH+8i3KWAG zeD6~YC1(aY0z&Zp9jF#Fn4A)bMe>A$&%;&+c)a^S%iMjW|4ual$)UfXh0I_|%Kth{{dCHhY~}t*}#Eo29i-}}%ra)L=Abe~-Vry`tZ#CP4Jzukr$p`Q%jp@!9{41Fkgz zZ~h0p_u)r}KH>(W{(s~c%%sQ%+g`bXwv ztbfTtWd&hEQ^Wx|ciyOqJa=uy`PaP}4iFa0zk2^4b*KJArV>90>g*0Y!UY~M0oA|f zDat>v;lHu0ykK0ay8+?(G|Tt~Xw++ZdBMc6-^Bl-3-kS^8}I@2Q<=Z$34CDE zd)W`L!rsOLOOxrHj_42BL@Iw_36();zY6mouem=ME;N2IJh=0sgRTJOxXlMx9wNAZy4L)_Q%U?y7S{k_{mS@HU!s4vLbA(WasbyN63V+4dpr~HJOKpM z2ypKM;xql7Y64)qpyC2xNvi+&8~u&BYuBBZ_Y(P_vjRYWxf@&eLoV&@J(&v1APC0! z)!br&K(lWCqEkYB1i{plfH|Cjf3w^SFHit^pyrf+h>i}2Vu^tXpdoKTDEI3e<$sLk z{=maW{=x$`bXb3^L4ZMw!6ZHpa9Ruu44EGc%)>v}JY)Z6gM$tW-7Vh#T29ISz(*DR z!V?RFaRIv;XsQUX8V~(N>~9BQqW^Pv0QNS=cYy4YUtDyeU{cCIjc@+_kaqrp0=77` z_tR3kC@@V2RNo08h0cnC3GU}zmEq-rc%VP}Vg6e{*F9ADm-)~AQkmEAi@9VBu<`Na z`fFsTi2-tV?r#x5?=`7&KzrYJiy()P<6uA``W<-hV*jK1T#dgfa*t;h2UgAhsAm7b zBXs=(4`^yJ{x`D}i39ns{_j51{d|Ki0Z{4s{^lZpjtB$qUUy4Hj7B^UEl{cpU<>`v zQppbfjYo&livV~)<7@EN*xltVFn5^H!@%(TQL4JBf8cxe0k^zJ5ilx)N9@!SB$>X_ z-wQTinE7|A35Z<*!MK|{m^r#YO&);>S?TPio@hsDd1C=@6>3f}Fn3(PQ%%79-)J^< zFL@P-dmgI)Xq$gC0N0uT>I-1l5P2j0Q{t M@M2tmem9l_pci%_Lu(D3q7pkSy#KtNDH{LcRI@GPLuR@!yKn%F3>9Ap}}1+W9N;6Okf$Us2k zK&imUAZV$+h#;tdM;9NJa@ZH%waU-7JhEVf z#QlkNcsRjSJT22V-CJ9F+PSqeDW=23XbE@DCSJsLY>uG-d~lgJwvuMjB`0Zi^*P+% zQRB%z!7VJNXVbd58p}*Zn7AS8dj{gfDf9Me&^ZeE{A$#G<1!^cHsvQ#HJ4UdYr&nE z9)k`DLe0thVBae}SBhV?+@z112LqhJmJQ2wq7tM$F_d!&5w&BypGU1}*4o%VWL4bP(0gnUXNnCB#?W+;r*GHsi}EI3@c-H+&p>>_RrDKYnYtOm_aZ_1GQP` zM|`{kp!^ydT3;e-%BdWU(u0w~lxiC>SV6#b02Wb-Qm@;d)B_`}TK$f5?yVz{`=)D- zou*i{liJ5K$q7wPGojw_r}s?OQa{=*`Wh(hn7SJJ3f{t)XDi-J`H?JB6a1-+Fd?@e zB~Z!MD2+)JioOT3hXBSR*uiWSU_k1);x^j_L}{0P2oS)2wgn#lGUIySJ(TGJu8@T@ zqB`eI^!&a2r&?vNG}3t|g5*#P94fsmrindc35f(*`6;)F*N=;lR4ggnmm0D_kluW#Evv@B*DaiXBI9XN-whZMjShps+m3#gO+^m( zlqegAgYj%XAQ(dVC#z4F&vJ1ysN(HZ5{IKY+;^`m9jY#C+4Yo_djkZbakBigT>5M^ zf9lp?VQL*4NN}#8*Yv?zmJ1U)j;&n=kb%nS`gPVDh`ZL;R8WD*HB^@QOT2V?(E!Ja zPm=(jCRe~$Yd4Rf)>V@kp+drJ`HT*HNg?o}U6m)XhvM=%yD)yK)JSX+Ri@&7g}i~Y zTkRRYPP3|tFd&0+XWkwUQosYL_Hrjbr4#e4W;57-g>~AmD!(|`ho8xbk`Mk7fRzV< z#$Sub5i5_H1HmPbd73Ot>k}VV03|1jgy=5|)Qd1 z;n4I46FK`M?#&m=vQC=lotiYGeUvj7~nF`>A^4>vh+2=Mie; zil4MnvsbY_n9Z`rZ378MiO7aLZe89ZKtVnuMH7kXv_a;Cl3TK+-OLsNGOkxExrDnZH2wY4T=izM zgE2NaIr*Z*Cxq=Z7@IVlwDX$N7F{q%l8{-fPB<=}YF+Tt6l zf$szVe+6L*-b_sczoLU02MFj75ik(JKV?EeOhSoPDP9X6pvP#eXJjOxCr#&IV|}IL z<+jmU_p_55pd#0C)m0)QxoD;K%o&4w^SuA6^1XHR5!unR-M!?S-BIWIaW~a*0eh7xk`T1hyfHEA zAe*JXRzfcaun*{&>rVf4aeR3`&TJ@ToIviFm~}FfJK5$5+cd7@vB9k7dD7V>xM?&I z9AB76=+ysy^q&+L_70WaMdP$NyLAl5Yapk*F$u!c27CuzkTV+9@iP{B1{wwcT#Z&- zdD%VM?`N}2qM-V3d~U*>ByxBh{ZRy$hzf!?25_1v7Ea@6LK&QnqS3P%}=PJe$~STLV2t+^={se zI?S5@&Vg-A1yM=W=Xl;)9P`uMcDGW;dUvfH}6S3pdUpY!i3pRPau8c6^(=r>2 z_oBpPX;JvzZ5`J^u+0jj*6IxzY552MImy2gFs{~Gr+$8y6QIwZ_V5^WWI1i351Gjg z62C}8PTkEif$4GBY{gRuDr3qMk;vX7@Wz`1`Zv>5?oRaCA|?7^EIRm1)9Lb13VVm- zuSZBEjSMO`zvQ%Tv~wF94=KSqbcC9{ewU7R5K%}FpC@??wQsZ4Qs~WVYK#T?ym7XDDzza zhlQk{azAJlKP(bN6pocTQ$yjf;VvwgX5_a#e`$-LFq1wsF%Q^g=%`$bkVoPeoOlJ{ zr>j+8;k*_%-6Oo+pb=)Od0>e`r3*y{b8u$hOC=bBVf-~sT&7jP`l0xwiI)|ng~~4K zGDLJ+`9)X{RT!rpvs5eh+s;?JBr{Y26W=OI)j0(bZ0J6hb0H$wx^OW(`CXKIHiNKP zavD=Xm&JrGwDQ<=pD(e#wV~XBbn}pfHRW0{B98_wH>b>?2MCO!=*?4rrsuzngb}ek zZr5^*5x*CZNTI_&LqcXShI&dUhypSL1=&PxMhaha$j#!0CH@wjTaOfcyjD8^vVO`P zGwBpKWb)~A>Jd!1kJ;H-;V2ufRu3)5mUHeAUXY5b4ZQLUR~)uWKmDXjYVbSKqz8Jv zG9b0MvSqHr$y>klfU26^+GR`KcyMqtxB47PSAI>J+p!1B5`{HeL->o0h=q?KP+^jP zp+rLdCf8~XESQEt_)uQj>wL%qAhh)$Dy~z)746P*I|A(=p?3Jq0=>uqC*k%>qLn~0 zsVCpyHB&PiAwAuim|5!;J!U=d9RUbV7aBJy-)F6a`6laJ57UQdZ~o4ic$Uq@vVYUk zsEcq-$!|ZL8sQhM;9TH>TvgWzf)1{j#?=Bn)dfwDT(SPu>3yUAZF;W<0GSSCkn4LB z`e$uAW@_JU70s0* zKTqhV1+MhzIvc*m9{aI;XHJpHDV`XZr`TskuD`BvRR5cemT|35w%CzZ50AB`x~V^Z zxM}H5D$2^y{U%tfne-Q+c(_?8%7#6i7CYLu!QeOdv3{AGm=4BP*0eZ2d`F6{Th$!g zK|Js2DkO~R+k;Ii3QQf!_6CB!S}}NZ$Y+b5H~x86gb#mP-gk#tg&Xw4nGm zyVI5&-|e9XtSR*HQ;-#YyrEv_g+bt!^~kwY%)vz>e{E(-&Gvi&+w~kz#|7R9#-ziR zkmP7@Hx?t;)(iO*+KtOFq5UG2uV$OmY9?z&!ro~G@!~x<_MgA=mW(w5PDf|hQu z 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 None: print_library_version() console_log_level = "DEBUG" # or "INFO" script_dir: Path = Path(__file__).absolute().parent - # script_dir = Path( - # r"E:\Library\Automation\ryan-tools\tests\test_data\tuflow\tutorials\Module_03" - # ) + script_dir = Path(r"E:\Library\Automation\ryan-tools\tests\test_data\tuflow\tutorials\Module_11") if not change_working_directory(target_dir=script_dir): return main_processing( paths_to_process=[script_dir], - include_data_types=["Nmx", "Cmx", "Chan", "ccA"], + # include_data_types=["Nmx", "Cmx", "Chan", "ccA", "RLL_Qmx"], + include_data_types=["RLL_Qmx"], console_log_level=console_log_level, output_parquet=False, ) diff --git a/ryan_library/classes/column_definitions.py b/ryan_library/classes/column_definitions.py index 03f75df8..9809ff85 100644 --- a/ryan_library/classes/column_definitions.py +++ b/ryan_library/classes/column_definitions.py @@ -103,11 +103,38 @@ def default(cls) -> "ColumnMetadataRegistry": description="Channel identifier from the 1d_nwk file.", value_type="string", ), + "ID": ColumnDefinition( + name="ID", + description="Reporting Location Line identifier from the RLL outputs.", + value_type="string", + ), + "Location ID": ColumnDefinition( + name="Location ID", + description=( + "Normalized location identifier used when grouping maximums across different source types." + ), + value_type="string", + ), "Type": ColumnDefinition( name="Type", description="2d_po quantity type (for example Flow, Water Level, Velocity).", value_type="string", ), + "H": ColumnDefinition( + name="H", + description="Water level reported for the location at the time of the maximum flow event.", + value_type="float", + ), + "dQ": ColumnDefinition( + name="dQ", + description="Differential flow reported by the RLL maximum output.", + value_type="float", + ), + "Time dQ": ColumnDefinition( + name="Time dQ", + description="Time associated with the differential flow reported by the RLL output.", + value_type="float", + ), "aep_text": ColumnDefinition( name="aep_text", description="Annual exceedance probability label parsed from the run code (e.g. '01p').", diff --git a/ryan_library/classes/tuflow_results_validation_and_datatypes.json b/ryan_library/classes/tuflow_results_validation_and_datatypes.json index ff3bc3ff..a2e53208 100644 --- a/ryan_library/classes/tuflow_results_validation_and_datatypes.json +++ b/ryan_library/classes/tuflow_results_validation_and_datatypes.json @@ -144,6 +144,31 @@ } } }, + "RLL_Qmx": { + "processor": "RLLQmxProcessor", + "suffixes": [ + "_RLL_Qmx.csv" + ], + "output_columns": { + "ID": "string", + "Time": "float", + "Q": "float", + "dQ": "float", + "Time dQ": "float", + "H": "float" + }, + "processingParts": { + "dataformat": "Maximums", + "columns_to_use": { + "ID": "string", + "Time Qmax": "float", + "Qmax": "float", + "dQmax": "float", + "Time dQmax": "float", + "H": "float" + } + } + }, "Q": { "processor": "QProcessor", "suffixes": [ diff --git a/ryan_library/processors/tuflow/RLLQmxProcessor.py b/ryan_library/processors/tuflow/RLLQmxProcessor.py new file mode 100644 index 00000000..1f858ef1 --- /dev/null +++ b/ryan_library/processors/tuflow/RLLQmxProcessor.py @@ -0,0 +1,60 @@ +# ryan_library/processors/tuflow/rll_qmx_processor.py + +import pandas as pd +from loguru import logger + +from .base_processor import BaseProcessor + + +class RLLQmxProcessor(BaseProcessor): + """Processor for '_RLL_Qmx.csv' files produced by the Reporting Location Lines maximum export.""" + + def process(self) -> pd.DataFrame: + """Process the '_RLL_Qmx.csv' file and return the cleaned DataFrame.""" + logger.info(f"Starting processing of RLL Qmx file: {self.file_path}") + try: + status: int = self.read_maximums_csv() + if status != 0: + logger.error(f"Processing aborted for file: {self.file_path} due to previous errors.") + self.df = pd.DataFrame() + return self.df + + self._reshape_rll_qmx_data() + + self.add_common_columns() + self.apply_output_transformations() + + if not self.validate_data(): + logger.error(f"{self.file_name}: Data validation failed.") + self.df = pd.DataFrame() + return self.df + + self.processed = True + logger.info(f"Completed processing of RLL Qmx file: {self.file_path}") + return self.df + except Exception as exc: + logger.error(f"Failed to process RLL Qmx file {self.file_path}: {exc}") + self.df = pd.DataFrame() + return self.df + + def _reshape_rll_qmx_data(self) -> None: + """Rename columns and align schema with other maximum datasets.""" + required_columns: list[str] = ["ID", "Qmax", "Time Qmax", "dQmax", "Time dQmax", "H"] + missing_columns: list[str] = [col for col in required_columns if col not in self.df.columns] + if missing_columns: + logger.error( + f"{self.file_name}: Missing required columns for RLL Qmx processing: {missing_columns}" + ) + self.df = pd.DataFrame() + return + + rename_map: dict[str, str] = { + "Qmax": "Q", + "Time Qmax": "Time", + "dQmax": "dQ", + "Time dQmax": "Time dQ", + } + self.df.rename(columns=rename_map, inplace=True) + + ordered_columns: list[str] = ["ID", "Time", "Q", "dQ", "Time dQ", "H"] + self.df = self.df[ordered_columns] diff --git a/ryan_library/processors/tuflow/processor_collection.py b/ryan_library/processors/tuflow/processor_collection.py index b42782e5..01947897 100644 --- a/ryan_library/processors/tuflow/processor_collection.py +++ b/ryan_library/processors/tuflow/processor_collection.py @@ -3,7 +3,7 @@ from collections.abc import Collection from loguru import logger import pandas as pd -from pandas import DataFrame +from pandas import DataFrame, Series from ryan_library.functions.dataframe_helpers import ( reorder_columns, reorder_long_columns, @@ -58,7 +58,9 @@ def filter_locations(self, locations: Collection[str] | None) -> frozenset[str]: continue processor.filter_locations(normalized_locations) - filtered_processors: list[BaseProcessor] = [processor for processor in self.processors if not processor.df.empty] + filtered_processors: list[BaseProcessor] = [ + processor for processor in self.processors if not processor.df.empty + ] removed_processors: int = len(self.processors) - len(filtered_processors) self.processors = filtered_processors @@ -107,9 +109,7 @@ def combine_1d_timeseries(self) -> pd.DataFrame: columns_to_drop: list[str] = ["file", "rel_path", "path", "directory_path"] # Check for existing columns and drop them - existing_columns_to_drop: list[str] = [ - col for col in columns_to_drop if col in combined_df.columns - ] + existing_columns_to_drop: list[str] = [col for col in columns_to_drop if col in combined_df.columns] if existing_columns_to_drop: combined_df.drop(columns=existing_columns_to_drop, inplace=True) logger.debug(f"Dropped columns {existing_columns_to_drop} from DataFrame.") @@ -118,9 +118,7 @@ def combine_1d_timeseries(self) -> pd.DataFrame: # Reset categorical ordering # Group by 'internalName', 'Chan ID', and 'Time' group_keys: list[str] = ["internalName", "Chan ID", "Time"] - missing_keys: list[str] = [ - key for key in group_keys if key not in combined_df.columns - ] + missing_keys: list[str] = [key for key in group_keys if key not in combined_df.columns] if missing_keys: logger.error(f"Missing group keys {missing_keys} in Timeseries data.") return pd.DataFrame() @@ -128,9 +126,7 @@ def combine_1d_timeseries(self) -> pd.DataFrame: combined_df = reorder_long_columns(df=combined_df) grouped_df: DataFrame = combined_df.groupby(group_keys).agg("max").reset_index() - logger.debug( - f"Grouped {len(timeseries_processors)} Timeseries DataFrame with {len(grouped_df)} rows." - ) + logger.debug(f"Grouped {len(timeseries_processors)} Timeseries DataFrame with {len(grouped_df)} rows.") return grouped_df @@ -153,44 +149,54 @@ def combine_1d_maximums(self) -> pd.DataFrame: return pd.DataFrame() # Concatenate DataFrames - combined_df: DataFrame = pd.concat( - [p.df for p in maximums_processors if not p.df.empty], ignore_index=True - ) + combined_df: DataFrame = pd.concat([p.df for p in maximums_processors if not p.df.empty], ignore_index=True) logger.debug(f"Combined Maximums/ccA DataFrame with {len(combined_df)} rows.") # Columns to drop columns_to_drop: list[str] = ["file", "rel_path", "path", "Time"] # Check for existing columns and drop them - existing_columns_to_drop: list[str] = [ - col for col in columns_to_drop if col in combined_df.columns - ] + existing_columns_to_drop: list[str] = [col for col in columns_to_drop if col in combined_df.columns] if existing_columns_to_drop: combined_df.drop(columns=existing_columns_to_drop, inplace=True) logger.debug(f"Dropped columns {existing_columns_to_drop} from DataFrame.") combined_df = reorder_long_columns(df=combined_df) + combined_df = self._ensure_location_identifier(df=combined_df) # Reset categorical ordering combined_df = reset_categorical_ordering(combined_df) - # Group by 'internalName' and 'Chan ID' - group_keys: list[str] = ["internalName", "Chan ID"] - missing_keys: list[str] = [ - key for key in group_keys if key not in combined_df.columns - ] + if "Location ID" not in combined_df.columns: + logger.error("Location ID column is missing after maximums preprocessing.") + return pd.DataFrame() + + missing_location_mask: Series[bool] = combined_df["Location ID"].isna() + if missing_location_mask.any(): + logger.warning( + "Dropping {count} rows without a location identifier from Maximums/ccA data.", + count=int(missing_location_mask.sum()), + ) + combined_df = combined_df[~missing_location_mask] + if combined_df.empty: + logger.error("No Maximums/ccA data remaining after removing rows without location identifiers.") + return pd.DataFrame() + + # Group by 'internalName' and the derived 'Location ID' + group_keys: list[str] = ["internalName", "Location ID"] + missing_keys: list[str] = [key for key in group_keys if key not in combined_df.columns] if missing_keys: logger.error(f"Missing group keys {missing_keys} in Maximums/ccA data.") return pd.DataFrame() - grouped_df: DataFrame = ( - combined_df.groupby(by=group_keys, observed=False).agg("max").reset_index() - ) + grouped_df: DataFrame = combined_df.groupby(by=group_keys, observed=False).agg("max").reset_index() p1_col: list[str] = [ "trim_runcode", "aep_text", "duration_text", "tp_text", + "Location ID", "Chan ID", + "ID", "Q", "V", "DS_h", @@ -219,12 +225,34 @@ def combine_1d_maximums(self) -> pd.DataFrame: prefix_order=["R"], second_priority_columns=p2_col, ) - logger.debug( - f"Grouped {len(maximums_processors)} Maximums/ccA DataFrame with {len(grouped_df)} rows." - ) + logger.debug(f"Grouped {len(maximums_processors)} Maximums/ccA DataFrame with {len(grouped_df)} rows.") logger.debug("line157") return grouped_df + def _ensure_location_identifier(self, df: DataFrame) -> DataFrame: + """Ensure a 'Location ID' column exists for grouping maximum datasets.""" + if df.empty: + df["Location ID"] = pd.Series(dtype="string") + return df + + candidate_columns: list[str] = ["Chan ID", "ID", "Location"] + available_columns: list[str] = [col for col in candidate_columns if col in df.columns] + + if not available_columns: + logger.error("No location-based columns available to derive 'Location ID'.") + df["Location ID"] = pd.Series(pd.NA, index=df.index, dtype="string") + return df + + location_series: pd.Series = pd.Series(pd.NA, index=df.index, dtype="string") + for column in available_columns: + source_values: Series[str] = df[column].astype("string") + mask: Series[bool] = location_series.isna() & source_values.notna() + if mask.any(): + location_series.loc[mask] = source_values.loc[mask] + + df["Location ID"] = location_series + return df + def combine_raw(self) -> pd.DataFrame: """Concatenate all DataFrames together without any grouping. @@ -233,9 +261,7 @@ def combine_raw(self) -> pd.DataFrame: logger.debug("Combining raw data without grouping.") # Concatenate all DataFrames - combined_df: DataFrame = pd.concat( - [p.df for p in self.processors if not p.df.empty], ignore_index=True - ) + combined_df: DataFrame = pd.concat([p.df for p in self.processors if not p.df.empty], ignore_index=True) logger.debug(f"Combined Raw DataFrame with {len(combined_df)} rows.") combined_df = reorder_long_columns(df=combined_df) @@ -254,21 +280,15 @@ def pomm_combine(self) -> pd.DataFrame: logger.debug("Combining POMM data.") # Filter processors with dataformat 'POMM' - pomm_processors: list[BaseProcessor] = [ - p for p in self.processors if p.dataformat.lower() == "pomm" - ] + pomm_processors: list[BaseProcessor] = [p for p in self.processors if p.dataformat.lower() == "pomm"] if not pomm_processors: logger.warning("No processors with dataformat 'POMM' found.") return pd.DataFrame() # Concatenate DataFrames - combined_df: DataFrame = pd.concat( - [p.df for p in pomm_processors if not p.df.empty], ignore_index=True - ) - logger.debug( - f"Combined {len(pomm_processors)} POMM DataFrame with {len(combined_df)} rows." - ) + combined_df: DataFrame = pd.concat([p.df for p in pomm_processors if not p.df.empty], ignore_index=True) + logger.debug(f"Combined {len(pomm_processors)} POMM DataFrame with {len(combined_df)} rows.") combined_df = reorder_long_columns(df=combined_df) @@ -286,21 +306,15 @@ def po_combine(self) -> pd.DataFrame: logger.debug("Combining PO data.") # Filter processors with dataformat 'PO' - po_processors: list[BaseProcessor] = [ - p for p in self.processors if p.dataformat.lower() == "po" - ] + po_processors: list[BaseProcessor] = [p for p in self.processors if p.dataformat.lower() == "po"] if not po_processors: logger.warning("No processors with dataformat 'PO' found.") return pd.DataFrame() # Concatenate DataFrames - combined_df = pd.concat( - [p.df for p in po_processors if not p.df.empty], ignore_index=True - ) - logger.debug( - f"Combined {len(po_processors)} PO DataFrame with {len(combined_df)} rows." - ) + combined_df = pd.concat([p.df for p in po_processors if not p.df.empty], ignore_index=True) + logger.debug(f"Combined {len(po_processors)} PO DataFrame with {len(combined_df)} rows.") combined_df: DataFrame = reorder_long_columns(df=combined_df) @@ -309,9 +323,7 @@ def po_combine(self) -> pd.DataFrame: return combined_df - def get_processors_by_data_type( - self, data_types: list[str] | str - ) -> "ProcessorCollection": + def get_processors_by_data_type(self, data_types: list[str] | str) -> "ProcessorCollection": """Retrieve processors matching a specific data_type or list of data_types. Args: @@ -366,8 +378,7 @@ def check_duplicates(self) -> dict[tuple[str, str], list[BaseProcessor]]: for (run_code, dtype), procs in duplicates.items(): files = ", ".join(p.file_name for p in procs) logger.warning( - f"Potential duplicate group: run_code='{run_code}', " - f"data_type='{dtype}' found in files: {files}" + f"Potential duplicate group: run_code='{run_code}', " f"data_type='{dtype}' found in files: {files}" ) else: logger.debug("No duplicate processors found by run_code & data_type.") diff --git a/setup.py b/setup.py index 09affacf..585cdc3d 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ name="ryan_functions", # Version scheme: yy.mm.dd.release_number # Increment when publishing new wheels - version="25.11.07.4", + version="25.11.11.2", packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]), include_package_data=True, # Include package data as specified in MANIFEST.in # package_data={"ryan_library": ["py.typed"]}, From ac5752ea22f350f0217c22bf90ddbeb9485cc280 Mon Sep 17 00:00:00 2001 From: Chain Frost Date: Tue, 11 Nov 2025 21:07:45 +0800 Subject: [PATCH 09/11] column defs --- ryan_library/classes/column_definitions.py | 140 +++++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/ryan_library/classes/column_definitions.py b/ryan_library/classes/column_definitions.py index 9809ff85..853221e1 100644 --- a/ryan_library/classes/column_definitions.py +++ b/ryan_library/classes/column_definitions.py @@ -68,6 +68,11 @@ def default(cls) -> "ColumnMetadataRegistry": description="Absolute maximum magnitude observed within the event time-series.", value_type="float", ), + "AbsValue": ColumnDefinition( + name="AbsValue", + description="Absolute value of the 2d_po timeseries result (ignoring sign).", + value_type="float", + ), "SignedAbsMax": ColumnDefinition( name="SignedAbsMax", description="Absolute maximum magnitude preserving the original sign (positive/negative).", @@ -83,6 +88,16 @@ def default(cls) -> "ColumnMetadataRegistry": description="Minimum value in the event window.", value_type="float", ), + "Area_Culv": ColumnDefinition( + name="Area_Culv", + description="Full cross-sectional area of the culvert barrel reported by ccA outputs.", + value_type="float", + ), + "Area_Max": ColumnDefinition( + name="Area_Max", + description="Maximum wetted area recorded for the culvert during the event (ccA output).", + value_type="float", + ), "Tmax": ColumnDefinition( name="Tmax", description="Time (hours) at which the maximum value occurs.", @@ -115,11 +130,91 @@ def default(cls) -> "ColumnMetadataRegistry": ), value_type="string", ), + "Time": ColumnDefinition( + name="Time", + description="Simulation time (hours) corresponding to the recorded statistic.", + value_type="float", + ), + "Q": ColumnDefinition( + name="Q", + description="Discharge/flow rate reported for the location.", + value_type="float", + ), + "V": ColumnDefinition( + name="V", + description="Velocity reported for the location.", + value_type="float", + ), "Type": ColumnDefinition( name="Type", description="2d_po quantity type (for example Flow, Water Level, Velocity).", value_type="string", ), + "DS_h": ColumnDefinition( + name="DS_h", + description="Downstream water level extracted from maximum-result CSVs (usually metres AHD).", + value_type="float", + ), + "US_h": ColumnDefinition( + name="US_h", + description="Upstream water level extracted from maximum-result CSVs (usually metres AHD).", + value_type="float", + ), + "DS_H": ColumnDefinition( + name="DS_H", + description="Downstream water level taken from the 1d_H timeseries output.", + value_type="float", + ), + "US_H": ColumnDefinition( + name="US_H", + description="Upstream water level taken from the 1d_H timeseries output.", + value_type="float", + ), + "US Invert": ColumnDefinition( + name="US Invert", + description="Invert elevation at the upstream end of the culvert/channel.", + value_type="float", + ), + "DS Invert": ColumnDefinition( + name="DS Invert", + description="Invert elevation at the downstream end of the culvert/channel.", + value_type="float", + ), + "US Obvert": ColumnDefinition( + name="US Obvert", + description="Crown/obvert elevation at the upstream end of the culvert/channel.", + value_type="float", + ), + "Height": ColumnDefinition( + name="Height", + description="Barrel height or diameter used for the culvert/channel definition.", + value_type="float", + ), + "Length": ColumnDefinition( + name="Length", + description="Culvert or channel length taken from the 1d_Chan file.", + value_type="float", + ), + "n or Cd": ColumnDefinition( + name="n or Cd", + description="Manning's n roughness (for open channels) or discharge coefficient (for culverts).", + value_type="float", + ), + "pSlope": ColumnDefinition( + name="pSlope", + description="Design slope for the structure (percent).", + value_type="float", + ), + "pBlockage": ColumnDefinition( + name="pBlockage", + description="Blockage percentage applied to the culvert/barrel.", + value_type="float", + ), + "Flags": ColumnDefinition( + name="Flags", + description="Source flags describing channel or culvert configuration issues.", + value_type="string", + ), "H": ColumnDefinition( name="H", description="Water level reported for the location at the time of the maximum flow event.", @@ -225,6 +320,51 @@ def default(cls) -> "ColumnMetadataRegistry": description="Fifth segment of the run code.", value_type="string", ), + "Value": ColumnDefinition( + name="Value", + description="Raw 2d_po metric captured at the reporting location.", + value_type="float", + ), + "pFull_Max": ColumnDefinition( + name="pFull_Max", + description="Maximum percent full reported by ccA.", + value_type="float", + ), + "pTime_Full": ColumnDefinition( + name="pTime_Full", + description="Time (hours) at which ccA reports the culvert as maximum percent full.", + value_type="float", + ), + "Dur_Full": ColumnDefinition( + name="Dur_Full", + description="Duration (hours) the culvert remained full according to ccA.", + value_type="float", + ), + "Dur_10pFull": ColumnDefinition( + name="Dur_10pFull", + description="Duration (hours) the culvert remained above 10 percent full (ccA output).", + value_type="float", + ), + "Sur_CD": ColumnDefinition( + name="Sur_CD", + description="Surcharge coefficient/depth reported by ccA when the culvert is surcharged.", + value_type="float", + ), + "Dur_Sur": ColumnDefinition( + name="Dur_Sur", + description="Duration (hours) the culvert experienced surcharge conditions.", + value_type="float", + ), + "pTime_Sur": ColumnDefinition( + name="pTime_Sur", + description="Time (hours) at which the surcharge condition peaked.", + value_type="float", + ), + "TFirst_Sur": ColumnDefinition( + name="TFirst_Sur", + description="Time (hours) when the surcharge condition first began.", + value_type="float", + ), "MedianAbsMax": ColumnDefinition( name="MedianAbsMax", description="Absolute maxima across median of temporal patterns for the group.", From e27d9dda3f588dd7a319dc66b0ae2fb943cd9d86 Mon Sep 17 00:00:00 2001 From: Chain Frost Date: Sun, 16 Nov 2025 11:48:44 +0800 Subject: [PATCH 10/11] added run_hy8 submodule --- .gitmodules | 3 + repo-scripts/update_run_hy8.py | 121 ++++++++++++++++++ .../functions/tuflow/tuflow_common.py | 5 +- tests/sample_scenarios.json | 95 ++++++++++++++ vendor/__init__.py | 15 ++- vendor/run_hy8 | 1 + vendor/run_hy8.UPSTREAM | 8 ++ 7 files changed, 243 insertions(+), 5 deletions(-) create mode 100644 repo-scripts/update_run_hy8.py create mode 100644 tests/sample_scenarios.json create mode 160000 vendor/run_hy8 create mode 100644 vendor/run_hy8.UPSTREAM diff --git a/.gitmodules b/.gitmodules index 70954703..c8018ada 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "unsorted"] path = unsorted url = https://github.com/Chain-Frost/ryan-tools-unsorted.git +[submodule "vendor/run_hy8"] + path = vendor/run_hy8 + url = https://github.com/Chain-Frost/run-hy8.git diff --git a/repo-scripts/update_run_hy8.py b/repo-scripts/update_run_hy8.py new file mode 100644 index 00000000..37e1ef7f --- /dev/null +++ b/repo-scripts/update_run_hy8.py @@ -0,0 +1,121 @@ +""" +Utilities for synchronizing the run-hy8 vendored submodule. + +Running this script will pull the latest upstream commit for vendor/run_hy8 and +refresh vendor/run_hy8.UPSTREAM with the new commit hash and retrieval date. +""" + +from __future__ import annotations + +import argparse +import subprocess +from datetime import date +from pathlib import Path + +REPO_URL = "https://github.com/Chain-Frost/run-hy8.git" +SUBMODULE_PATH: Path = Path("vendor") / "run_hy8" +UPSTREAM_FILE: Path = Path("vendor") / "run_hy8.UPSTREAM" +INSTRUCTIONS = ( + "Update instructions:\n" + "1. From repository root run `git submodule update --remote vendor/run_hy8`.\n" + "2. Verify the new commit hash matches expectations and update this file.\n" + "3. Commit the submodule pointer change alongside any dependent code changes.\n" +) + + +def parse_args() -> argparse.Namespace: + """Collect CLI arguments so the script can be automated or customized.""" + parser = argparse.ArgumentParser( + description="Update the run-hy8 submodule and refresh metadata.", allow_abbrev=False + ) + parser.add_argument( + "--root", + type=Path, + default=None, + help="Repository root (defaults to auto-detection based on this script).", + ) + parser.add_argument( + "--skip-update", + action="store_true", + help="Only rewrite metadata using the currently checked-out submodule commit.", + ) + return parser.parse_args() + + +def find_repo_root(preferred: Path | None) -> Path: + """Walk upward from the guessed location until we find a `.git` directory.""" + if preferred is not None: + root_candidate: Path = preferred.resolve() + else: + root_candidate = Path(__file__).resolve().parent + + for path in (root_candidate, *root_candidate.parents): + if (path / ".git").exists(): + return path + raise RuntimeError("Unable to locate repository root (missing .git directory).") + + +def capture(args: list[str], cwd: Path, *, check: bool = True) -> str: + """Run a subprocess and return stdout, optionally surfacing non-zero exits.""" + result: subprocess.CompletedProcess[str] = subprocess.run(args=args, cwd=cwd, capture_output=True, text=True) + if check and result.returncode != 0: + raise RuntimeError( + f"Command {' '.join(args)} failed with exit code {result.returncode}:\n{result.stderr.strip()}" + ) + return result.stdout.strip() + + +def update_submodule(root: Path) -> None: + """Fetch the latest upstream commit for the vendor submodule.""" + capture( + args=["git", "submodule", "update", "--init", "--remote", str(object=SUBMODULE_PATH).replace("\\", "/")], + cwd=root, + ) + + +def current_commit(submodule_dir: Path) -> tuple[str, str]: + """Read the detached commit hash plus a friendly ref (branch or 'detached').""" + commit_hash: str = capture(args=["git", "rev-parse", "HEAD"], cwd=submodule_dir) + try: + branch: str = capture(args=["git", "symbolic-ref", "--quiet", "--short", "HEAD"], cwd=submodule_dir) + except RuntimeError: + branch = "detached" + return commit_hash, branch + + +def write_metadata(root: Path, commit_hash: str, branch: str) -> Path: + """Rewrite vendor/run_hy8.UPSTREAM with the new commit and date.""" + content: str = ( + f"Repository: {REPO_URL}\n" + f"Commit: {commit_hash} ({branch})\n" + f"Retrieved: {date.today().isoformat()}\n\n" + f"{INSTRUCTIONS}" + ) + target: Path = root / UPSTREAM_FILE + target.write_text(data=content, encoding="utf-8") + return target + + +def main() -> int: + """High-level orchestration: update submodule (optional) and metadata.""" + args: argparse.Namespace = parse_args() + root: Path = find_repo_root(preferred=args.root) + submodule_dir: Path = root / SUBMODULE_PATH + if not submodule_dir.exists(): + raise FileNotFoundError(f"Submodule directory {submodule_dir} does not exist. Has it been initialized?") + + if not args.skip_update: + print("Updating vendor/run_hy8 from upstream ...") + update_submodule(root) + + commit_hash, branch = current_commit(submodule_dir=submodule_dir) + metadata_path: Path = write_metadata(root=root, commit_hash=commit_hash, branch=branch) + + print(f"Vendor submodule now points at {commit_hash} ({branch}).") + print(f"Wrote metadata to {metadata_path.relative_to(root)}.") + print("Remember to commit the submodule pointer and metadata file together.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/ryan_library/functions/tuflow/tuflow_common.py b/ryan_library/functions/tuflow/tuflow_common.py index bfbb35de..a64fdfaa 100644 --- a/ryan_library/functions/tuflow/tuflow_common.py +++ b/ryan_library/functions/tuflow/tuflow_common.py @@ -3,10 +3,7 @@ from pathlib import Path from multiprocessing import Pool from dataclasses import dataclass, field -from queue import Queue from typing import Any - -import pandas as pd from loguru import logger from ryan_library.functions.file_utils import ( @@ -14,7 +11,7 @@ is_non_zero_file, ) from ryan_library.functions.misc_functions import calculate_pool_size -from ryan_library.functions.loguru_helpers import setup_logger, worker_initializer +from ryan_library.functions.loguru_helpers import worker_initializer from ryan_library.processors.tuflow.base_processor import BaseProcessor from ryan_library.processors.tuflow.processor_collection import ProcessorCollection from ryan_library.classes.suffixes_and_dtypes import SuffixesConfig diff --git a/tests/sample_scenarios.json b/tests/sample_scenarios.json new file mode 100644 index 00000000..0356e86f --- /dev/null +++ b/tests/sample_scenarios.json @@ -0,0 +1,95 @@ +[ + { + "index": 0, + "internalName": "Sawmill Creek South", + "Chan ID": "C-001", + "Q": 0.01, + "V": "", + "US_h": 95.5, + "DS_h": 94.8, + "Length": 34.5, + "n or Cd": 0.024, + "US Invert": 94.0, + "DS Invert": 93.5, + "Height": 0.9, + "number_interp": 1, + "roadway_crest": 105.0 + }, + { + "index": 1, + "internalName": "Sawmill Creek Spur", + "Chan ID": "C-001A", + "Q": 0.02, + "V": "", + "US_h": 95.5, + "DS_h": 94.6, + "Length": 32.0, + "n or Cd": 0.024, + "US Invert": 94.1, + "DS Invert": 93.7, + "Height": 0.9, + "number_interp": 3, + "roadway_crest": 105.0 + }, + { + "index": 2, + "internalName": "Dry Gulch Crossing", + "Chan ID": "C-002", + "Q": 0.03, + "V": "", + "US_h": 96.4, + "DS_h": 95.9, + "Length": 28.0, + "n or Cd": 0.022, + "US Invert": 95.2, + "DS Invert": 95.0, + "Height": 1.0, + "number_interp": 1 + }, + { + "index": 3, + "internalName": "Dry Gulch Spur", + "Chan ID": "C-002A", + "Q": 0.01, + "V": "", + "US_h": 96.4, + "DS_h": 95.8, + "Length": 30.0, + "n or Cd": 0.025, + "US Invert": 95.4, + "DS Invert": 95.1, + "Height": 0.8, + "number_interp": 1 + }, + { + "index": 4, + "internalName": "Irrigation Canal", + "Chan ID": "C-003", + "Q": 0.01, + "V": "", + "US_h": 96.9, + "DS_h": 96.1, + "Length": 36.0, + "n or Cd": 0.024, + "US Invert": 95.7, + "DS Invert": 95.6, + "Height": 1.1, + "number_interp": 2 + }, + { + "index": 5, + "internalName": "Coastal Relief", + "Chan ID": "C-004", + "Q": 0.05, + "V": "", + "US_h": 14.8, + "DS_h": 14.2, + "Length": 20.0, + "n or Cd": 0.015, + "US Invert": 13.5, + "DS Invert": 13.1, + "Height": 1.5, + "number_interp": 2, + "roadway_crest": 22.0 + } +] diff --git a/vendor/__init__.py b/vendor/__init__.py index 211e1901..7b2a5ab3 100644 --- a/vendor/__init__.py +++ b/vendor/__init__.py @@ -1 +1,14 @@ -from .PyHMA import * +from __future__ import annotations + +import sys +from pathlib import Path + +# Re-export PyHMA to maintain existing imports. +from .PyHMA import * # noqa: F401,F403 + +# Make the run-hy8 submodule importable without installing it separately. +RUN_HY8_SRC: Path = Path(__file__).with_name("run_hy8") / "src" +if RUN_HY8_SRC.is_dir(): + run_hy8_src = str(RUN_HY8_SRC.resolve()) + if run_hy8_src not in sys.path: + sys.path.append(run_hy8_src) diff --git a/vendor/run_hy8 b/vendor/run_hy8 new file mode 160000 index 00000000..617d45e3 --- /dev/null +++ b/vendor/run_hy8 @@ -0,0 +1 @@ +Subproject commit 617d45e3bc0a7f3eed6a1d30cd6a546f6cb57a8a diff --git a/vendor/run_hy8.UPSTREAM b/vendor/run_hy8.UPSTREAM new file mode 100644 index 00000000..6cb84e06 --- /dev/null +++ b/vendor/run_hy8.UPSTREAM @@ -0,0 +1,8 @@ +Repository: https://github.com/Chain-Frost/run-hy8.git +Commit: 617d45e3bc0a7f3eed6a1d30cd6a546f6cb57a8a (main) +Retrieved: 2025-11-16 + +Update instructions: +1. From repository root run `git submodule update --remote vendor/run_hy8`. +2. Verify the new commit hash matches expectations and update this file. +3. Commit the submodule pointer change alongside any dependent code changes. From 32be88859beca4156b733685c1e34802afae0a52 Mon Sep 17 00:00:00 2001 From: Chain Frost Date: Sun, 16 Nov 2025 13:11:30 +0800 Subject: [PATCH 11/11] adjust tuflowstringparser for extra bits, refactor POMM processing workflows --- ...yan_functions-25.11.16.1-py3-none-any.whl} | Bin 141947 -> 218621 bytes ryan_library/classes/tuflow_string_classes.py | 139 ++++++++++- ryan_library/functions/tuflow/pomm_combine.py | 6 +- .../functions/tuflow/tuflow_common.py | 72 +++++- ryan_library/scripts/pomm_utils.py | 232 +++++------------- setup.py | 2 +- .../tuflow/tutorials/TUFLOW_Culvert_Merge.py | 33 --- vendor/run_hy8 | 2 +- vendor/run_hy8.UPSTREAM | 2 +- 9 files changed, 255 insertions(+), 233 deletions(-) rename dist/{ryan_functions-25.11.11.2-py3-none-any.whl => ryan_functions-25.11.16.1-py3-none-any.whl} (52%) delete mode 100644 tests/test_data/tuflow/tutorials/TUFLOW_Culvert_Merge.py diff --git a/dist/ryan_functions-25.11.11.2-py3-none-any.whl b/dist/ryan_functions-25.11.16.1-py3-none-any.whl similarity index 52% rename from dist/ryan_functions-25.11.11.2-py3-none-any.whl rename to dist/ryan_functions-25.11.16.1-py3-none-any.whl index 08c14b347373a19324c3f02c37f091dc3e7c6202..97214e880c720b6606b73ba869defdc47b8fc4a7 100644 GIT binary patch delta 98341 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 z

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 biuS4+8n8b?LGcIvrE#I4$Z-NI3(Efka>$Vl delta 21067 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 HIE4NKs3X*2 diff --git a/ryan_library/classes/tuflow_string_classes.py b/ryan_library/classes/tuflow_string_classes.py index a69fb0ec..1af77f1f 100644 --- a/ryan_library/classes/tuflow_string_classes.py +++ b/ryan_library/classes/tuflow_string_classes.py @@ -1,4 +1,5 @@ # ryan_library\classes\tuflow_string_classes.py +import math import re from pathlib import Path from loguru import logger @@ -64,7 +65,9 @@ class TuflowStringParser: # Precompile regex patterns for efficiency # ``TP_PATTERN`` finds patterns like ``TP01`` that are surrounded by ``_`` or ``+`` (or appear at the edges). - TP_PATTERN: re.Pattern[str] = re.compile(pattern=r"(?:[_+]|^)TP(\d{2})(?:[_+]|$)", flags=re.IGNORECASE) + # Keep the core ``TP##``/``TP#`` style bounded by delimiters so we do not + # accidentally read other numbers embedded within filenames. + TP_PATTERN: re.Pattern[str] = re.compile(pattern=r"(?:[_+]|^)TP(\d{1,2})(?:[_+]|$)", flags=re.IGNORECASE) # ``DURATION_PATTERN`` captures 3-5 digits followed by ``m`` (e.g. ``00360m`` or ``360m``). DURATION_PATTERN: re.Pattern[str] = re.compile(pattern=r"(?:[_+]|^)(\d{3,5})[mM](?:[_+]|$)", flags=re.IGNORECASE) # ``AEP_PATTERN`` matches values like ``01.00p`` or ``5p`` and reads the numeric portion before ``p``. @@ -72,8 +75,14 @@ class TuflowStringParser: pattern=r"(?:^|[_+])(?P(?P\d+(?:\.\d{1,2})?)p|(?PPMPF|PMP))(?=$|[_+])", flags=re.IGNORECASE, ) + # ``GENERIC_TP_PATTERN`` handles free-form strings such as "tp 3". + GENERIC_TP_PATTERN: re.Pattern[str] = re.compile(pattern=r"TP?\s*(\d{1,2})", flags=re.IGNORECASE) + HUMAN_DURATION_PATTERN: re.Pattern[str] = re.compile( + pattern=r"(?P\d+(?:\.\d+)?)\s*(?Phours?|hrs?|hr|h|minutes?|mins?|min|m)(?=$|[^A-Za-z0-9])", + flags=re.IGNORECASE, + ) - def __init__(self, file_path: Path | str): + def __init__(self, file_path: Path | str) -> None: """Initialize the TuflowStringParser with the given file path. Args: file_path (Path | str): Path to the file to be processed.""" @@ -89,6 +98,103 @@ def __init__(self, file_path: Path | str): self.aep: RunCodeComponent | None = self.parse_aep(string=self.clean_run_code) self.trim_run_code: str = self.trim_the_run_code() + @staticmethod + def _coerce_text(value: object) -> str | None: + """Convert ``value`` into a cleaned string or ``None`` if it is effectively empty.""" + + if value is None: + return None + try: + if math.isnan(value): # type: ignore[arg-type] + return None + except (TypeError, ValueError): + pass + text: str = str(value).strip() + if not text: + return None + lowered: str = text.lower() + if lowered in {"nan", "none", ""}: + return None + return text + + @classmethod + def normalize_tp_label(cls, value: object) -> str | None: + """Return a canonical ``TP##`` label extracted from ``value`` when possible.""" + + text: str | None = cls._coerce_text(value) + if text is None: + return None + + # Normalise ``+`` to ``_`` so ``TP##`` detection works for both + # delimiter styles used in TUFLOW artefacts. + normalized: str = text.replace("+", "_") + match: re.Match[str] | None = cls.TP_PATTERN.search(normalized.upper()) + digits: str | None = match.group(1) if match else None + if digits is None: + generic_match: re.Match[str] | None = cls.GENERIC_TP_PATTERN.search(text) + if generic_match: + digits = generic_match.group(1) + if digits is None: + fallback: re.Match[str] | None = re.search(r"\b(\d{1,2})\b", text) + digits = fallback.group(1) if fallback else None + if digits is None: + return None + try: + numeric = int(digits) + except ValueError: + logger.debug(f"Unable to parse TP digits from value {text!r}") + return None + return f"TP{numeric:02d}" + + @classmethod + def normalize_duration_value(cls, value: object) -> float: + """Return the numeric component of a duration string or ``nan`` when parsing fails.""" + + text: str | None = cls._coerce_text(value) + if text is None: + return float("nan") + + # ``DURATION_PATTERN`` covers the common ``00120m`` style. + normalized: str = text.replace("+", "_") + match: re.Match[str] | None = cls.DURATION_PATTERN.search(normalized) + if match: + return float(match.group(1)) + + for human_match in cls.HUMAN_DURATION_PATTERN.finditer(normalized): + minutes: float | None = cls._minutes_from_human_match(human_match) + if minutes is not None: + return minutes + + fallback: re.Match[str] | None = re.search(r"\d{3,}", normalized) + if fallback: + return float(fallback.group(0)) + + logger.debug(f"Unable to parse duration from value {text!r}") + return float("nan") + + @staticmethod + def _minutes_from_human_match(match: re.Match[str]) -> float | None: + """Convert a ``HUMAN_DURATION_PATTERN`` match into minutes.""" + + value_str: str = match.group("value") + unit: str = match.group("unit").lower() + digits_only: str = value_str.replace(".", "") + if unit == "m" and len(digits_only) <= 2: + # Short bare ``m`` tokens (e.g. "5m") represent grid resolutions + # rather than durations, so ignore them and keep searching. + return None + + try: + magnitude = float(value_str) + except ValueError: + return None + + if unit in {"hours", "hour", "hrs", "hr", "h"}: + return magnitude * 60.0 + if unit in {"minutes", "minute", "mins", "min", "m"}: + return magnitude + return None + @staticmethod def clean_runcode(run_code: str) -> str: """Replace '+' with '_' to standardize delimiters. @@ -133,7 +239,7 @@ def extract_raw_run_code(self) -> str: """ for suffix in self.suffixes.keys(): if self.file_name.lower().endswith(suffix.lower()): - run_code = self.file_name[: -len(suffix)] + run_code: str = self.file_name[: -len(suffix)] logger.debug(f"Extracted raw run code '{run_code}' from file name '{self.file_name}'") return run_code logger.debug(f"No suffix matched; using entire file name '{self.file_name}' as run code") @@ -150,8 +256,8 @@ def extract_run_code_parts(clean_run_code: str) -> dict[str, str]: Returns: dict[str, str]: Dictionary of run code parts with keys like 'R01', 'R02', etc. """ - run_code_parts = clean_run_code.split("_") - r_dict = {f"R{index:02}": part for index, part in enumerate(run_code_parts, start=1)} + run_code_parts: list[str] = clean_run_code.split("_") + r_dict: dict[str, str] = {f"R{index:02}": part for index, part in enumerate(run_code_parts, start=1)} logger.debug(f"Extracted run code parts: {r_dict}") return r_dict @@ -165,9 +271,9 @@ def parse_tp(self, string: str) -> RunCodeComponent | None: Returns: Optional[RunCodeComponent]: Parsed TP component or None if not found. """ - match = self.TP_PATTERN.search(string) + match: re.Match[str] | None = self.TP_PATTERN.search(string) if match: - tp_value = match.group(1) + tp_value: str = match.group(1) logger.debug(f"Parsed TP value: {tp_value}") return RunCodeComponent(raw_value=tp_value, component_type="TP") logger.debug("No TP component found") @@ -183,11 +289,22 @@ def parse_duration(self, string: str) -> RunCodeComponent | None: Returns: Optional[RunCodeComponent]: Parsed Duration component or None if not found. """ - match = self.DURATION_PATTERN.search(string) + # Apply the same normalisation rules as ``normalize_duration_value`` so + # both parsing paths stay in sync. + normalized: str = string.replace("+", "_") + match: re.Match[str] | None = self.DURATION_PATTERN.search(normalized) if match: duration_value = match.group(1) logger.debug(f"Parsed Duration value: {duration_value}") return RunCodeComponent(raw_value=duration_value, component_type="Duration") + + for human_match in self.HUMAN_DURATION_PATTERN.finditer(normalized): + minutes: float | None = self._minutes_from_human_match(human_match) + if minutes is None: + continue + minutes_str: str = str(int(minutes)) if minutes.is_integer() else str(minutes) + logger.debug(f"Parsed Duration value from human-readable token: {minutes_str}") + return RunCodeComponent(raw_value=minutes_str, component_type="Duration") logger.debug("No Duration component found") return None @@ -219,9 +336,11 @@ def trim_the_run_code(self) -> str: Returns: str: Cleaned run code. """ - components_to_remove = {str(component).lower() for component in [self.aep, self.duration, self.tp] if component} + components_to_remove: set[str] = { + str(component).lower() for component in [self.aep, self.duration, self.tp] if component + } logger.debug(f"Components to remove: {components_to_remove}") - trimmed_runcode = "_".join( + trimmed_runcode: str = "_".join( part for part in self.clean_run_code.split("_") if part.lower() not in components_to_remove ) logger.debug(f"Trimmed run code: {trimmed_runcode}") diff --git a/ryan_library/functions/tuflow/pomm_combine.py b/ryan_library/functions/tuflow/pomm_combine.py index 9f0657e4..4edd270e 100644 --- a/ryan_library/functions/tuflow/pomm_combine.py +++ b/ryan_library/functions/tuflow/pomm_combine.py @@ -7,10 +7,8 @@ import pandas as pd from loguru import logger -from ryan_library.scripts.pomm_utils import ( - collect_files, - process_files_in_parallel, -) +from ryan_library.scripts.pomm_utils import process_files_in_parallel +from ryan_library.functions.tuflow.tuflow_common import collect_files from ryan_library.processors.tuflow.base_processor import BaseProcessor from ryan_library.processors.tuflow.processor_collection import ProcessorCollection from ryan_library.functions.file_utils import ensure_output_directory diff --git a/ryan_library/functions/tuflow/tuflow_common.py b/ryan_library/functions/tuflow/tuflow_common.py index a64fdfaa..a7f80026 100644 --- a/ryan_library/functions/tuflow/tuflow_common.py +++ b/ryan_library/functions/tuflow/tuflow_common.py @@ -3,6 +3,7 @@ from pathlib import Path from multiprocessing import Pool from dataclasses import dataclass, field +from collections.abc import Iterable from typing import Any from loguru import logger @@ -17,6 +18,10 @@ from ryan_library.classes.suffixes_and_dtypes import SuffixesConfig +def _string_list() -> list[str]: + return [] + + @dataclass(frozen=True) class ScenarioConfig: """One export scenario.""" @@ -25,31 +30,72 @@ class ScenarioConfig: parquet_prefix: str # e.g. "1d_timeseries_data" excel_sheet: str # e.g. "Timeseries" export_parquet: bool # only True for timeseries - column_order: list[str] = field(default_factory=list) + column_order: list[str] = field(default_factory=_string_list) def collect_files( - paths_to_process: list[Path], - include_data_types: list[str], + paths_to_process: Iterable[Path], + include_data_types: Iterable[str], suffixes_config: SuffixesConfig, ) -> list[Path]: + """Return all non-empty files matching ``include_data_types`` underneath ``paths_to_process``.""" + + normalized_roots: list[Path] = [] + seen_roots: set[Path] = set() + for candidate in paths_to_process: + path = Path(candidate) + if path in seen_roots: + continue + seen_roots.add(path) + normalized_roots.append(path) + + deduped_types: list[str] = [] + seen_types: set[str] = set() + for data_type in include_data_types: + if data_type in seen_types: + continue + seen_types.add(data_type) + deduped_types.append(data_type) + + if not deduped_types: + logger.error("No data types were supplied for file collection.") + return [] + data_map: dict[str, list[str]] = suffixes_config.invert_suffix_to_type() suffixes: list[str] = [] - for dt in include_data_types: - if dt in data_map: - suffixes.extend(data_map[dt]) - else: - logger.error(f"No suffixes for data type '{dt}'") + for data_type in deduped_types: + dt_suffixes: list[str] | None = data_map.get(data_type) + if not dt_suffixes: + logger.error(f"No suffixes for data type '{data_type}'. Skipping.") + continue + suffixes.extend(dt_suffixes) + + suffixes = list(dict.fromkeys(suffixes)) if not suffixes: + logger.error("No suffixes found for the requested data types.") return [] - patterns: list[str] = [f"*{s}" for s in suffixes] - roots: list[Path] = [p for p in paths_to_process if p.is_dir()] - for bad in set(paths_to_process) - set(roots): - logger.warning(f"Skipping non-dir {bad}") + patterns: list[str] = [f"*{suffix}" for suffix in suffixes] + roots: list[Path] = [p for p in normalized_roots if p.is_dir()] + invalid_roots: list[Path] = [p for p in normalized_roots if not p.is_dir()] + for bad_root in invalid_roots: + logger.warning(f"Skipping non-directory path {bad_root}") + + if not roots: + logger.warning("No valid directories were supplied for file collection.") + return [] files: list[Path] = find_files_parallel(root_dirs=roots, patterns=patterns) - return [f for f in files if is_non_zero_file(f)] + filtered_files: list[Path] = [] + seen_files: set[Path] = set() + for file_path in files: + if not is_non_zero_file(file_path): + continue + if file_path in seen_files: + continue + seen_files.add(file_path) + filtered_files.append(file_path) + return filtered_files def process_file(file_path: Path) -> BaseProcessor | None: diff --git a/ryan_library/scripts/pomm_utils.py b/ryan_library/scripts/pomm_utils.py index eb41e390..8b31ecfd 100644 --- a/ryan_library/scripts/pomm_utils.py +++ b/ryan_library/scripts/pomm_utils.py @@ -3,26 +3,23 @@ from pathlib import Path from multiprocessing import Pool -from collections.abc import Collection, Iterable, Mapping +from collections.abc import Collection, Mapping, Sequence from datetime import datetime, timezone -from importlib.metadata import PackageNotFoundError, version from typing import Any import pandas as pd from loguru import logger +from pandas import Series -from ryan_library.classes.column_definitions import ColumnMetadataRegistry +from ryan_library.classes.column_definitions import ColumnDefinition, ColumnMetadataRegistry from ryan_library.functions.pandas.median_calc import median_calc - -from ryan_library.functions.file_utils import ( - find_files_parallel, - is_non_zero_file, -) -from ryan_library.functions.misc_functions import ExcelExporter, calculate_pool_size +from ryan_library.functions.misc_functions import ExcelExporter, calculate_pool_size, get_tools_version from ryan_library.processors.tuflow.base_processor import BaseProcessor from ryan_library.processors.tuflow.processor_collection import ProcessorCollection from ryan_library.classes.suffixes_and_dtypes import SuffixesConfig from ryan_library.functions.loguru_helpers import setup_logger, worker_initializer +from ryan_library.functions.tuflow.tuflow_common import collect_files +from ryan_library.classes.tuflow_string_classes import TuflowStringParser NAType = type(pd.NA) @@ -30,52 +27,21 @@ DATA_DICTIONARY_SHEET_NAME: str = "data-dictionary" -def collect_files( - paths_to_process: Iterable[Path], - include_data_types: Iterable[str], - suffixes_config: SuffixesConfig, -) -> list[Path]: - """Collect and filter files based on specified data types. - - Args: - paths_to_process (list[Path]): Directories to search. - include_data_types (list[str] ): List of data types to include. - suffixes_config (SuffixesConfig ): Suffixes configuration instance. - - Returns: - list[Path]: List of valid file paths.""" - - csv_file_list: list[Path] = [] - suffixes: list[str] = [] - - # Determine which suffixes to include based on data types - # Invert suffixes config - data_type_to_suffix: dict[str, list[str]] = suffixes_config.invert_suffix_to_type() +def _ordered_columns( + df: pd.DataFrame, + column_groups: Sequence[Sequence[str]], + info_columns: Sequence[str], +) -> list[str]: + """Return a column order keeping identifiers first and meta-data last.""" - for data_type in include_data_types: - dt_suffixes: list[str] | None = data_type_to_suffix.get(data_type) - if not dt_suffixes: - logger.error(f"No suffixes found for data type '{data_type}'. Skipping.") - continue - suffixes.extend(dt_suffixes) + ordered: list[str] = [] + for group in column_groups: + ordered.extend([column for column in group if column in df.columns]) - if not suffixes: - logger.error("No suffixes found for the specified data types.") - return csv_file_list - - # Prepend '*' for wildcard searching - patterns: list[str] = [f"*{suffix}" for suffix in suffixes] - - root_dirs: list[Path] = [p for p in paths_to_process if p.is_dir()] - invalid_dirs: set[Path] = set(paths_to_process) - set(root_dirs) - for invalid_dir in invalid_dirs: - logger.warning(f"Path {invalid_dir} is not a directory. Skipping.") - - matched_files: list[Path] = find_files_parallel(root_dirs=root_dirs, patterns=patterns) - csv_file_list.extend(matched_files) - - csv_file_list = [f for f in csv_file_list if is_non_zero_file(f)] - return csv_file_list + remaining: list[str] = [column for column in df.columns if column not in ordered and column not in info_columns] + ordered.extend(remaining) + ordered.extend([column for column in info_columns if column in df.columns]) + return ordered def process_file(file_path: Path, location_filter: frozenset[str] | None = None) -> BaseProcessor: @@ -306,7 +272,7 @@ def _build_metadata_rows( "Generated at": generated_at, "Filename timestamp": timestamp if timestamp else "not supplied", "Generator module": __name__, - "ryan_functions version": _resolve_package_version("ryan_functions"), + "ryan_functions version": get_tools_version(package="ryan_functions"), "Include POMM sheet": "Yes" if include_pomm else "No", f"{aep_dur_sheet_name} rows": str(len(aep_dur_max)), f"{aep_sheet_name} rows": str(len(aep_max)), @@ -320,22 +286,13 @@ def _build_metadata_rows( directories_series = aggregated_df["directory_path"].dropna() except AttributeError: directories_series = pd.Series(dtype="string") - unique_directories = sorted({str(Path(dir_value)) for dir_value in directories_series.unique()}) + unique_directories: list[str] = sorted({str(Path(dir_value)) for dir_value in directories_series.unique()}) if unique_directories: metadata["Source directories"] = "\n".join(unique_directories) return metadata -def _resolve_package_version(package_name: str) -> str: - """Return the installed version for ``package_name`` if available.""" - - try: - return version(package_name) - except PackageNotFoundError: - return "unknown" - - def _build_data_dictionary( registry: ColumnMetadataRegistry, sheet_frames: Mapping[str, pd.DataFrame], @@ -370,7 +327,7 @@ def _build_data_dictionary( continue dtype_map: dict[str, str] = {column: str(dtype) for column, dtype in frame.dtypes.items()} - definitions = registry.iter_definitions(columns, sheet_name=sheet_name) + definitions: list[ColumnDefinition] = registry.iter_definitions(columns, sheet_name=sheet_name) for column_name, definition in zip(columns, definitions): rows.append( @@ -384,7 +341,7 @@ def _build_data_dictionary( ) return pd.DataFrame( - rows, + data=rows, columns=["sheet", "column", "description", "value_type", "pandas_dtype"], ) @@ -402,7 +359,6 @@ def save_peak_report( output_filename: str = f"{timestamp}{suffix}" output_path: Path = script_directory / output_filename logger.info(f"Starting export of peak report to {output_path}") - logger.info(f"Starting export of peak report to {output_path}") save_to_excel( aep_dur_max=aep_dur_max, aep_max=aep_max, @@ -412,7 +368,6 @@ def save_peak_report( timestamp=timestamp, ) logger.info(f"Completed peak report export to {output_path}") - logger.info(f"Completed peak report export to {output_path}") def find_aep_dur_median(aggregated_df: pd.DataFrame) -> pd.DataFrame: @@ -447,54 +402,33 @@ def find_aep_dur_median(aggregated_df: pd.DataFrame) -> pd.DataFrame: rows.append(row) median_df = pd.DataFrame(rows) if not median_df.empty: - - def norm_tp(value: str | int | float | None) -> str | NAType: + # Normalise TP / duration text so the "mean storm equals median storm" flag is stable. + def _normalize_tp(value: object) -> str | NAType: if pd.isna(value): return pd.NA - cleaned = str(value).replace("TP", "") - numeric = pd.to_numeric(cleaned, errors="coerce") - return pd.NA if pd.isna(numeric) else f"TP{int(numeric):02d}" - - def norm_duration(value: object) -> float: - if pd.isna(value): - return float("nan") - text = str(value).strip().lower() - suffixes: tuple[str, ...] = ( - "hours", - "hour", - "hrs", - "hr", - "h", - "minutes", - "minute", - "mins", - "min", - "m", - ) - for suffix in suffixes: - if text.endswith(suffix): - text = text[: -len(suffix)] - break - cleaned = text.strip() - numeric = pd.to_numeric(cleaned, errors="coerce") - return float(numeric) if pd.notna(numeric) else float("nan") + normalized = TuflowStringParser.normalize_tp_label(value) + return pd.NA if normalized is None else normalized for column in ("median_TP", "mean_TP"): if column in median_df.columns: - median_df[column] = median_df[column].apply(norm_tp) + median_df[column] = median_df[column].apply(_normalize_tp) - mean_storm_matches = pd.Series(False, index=median_df.index) - required_cols = { + mean_storm_matches: Series[bool] = pd.Series(False, index=median_df.index) + required_cols: set[str] = { "median_duration", "mean_Duration", "median_TP", "mean_TP", } if required_cols.issubset(median_df.columns): - median_duration_norm = median_df["median_duration"].map(norm_duration) - mean_duration_norm = median_df["mean_Duration"].map(norm_duration) - duration_match = median_duration_norm.eq(mean_duration_norm) - tp_match = median_df["median_TP"].eq(median_df["mean_TP"]) + median_duration_norm: Series[float] = median_df["median_duration"].map( + TuflowStringParser.normalize_duration_value + ) + mean_duration_norm: Series[float] = median_df["mean_Duration"].map( + TuflowStringParser.normalize_duration_value + ) + duration_match: Series[bool] = median_duration_norm.eq(mean_duration_norm) + tp_match: Series[bool] = median_df["median_TP"].eq(median_df["mean_TP"]) mean_storm_matches = (duration_match & tp_match).fillna(False) median_df["mean_storm_is_median_storm"] = mean_storm_matches @@ -510,15 +444,11 @@ def norm_duration(value: object) -> float: median_columns: list[str] = ["MedianAbsMax", "median_duration", "median_TP"] info_columns: list[str] = ["low", "high", "count", "count_bin", "mean_storm_is_median_storm"] - ordered_cols: list[str] = [] - for group in (id_columns, mean_columns, median_columns): - ordered_cols.extend([col for col in group if col in median_df.columns]) - - remaining_cols: list[str] = [ - col for col in median_df.columns if col not in ordered_cols and col not in info_columns - ] - ordered_cols.extend(remaining_cols) - ordered_cols.extend([col for col in info_columns if col in median_df.columns]) + ordered_cols: list[str] = _ordered_columns( + df=median_df, + column_groups=(id_columns, mean_columns, median_columns), + info_columns=info_columns, + ) median_df = median_df[ordered_cols] logger.info("Created 'aep_dur_median' DataFrame with median records for each AEP-Duration group.") @@ -551,6 +481,8 @@ def find_aep_median_max(aep_dur_median: pd.DataFrame) -> pd.DataFrame: mean_df: pd.DataFrame = aep_dur_median.copy() mean_df["_mean_peakflow_numeric"] = pd.to_numeric(mean_df.get("mean_PeakFlow"), errors="coerce") # type: ignore if mean_df["_mean_peakflow_numeric"].notna().any(): + # When the mean columns are present we track the rows that best represent + # the maximum mean independently from the median selection above. idx_mean = ( mean_df[mean_df["_mean_peakflow_numeric"].notna()] .groupby(group_cols, observed=True)["_mean_peakflow_numeric"] @@ -582,45 +514,11 @@ def find_aep_median_max(aep_dur_median: pd.DataFrame) -> pd.DataFrame: "aep_bin", ] - ordered_cols: list[str] = [] - for group in (id_columns, mean_columns, median_columns): - ordered_cols.extend([col for col in group if col in aep_med_max.columns]) - - remaining_cols: list[str] = [ - col for col in aep_med_max.columns if col not in ordered_cols and col not in info_columns - ] - ordered_cols.extend(remaining_cols) - ordered_cols.extend([col for col in info_columns if col in aep_med_max.columns]) - - aep_med_max = aep_med_max[ordered_cols] - if not aep_med_max.empty: - id_columns: list[str] = ["aep_text", "duration_text", "Location", "Type", "trim_runcode"] - mean_columns: list[str] = [ - "mean_including_zeroes", - "mean_excluding_zeroes", - "mean_PeakFlow", - "mean_Duration", - "mean_TP", - ] - median_columns: list[str] = ["MedianAbsMax", "median_duration", "median_TP"] - info_columns: list[str] = [ - "low", - "high", - "count", - "count_bin", - "mean_storm_is_median_storm", - "aep_bin", - ] - - ordered_cols: list[str] = [] - for group in (id_columns, mean_columns, median_columns): - ordered_cols.extend([col for col in group if col in aep_med_max.columns]) - - remaining_cols: list[str] = [ - col for col in aep_med_max.columns if col not in ordered_cols and col not in info_columns - ] - ordered_cols.extend(remaining_cols) - ordered_cols.extend([col for col in info_columns if col in aep_med_max.columns]) + ordered_cols: list[str] = _ordered_columns( + df=aep_med_max, + column_groups=(id_columns, mean_columns, median_columns), + info_columns=info_columns, + ) aep_med_max = aep_med_max[ordered_cols] logger.info("Created 'aep_median_max' DataFrame with maximum median records for each AEP group.") @@ -647,15 +545,13 @@ def find_aep_dur_mean(aggregated_df: pd.DataFrame) -> pd.DataFrame: ] info_columns: list[str] = ["low", "high", "count", "count_bin", "mean_storm_is_median_storm"] - ordered_cols: list[str] = [] - for group in (id_columns, mean_columns): - ordered_cols.extend([col for col in group if col in aep_dur_median.columns]) - - remaining_cols: list[str] = [ - col for col in aep_dur_median.columns if col not in ordered_cols and col not in info_columns - ] - ordered_cols.extend(remaining_cols) - ordered_cols.extend([col for col in info_columns if col in aep_dur_median.columns]) + # Keep the mean-focused statistics grouped together; later callers drop any + # residual median columns so the mean workflow can diverge independently. + ordered_cols: list[str] = _ordered_columns( + df=aep_dur_median, + column_groups=(id_columns, mean_columns), + info_columns=info_columns, + ) return aep_dur_median[ordered_cols] @@ -699,15 +595,11 @@ def find_aep_mean_max(aep_dur_mean: pd.DataFrame) -> pd.DataFrame: "mean_bin", ] - ordered_cols: list[str] = [] - for group in (id_columns, mean_columns): - ordered_cols.extend([col for col in group if col in aep_mean_max.columns]) - - remaining_cols: list[str] = [ - col for col in aep_mean_max.columns if col not in ordered_cols and col not in info_columns - ] - ordered_cols.extend(remaining_cols) - ordered_cols.extend([col for col in info_columns if col in aep_mean_max.columns]) + ordered_cols: list[str] = _ordered_columns( + df=aep_mean_max, + column_groups=(id_columns, mean_columns), + info_columns=info_columns, + ) aep_mean_max = aep_mean_max[ordered_cols] diff --git a/setup.py b/setup.py index 5420d33c..5db000f2 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ name="ryan_functions", # Version scheme: yy.mm.dd.release_number # Increment when publishing new wheels - version="25.11.11.2", + version="25.11.16.1", packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]), include_package_data=True, # Include package data as specified in MANIFEST.in # package_data={"ryan_library": ["py.typed"]}, diff --git a/tests/test_data/tuflow/tutorials/TUFLOW_Culvert_Merge.py b/tests/test_data/tuflow/tutorials/TUFLOW_Culvert_Merge.py deleted file mode 100644 index 6e621b3f..00000000 --- a/tests/test_data/tuflow/tutorials/TUFLOW_Culvert_Merge.py +++ /dev/null @@ -1,33 +0,0 @@ -# TUFLOW_Culvert_Merge.py - -from pathlib import Path -import os -from ryan_library.scripts.tuflow_culverts_merge import main_processing - - -def main(): - """ - Wrapper script to merge culvert results - By default, it processes files in the directory where the script is located. - """ - - try: - # Determine the script directory - script_directory: Path = Path(__file__).absolute().parent - script_directory = Path( - # r"Q:\Library\Automation\ryan-tools\tests\test_data\tuflow\tutorials" - r"E:\Library\Automation\ryan-tools\tests\test_data\tuflow\tutorials" - ) - os.chdir(script_directory) - - # You can pass a list of paths here if needed; default is the script directory - main_processing([script_directory], console_log_level="DEBUG") - except Exception as e: - print(f"Failed to change working directory: {e}") - os.system("PAUSE") - exit(1) - - -if __name__ == "__main__": - main() - os.system("PAUSE") diff --git a/vendor/run_hy8 b/vendor/run_hy8 index 617d45e3..6b84b9ea 160000 --- a/vendor/run_hy8 +++ b/vendor/run_hy8 @@ -1 +1 @@ -Subproject commit 617d45e3bc0a7f3eed6a1d30cd6a546f6cb57a8a +Subproject commit 6b84b9eabcf2960798f0b2ab6d7fbe670b1cfc85 diff --git a/vendor/run_hy8.UPSTREAM b/vendor/run_hy8.UPSTREAM index 6cb84e06..2c568e12 100644 --- a/vendor/run_hy8.UPSTREAM +++ b/vendor/run_hy8.UPSTREAM @@ -1,5 +1,5 @@ Repository: https://github.com/Chain-Frost/run-hy8.git -Commit: 617d45e3bc0a7f3eed6a1d30cd6a546f6cb57a8a (main) +Commit: 6b84b9eabcf2960798f0b2ab6d7fbe670b1cfc85 (detached) Retrieved: 2025-11-16 Update instructions: