From a8964a9cb55b53de49e09030f3cdbc5723b0360c Mon Sep 17 00:00:00 2001 From: Chain-Frost Date: Wed, 12 Nov 2025 12:05:18 +0800 Subject: [PATCH 1/3] Extend culvert mean report outputs --- .../TUFLOW_Culvert-mean-max-aep-dur.py | 34 +++ ryan_library/classes/column_definitions.py | 88 ++++++ .../scripts/tuflow/tuflow_culverts_mean.py | 258 ++++++++++++++++++ 3 files changed, 380 insertions(+) create mode 100644 ryan-scripts/TUFLOW-python/TUFLOW_Culvert-mean-max-aep-dur.py create mode 100644 ryan_library/scripts/tuflow/tuflow_culverts_mean.py diff --git a/ryan-scripts/TUFLOW-python/TUFLOW_Culvert-mean-max-aep-dur.py b/ryan-scripts/TUFLOW-python/TUFLOW_Culvert-mean-max-aep-dur.py new file mode 100644 index 00000000..3ccf7fb7 --- /dev/null +++ b/ryan-scripts/TUFLOW-python/TUFLOW_Culvert-mean-max-aep-dur.py @@ -0,0 +1,34 @@ +# ryan-scripts\TUFLOW-python\TUFLOW_Culvert-mean-max-aep-dur.py + +from pathlib import Path +import os + +from ryan_library.scripts.tuflow.tuflow_culverts_mean import run_culvert_mean_report +from ryan_library.scripts.wrapper_utils import ( + change_working_directory, + print_library_version, +) + + +def main() -> None: + """Wrapper script to create culvert mean peak reports.""" + + print_library_version() + console_log_level = "INFO" # or "DEBUG" + script_directory: Path = Path(__file__).absolute().parent + + if not change_working_directory(target_dir=script_directory): + return + + run_culvert_mean_report( + script_directory=script_directory, + log_level=console_log_level, + ) + + print() + print_library_version() + + +if __name__ == "__main__": + main() + os.system("PAUSE") diff --git a/ryan_library/classes/column_definitions.py b/ryan_library/classes/column_definitions.py index 3f91b59d..1b2d1175 100644 --- a/ryan_library/classes/column_definitions.py +++ b/ryan_library/classes/column_definitions.py @@ -238,6 +238,26 @@ def default(cls) -> "ColumnMetadataRegistry": description="Temporal pattern associated with the mean storm for the group.", value_type="string", ), + "mean_Q": ColumnDefinition( + name="mean_Q", + description="Arithmetic mean discharge for the grouped culvert results.", + value_type="float", + ), + "mean_V": ColumnDefinition( + name="mean_V", + description="Arithmetic mean velocity for the grouped culvert results.", + value_type="float", + ), + "mean_DS_h": ColumnDefinition( + name="mean_DS_h", + description="Arithmetic mean downstream water level for the grouped culvert results.", + value_type="float", + ), + "mean_US_h": ColumnDefinition( + name="mean_US_h", + description="Arithmetic mean upstream water level for the grouped culvert results.", + value_type="float", + ), "low": ColumnDefinition( name="low", description="Minimum statistic encountered across all temporal patterns in the group.", @@ -263,6 +283,11 @@ def default(cls) -> "ColumnMetadataRegistry": description="Deprecated. Don't use. Indicates whether the mean storm matches the median storm selection.", value_type="boolean", ), + "mean_bin": ColumnDefinition( + name="mean_bin", + description="Number of events contributing non-null mean metrics within the group.", + value_type="int", + ), "aep_dur_bin": ColumnDefinition( name="aep_dur_bin", description="Count of records in the original AEP/Duration/Location/Type/run combination.", @@ -273,6 +298,69 @@ def default(cls) -> "ColumnMetadataRegistry": description="Count of records in the original AEP/Location/Type/run combination.", value_type="int", ), + "min_Q": ColumnDefinition( + name="min_Q", + description="Minimum discharge observed across the grouped culvert events.", + value_type="float", + ), + "min_V": ColumnDefinition( + name="min_V", + description="Minimum velocity observed across the grouped culvert events.", + value_type="float", + ), + "min_DS_h": ColumnDefinition( + name="min_DS_h", + description="Minimum downstream water level observed across the grouped culvert events.", + value_type="float", + ), + "min_US_h": ColumnDefinition( + name="min_US_h", + description="Minimum upstream water level observed across the grouped culvert events.", + value_type="float", + ), + "max_Q": ColumnDefinition( + name="max_Q", + description="Maximum discharge observed across the grouped culvert events.", + value_type="float", + ), + "max_V": ColumnDefinition( + name="max_V", + description="Maximum velocity observed across the grouped culvert events.", + value_type="float", + ), + "max_DS_h": ColumnDefinition( + name="max_DS_h", + description="Maximum downstream water level observed across the grouped culvert events.", + value_type="float", + ), + "max_US_h": ColumnDefinition( + name="max_US_h", + description="Maximum upstream water level observed across the grouped culvert events.", + value_type="float", + ), + "adopted_Q": ColumnDefinition( + name="adopted_Q", + description=( + "Discharge from the event whose Q value is closest to the sample mean for the group; used as the adopted" + " representative flow." + ), + value_type="float", + ), + "adopted_V": ColumnDefinition( + name="adopted_V", + description="Velocity from the adopted event associated with the adopted_Q selection.", + value_type="float", + ), + "adopted_DS_h": ColumnDefinition( + name="adopted_DS_h", + description="Downstream water level from the adopted event associated with the adopted_Q selection.", + value_type="float", + ), + "adopted_US_h": ColumnDefinition( + name="adopted_US_h", + description="Upstream water level from the adopted event associated with the adopted_Q selection.", + value_type="float", + ), } sheet_specific: dict[str, dict[str, ColumnDefinition]] = { diff --git a/ryan_library/scripts/tuflow/tuflow_culverts_mean.py b/ryan_library/scripts/tuflow/tuflow_culverts_mean.py new file mode 100644 index 00000000..9a46d845 --- /dev/null +++ b/ryan_library/scripts/tuflow/tuflow_culverts_mean.py @@ -0,0 +1,258 @@ +"""Generate AEP/Duration mean summaries for culvert result datasets.""" + +from __future__ import annotations + +from collections.abc import Sequence +from datetime import datetime +from pathlib import Path + +import pandas as pd +from loguru import logger +from pandas.api.types import is_numeric_dtype + +from ryan_library.functions.loguru_helpers import setup_logger +from ryan_library.functions.misc_functions import ExcelExporter +from ryan_library.functions.tuflow.tuflow_common import bulk_read_and_merge_tuflow_csv + +DEFAULT_CULVERT_DATA_TYPES: tuple[str, ...] = ("Nmx", "Cmx", "Chan", "ccA") + + +def run_culvert_mean_report( + script_directory: Path | None = None, + log_level: str = "INFO", + include_data_types: Sequence[str] | None = None, + export_raw: bool = True, +) -> None: + """Generate AEP/Duration mean statistics for culvert results and export them to Excel.""" + + if script_directory is None: + script_directory = Path.cwd() + + data_types: list[str] = list(include_data_types) if include_data_types else list(DEFAULT_CULVERT_DATA_TYPES) + + with setup_logger(console_log_level=log_level) as log_queue: + collection = bulk_read_and_merge_tuflow_csv( + paths_to_process=[script_directory], + include_data_types=data_types, + log_queue=log_queue, + ) + log_queue.close() + log_queue.join_thread() + + if not collection.processors: + logger.warning("No culvert result files were processed. Skipping export.") + return + + aggregated_df: pd.DataFrame = collection.combine_1d_maximums() + if aggregated_df.empty: + logger.warning("Combined culvert maximums DataFrame is empty. Skipping export.") + return + + aep_dur_mean: pd.DataFrame = find_culvert_aep_dur_mean(aggregated_df) + if aep_dur_mean.empty: + logger.warning("Unable to calculate AEP/Duration mean statistics. Skipping export.") + return + + aep_mean_max: pd.DataFrame = find_culvert_aep_mean_max(aep_dur_mean) + + timestamp: str = datetime.now().strftime(format="%Y%m%d-%H%M") + output_name: str = f"{timestamp}_culvert_mean_peaks.xlsx" + sheet_names: list[str] = ["aep-dur-mean", "aep-mean-max"] + sheet_frames: list[pd.DataFrame] = [aep_dur_mean, aep_mean_max] + + if export_raw: + sheet_names.append("culvert-maximums") + sheet_frames.append(aggregated_df) + + ExcelExporter().export_dataframes( + export_dict={ + Path(output_name).stem: { + "dataframes": sheet_frames, + "sheets": sheet_names, + } + }, + output_directory=script_directory, + file_name=output_name, + ) + + logger.info("Culvert mean report exported to {}", script_directory / output_name) + + +ADOPTED_SOURCE_COLUMNS: tuple[str, ...] = ("Q", "V", "DS_h", "US_h") + + +def find_culvert_aep_dur_mean(aggregated_df: pd.DataFrame) -> pd.DataFrame: + """Return mean statistics grouped by AEP, Duration, TP and Culvert.""" + + if aggregated_df.empty: + return pd.DataFrame() + + group_columns: list[str] = _group_columns(aggregated_df, include_duration=True) + if not group_columns: + logger.error("Required grouping columns were not found in the aggregated culvert DataFrame.") + return pd.DataFrame() + + numeric_columns: list[str] = [col for col in aggregated_df.columns if is_numeric_dtype(aggregated_df[col])] + if not numeric_columns: + logger.warning("No numeric columns available for culvert mean calculations.") + return pd.DataFrame() + + grouped = aggregated_df.groupby(group_columns, observed=True) + + mean_df: pd.DataFrame = grouped[numeric_columns].mean(numeric_only=True).reset_index() + rename_map: dict[str, str] = {column: f"mean_{column}" for column in numeric_columns} + mean_df = mean_df.rename(columns=rename_map) + + count_df: pd.DataFrame = grouped.size().rename("count").reset_index() + count_df["count"] = count_df["count"].astype("Int64") + + range_columns: list[str] = [column for column in ADOPTED_SOURCE_COLUMNS if column in aggregated_df.columns] + min_df: pd.DataFrame = pd.DataFrame() + max_df: pd.DataFrame = pd.DataFrame() + if range_columns: + min_df = grouped[range_columns].min(numeric_only=True).reset_index() + min_df = min_df.rename(columns={column: f"min_{column}" for column in range_columns}) + + max_df = grouped[range_columns].max(numeric_only=True).reset_index() + max_df = max_df.rename(columns={column: f"max_{column}" for column in range_columns}) + + adopted_rows: list[dict[str, object]] = [] + if range_columns: + for key, group in grouped: + key_values: dict[str, object] + if isinstance(key, tuple): + key_values = dict(zip(group_columns, key, strict=False)) + else: + key_values = {group_columns[0]: key} + + adopted_entry: dict[str, object] = {**key_values} + + q_series = pd.to_numeric(group.get("Q"), errors="coerce") if "Q" in group.columns else None + if q_series is not None and q_series.notna().any(): + mean_q = float(q_series.mean()) + idx = (q_series - mean_q).abs().idxmin() + closest_row = group.loc[idx] + for column in range_columns: + adopted_entry[f"adopted_{column}"] = closest_row.get(column, pd.NA) + else: + for column in range_columns: + adopted_entry[f"adopted_{column}"] = pd.NA + + adopted_rows.append(adopted_entry) + + adopted_df: pd.DataFrame = pd.DataFrame(adopted_rows) if adopted_rows else pd.DataFrame() + + merged: pd.DataFrame = count_df.copy() + for frame in (mean_df, adopted_df, min_df, max_df): + if not frame.empty: + merged = merged.merge(frame, on=group_columns, how="left") + + merged = merged.sort_values(group_columns, ignore_index=True) + + ordered_columns: list[str] = _ordered_columns( + merged, + lead=group_columns, + secondary=["count"], + value_prefixes=("mean_", "adopted_", "min_", "max_"), + ) + return merged.loc[:, ordered_columns] + + +def find_culvert_aep_mean_max(aep_dur_mean: pd.DataFrame) -> pd.DataFrame: + """Return the duration row containing the highest mean discharge for each AEP/culvert group.""" + + if aep_dur_mean.empty: + return pd.DataFrame() + + metric_column: str | None = _preferred_metric_column(aep_dur_mean) + if metric_column is None: + logger.warning("No mean columns were found for culvert mean-max calculation.") + return pd.DataFrame() + + group_columns: list[str] = _group_columns(aep_dur_mean, include_duration=False) + if not group_columns: + logger.error("Required grouping columns were not found for the culvert mean-max calculation.") + return pd.DataFrame() + + df: pd.DataFrame = aep_dur_mean.copy() + df["_mean_metric"] = pd.to_numeric(df[metric_column], errors="coerce") + df["_has_metric"] = df["_mean_metric"].notna() + + if not df["_has_metric"].any(): + logger.warning("Mean metric column '{}' does not contain any numeric values.", metric_column) + return pd.DataFrame() + + df["mean_bin"] = ( + df.groupby(group_columns, observed=True)["_has_metric"].transform("sum").astype("Int64") + ) + + idx = df[df["_has_metric"]].groupby(group_columns, observed=True)["_mean_metric"].idxmax() + result: pd.DataFrame = df.loc[idx].drop(columns=["_mean_metric", "_has_metric"]).reset_index(drop=True) + result = result.sort_values(group_columns, ignore_index=True) + + ordered_columns: list[str] = _ordered_columns( + result, + lead=_group_columns(result, include_duration=True), + secondary=["count", "mean_bin"], + value_prefixes=("mean_", "adopted_", "min_", "max_"), + ) + return result.loc[:, ordered_columns] + + +def _preferred_metric_column(aep_dur_mean: pd.DataFrame) -> str | None: + """Return the preferred mean column used to identify maximum durations.""" + + candidate_columns: list[str] = [column for column in aep_dur_mean.columns if column.startswith("mean_")] + if not candidate_columns: + return None + + preferred_order: tuple[str, ...] = ( + "mean_Q", + "mean_V", + "mean_DS_h", + "mean_US_h", + ) + for preferred in preferred_order: + if preferred in candidate_columns: + return preferred + return candidate_columns[0] + + +def _group_columns(df: pd.DataFrame, include_duration: bool) -> list[str]: + """Return the ordered list of grouping columns present in ``df``.""" + + base_order: list[str] = ["aep_text"] + if include_duration: + base_order.append("duration_text") + base_order.append("tp_text") + base_order.append("trim_runcode") + base_order.append("Chan ID") + + resolved = [column for column in base_order if column in df.columns] + if "Chan ID" not in resolved: + return [] + return resolved + + +def _ordered_columns( + df: pd.DataFrame, + lead: list[str], + secondary: list[str], + value_prefixes: Sequence[str], +) -> list[str]: + """Return an ordered list of columns for presentation.""" + + ordered: list[str] = [] + for column in lead + secondary: + if column in df.columns and column not in ordered: + ordered.append(column) + + for prefix in value_prefixes: + prefixed_columns: list[str] = sorted([column for column in df.columns if column.startswith(prefix)]) + ordered.extend([column for column in prefixed_columns if column not in ordered]) + + for column in df.columns: + if column not in ordered: + ordered.append(column) + + return ordered From ae5df7cc1bca6307a703685632f6211ef5c10faa Mon Sep 17 00:00:00 2001 From: Ryan Brook Date: Wed, 12 Nov 2025 13:07:21 +0800 Subject: [PATCH 2/3] build --- ...yan_functions-25.11.12.1-py3-none-any.whl} | Bin 148519 -> 151838 bytes setup.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename dist/{ryan_functions-25.11.7.4-py3-none-any.whl => ryan_functions-25.11.12.1-py3-none-any.whl} (87%) diff --git a/dist/ryan_functions-25.11.7.4-py3-none-any.whl b/dist/ryan_functions-25.11.12.1-py3-none-any.whl similarity index 87% rename from dist/ryan_functions-25.11.7.4-py3-none-any.whl rename to dist/ryan_functions-25.11.12.1-py3-none-any.whl index c51cfd83de4848f18ad185efe7d15c1f63851e32..a07e11212f9829c885956c159c9d34460d19423a 100644 GIT binary patch delta 12995 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 DZUD`l delta 9697 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 P^gBQf1EWF@f};Ne3GbrT diff --git a/setup.py b/setup.py index 09affacf..86c92a2d 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.12.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"]}, From 6652417103f9464fb9bd22a87daf3a2b7a4e4c2b Mon Sep 17 00:00:00 2001 From: Chain Frost Date: Tue, 18 Nov 2025 09:55:09 +0800 Subject: [PATCH 3/3] tweak wrapper --- .../TUFLOW_Culvert-mean-max-aep-dur.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/ryan-scripts/TUFLOW-python/TUFLOW_Culvert-mean-max-aep-dur.py b/ryan-scripts/TUFLOW-python/TUFLOW_Culvert-mean-max-aep-dur.py index 3ccf7fb7..d277b607 100644 --- a/ryan-scripts/TUFLOW-python/TUFLOW_Culvert-mean-max-aep-dur.py +++ b/ryan-scripts/TUFLOW-python/TUFLOW_Culvert-mean-max-aep-dur.py @@ -4,10 +4,13 @@ import os from ryan_library.scripts.tuflow.tuflow_culverts_mean import run_culvert_mean_report -from ryan_library.scripts.wrapper_utils import ( - change_working_directory, - print_library_version, -) +from ryan_library.scripts.wrapper_utils import change_working_directory, print_library_version + +# Toggle the specific culvert data types to collect. Leave empty to accept the library defaults. +INCLUDED_DATA_TYPES: tuple[str, ...] = ("Nmx", "Cmx", "Chan", "ccA", "RLL_Qmx") + +# Toggle the export of the raw culvert-maximums sheet (may be extremely large). +EXPORT_RAW_MAXIMUMS: bool = True def main() -> None: @@ -20,9 +23,13 @@ def main() -> None: if not change_working_directory(target_dir=script_directory): return + include_data_types: tuple[str, ...] | None = INCLUDED_DATA_TYPES or None + run_culvert_mean_report( script_directory=script_directory, log_level=console_log_level, + include_data_types=include_data_types, + export_raw=EXPORT_RAW_MAXIMUMS, ) print()