Source code for supy._supy_module

# ###########################################################################
# SuPy: SUEWS for Python
#
# Authors:
# Ting Sun, ting.sun@reading.ac.uk
#
# History:
# 20 Jan 2018: first alpha release
# 01 Feb 2018: performance improvement
# 03 Feb 2018: improvement in output processing
# 08 Mar 2018: pypi packaging
# 04 Oct 2018: overhaul of structure
# 05 Oct 2018: added sample run data
# 28 Apr 2019: added support for parallel run
###########################################################################

import logging
import os
from pathlib import Path
import sys
import time
import warnings
from typing import Optional

import numpy as np
import pandas
import pandas as pd

from ._check import check_forcing, check_state
from ._env import logger_supy, trv_supy_module
from ._load import (
    load_df_state,
    load_InitialCond_grid_df,
    load_SUEWS_dict_ModConfig,
    load_SUEWS_Forcing_met_df_raw,
    load_SUEWS_Forcing_met_df_yaml,
    resample_forcing_met,
)
from ._post import resample_output
from ._save import (
    get_save_info,
    save_df_output,
    save_df_output_parquet,
    save_df_state,
    save_initcond_nml,
)
from ._version import __version__

# from .util._config import init_config_from_yaml
from .data_model import init_config_from_yaml

# set up logging module
logger_supy.setLevel(logging.INFO)


_FUNCTIONAL_DEPRECATIONS = {
    "init_supy": "the object-oriented `SUEWSSimulation` interface (see `supy.suews_sim.SUEWSSimulation`)",
    "load_forcing_grid": "`SUEWSSimulation.update_forcing` or `read_forcing` utilities",
    "load_sample_data": "`SUEWSSimulation` helpers (e.g., `SUEWSSimulation('sample_config.yml')`)",
    "run_supy": "`SUEWSSimulation.run`",
    "run_supy_sample": "`SUEWSSimulation` sample workflows",
    "save_supy": "`SUEWSSimulation.save`",
    "init_config": "`SUEWSSimulation` or `SUEWSConfig` constructors",
    "resample_output": "`SUEWSOutput.resample` (e.g., `output.resample(freq='h')`)",
}


def _warn_functional_deprecation(name: str) -> None:
    """Emit a standardized deprecation warning for the legacy functional API."""
    replacement = _FUNCTIONAL_DEPRECATIONS.get(name, "the object-oriented API")
    warnings.warn(
        f"`supy.{name}` is deprecated and will be removed in a future release. "
        f"Please migrate to {replacement}.",
        DeprecationWarning,
        stacklevel=3,
    )


##############################################################################
# 1. compact wrapper for loading SUEWS settings
# @functools.lru_cache(maxsize=16)
def _init_supy(
    path_init: str,
    force_reload=True,
    check_input=False,
) -> pd.DataFrame:
    """Initialise supy by loading initial model states.

    Parameters
    ----------
    path_init : str
        Path to a file that can initialise SuPy, which can be either of the follows:
            * SUEWS :ref:`RunControl.nml<suews:RunControl.nml>`: a namelist file for SUEWS configurations
            * SuPy `df_state.csv`: a CSV file including model states produced by a SuPy run via :py:func:`supy.save_supy`

    force_reload: bool, optional
        Flag to force reload all initialisation files by clearing all cached states, with default value `True` (i.e., force reload all files).
        Note: If the number of simulation grids is large (e.g., > 100), `force_reload=False` is strongly recommended for better performance.

    check_input: bool, optional
        flag for checking validity of input: `df_forcing` and `df_state_init`.
        If set to `True`, any detected invalid input will stop SuPy simulation;
        a `False` flag will bypass such validation and may incur kernel error if any invalid input.
        *Note: such checking procedure may take some time if the input is large.*
        (the default is `False`, which bypasses the validation).




    Returns
    -------
    df_state_init: pandas.DataFrame
        Initial model states.
        See `df_state_var` for details.

    Examples
    --------
    1. Use :ref:`RunControl.nml<suews:RunControl.nml>` to initialise SuPy

    >>> path_init = "~/SUEWS_sims/RunControl.nml"
    >>> df_state_init = supy.init_supy(path_init)

    2. Use ``df_state.csv`` to initialise SuPy

    >>> path_init = "~/SuPy_res/df_state_test.csv"
    >>> df_state_init = supy.init_supy(path_init)

    """
    try:
        path_init_x = Path(path_init).expanduser().resolve()
    except FileNotFoundError:
        logger_supy.exception(f"{path_init_x} does not exists!")
    else:
        if path_init_x.suffix == ".yml":
            # SUEWS `config_suews.yaml`:
            logger_supy.info("Loading config from yaml")
            df_state_init = init_config_from_yaml(path=path_init_x).to_df_state()
        else:
            logger_supy.warning(
                "Input is not a yaml file, loading from other sources. These methods will be deprecated in later versions.",
                stacklevel=2,
            )
            if path_init_x.suffix == ".nml":
                # SUEWS `RunControl.nml`:
                df_state_init = load_InitialCond_grid_df(
                    path_init_x,
                    force_reload=force_reload,
                )
            elif path_init_x.suffix == ".csv":
                # SuPy `df_state.csv`:
                df_state_init = load_df_state(path_init_x)
            else:
                logger_supy.critical(
                    f"{path_init_x} is NOT a valid file to initialise SuPy!"
                )
                raise RuntimeError(
                    f"{path_init_x} is NOT a valid file to initialise SuPy!"
                )
        if check_input:
            try:
                list_issues = check_state(df_state_init)
                if isinstance(list_issues, list):
                    logger_supy.critical(
                        f"`df_state_init` loaded from {path_init_x} is NOT valid to initialise SuPy!"
                    )
            except:
                raise RuntimeError(
                    f"{path_init_x} is NOT a valid file to initialise SuPy!"
                )

        return df_state_init


# # TODO:
# def load_forcing(path_pattern: str, grid: int = 0) -> pd.DataFrame:
#     pass


[docs] def init_supy( path_init: str, force_reload=True, check_input=False, ) -> pd.DataFrame: """Initialise supy by loading initial model states. .. deprecated:: 2025.11.20 This function is deprecated and will be removed in a future release. Please migrate to the object-oriented `SUEWSSimulation` interface (see `supy.suews_sim.SUEWSSimulation`). Parameters ---------- path_init : str Path to a file that can initialise SuPy, which can be either of the follows: * SUEWS :ref:`RunControl.nml<suews:RunControl.nml>`: a namelist file for SUEWS configurations * SuPy `df_state.csv`: a CSV file including model states produced by a SuPy run via :py:func:`supy.save_supy` force_reload: bool, optional Flag to force reload all initialisation files by clearing all cached states, with default value `True` (i.e., force reload all files). Note: If the number of simulation grids is large (e.g., > 100), `force_reload=False` is strongly recommended for better performance. check_input: bool, optional flag for checking validity of input: `df_forcing` and `df_state_init`. If set to `True`, any detected invalid input will stop SuPy simulation; a `False` flag will bypass such validation and may incur kernel error if any invalid input. *Note: such checking procedure may take some time if the input is large.* (the default is `False`, which bypasses the validation). Returns ------- df_state_init: pandas.DataFrame Initial model states. See `df_state_var` for details. See Also -------- supy.suews_sim.SUEWSSimulation : Modern object-oriented interface (recommended) Examples -------- 1. Use :ref:`RunControl.nml<suews:RunControl.nml>` to initialise SuPy >>> path_init = "~/SUEWS_sims/RunControl.nml" >>> df_state_init = supy.init_supy(path_init) 2. Use ``df_state.csv`` to initialise SuPy >>> path_init = "~/SuPy_res/df_state_test.csv" >>> df_state_init = supy.init_supy(path_init) """ _warn_functional_deprecation("init_supy") return _init_supy(path_init, force_reload=force_reload, check_input=check_input)
# TODO: # to be superseded by a more generic wrapper: load_forcing def _load_forcing_grid( path_init: str, grid: int, check_input=False, force_reload=True, df_state_init: pd.DataFrame = None, config=None, ) -> pd.DataFrame: """Load forcing data for a specific grid included in the index of `df_state_init </data-structure/supy-io.ipynb#df_state_init:-model-initial-states>`. Parameters ---------- path_runcontrol : str Path to SUEWS :ref:`RunControl.nml <suews:RunControl.nml>` grid : int Grid number check_input : bool, optional flag for checking validity of input: `df_forcing` and `df_state_init`. If set to `True`, any detected invalid input will stop SuPy simulation; a `False` flag will bypass such validation and may incur kernel error if any invalid input. *Note: such checking procedure may take some time if the input is large.* (the default is `False`, which bypasses the validation). Returns ------- df_forcing: pandas.DataFrame Forcing data. See `df_forcing_var` for details. Examples -------- >>> path_runcontrol = ( ... "~/SUEWS_sims/RunControl.nml" # a valid path to `RunControl.nml` ... ) >>> df_state_init = supy.init_supy(path_runcontrol) # get `df_state_init` >>> grid = df_state_init.index[ ... 0 ... ] # first grid number included in `df_state_init` >>> df_forcing = supy.load_forcing_grid( ... path_runcontrol, grid ... ) # get df_forcing """ try: path_init = Path(path_init).expanduser().resolve() except FileNotFoundError: logger_supy.exception(f"{path_init} does not exists!") else: if path_init.suffix == ".nml": # load settings from RunControl.nml dict_mod_cfg = load_SUEWS_dict_ModConfig(path_init) # load setting variables from dict_mod_cfg ( filecode, kdownzen, tstep_met_in, tstep_ESTM_in, multiplemetfiles, multipleestmfiles, dir_input_cfg, ) = ( dict_mod_cfg[x] for x in [ "filecode", "kdownzen", "resolutionfilesin", "resolutionfilesinestm", "multiplemetfiles", "multipleestmfiles", "fileinputpath", ] ) path_site = path_init.parent path_input = path_site / dict_mod_cfg["fileinputpath"] else: if config is None: config = init_config_from_yaml(path=path_init) path_site = path_init.parent forcing_file_val = ( config.model.control.forcing_file.value if hasattr(config.model.control.forcing_file, "value") else config.model.control.forcing_file ) if isinstance(forcing_file_val, list): # Handle list of files path_input = [path_site / f for f in forcing_file_val] else: # Handle single file path_input = path_site / forcing_file_val tstep_mod, lat, lon, alt, timezone = df_state_init.loc[ grid, [(x, "0") for x in ["tstep", "lat", "lng", "alt", "timezone"]] ].values # load raw data # met forcing if path_init.suffix == ".nml": df_forcing_met = load_SUEWS_Forcing_met_df_raw( path_input, filecode, grid, tstep_met_in, multiplemetfiles ) # resample raw data from tstep_in to tstep_mod df_forcing_met_tstep = resample_forcing_met( df_forcing_met, tstep_met_in, tstep_mod, lat, lon, alt, timezone, kdownzen, ) elif path_init.suffix == ".yml": df_forcing_met = load_SUEWS_Forcing_met_df_yaml(path_input) tstep_met_in = df_forcing_met.index[1] - df_forcing_met.index[0] tstep_met_in = int(tstep_met_in.total_seconds()) kdownzen = config.model.control.kdownzen if kdownzen is not None: kdownzen = kdownzen.value if kdownzen is None: df_forcing_met_tstep = resample_forcing_met( df_forcing_met, tstep_met_in, tstep_mod, lat, lon, alt, timezone ) else: df_forcing_met_tstep = resample_forcing_met( df_forcing_met, tstep_met_in, tstep_mod, lat, lon, alt, timezone, kdownzen, ) # coerced precision here to prevent numerical errors inside Fortran df_forcing = df_forcing_met_tstep.round(10) # new columns for later use in main calculation df_forcing[["iy", "id", "it", "imin"]] = df_forcing[ ["iy", "id", "it", "imin"] ].astype(np.int64) if check_input: try: list_issues = check_forcing(df_forcing) if isinstance(list_issues, list): logger_supy.critical( f"`df_forcing` loaded from {path_input} is NOT valid to drive SuPy!" ) except: sys.exit() return df_forcing # load sample data for quickly starting a demo run # TODO: to deprecate this by renaming for case consistency: load_SampleData-->load_sample_data
[docs] def load_forcing_grid( path_init: str, grid: int, check_input=False, force_reload=True, df_state_init: pd.DataFrame = None, config=None, ) -> pd.DataFrame: """Load forcing data for a specific grid included in the index of `df_state_init </data-structure/supy-io.ipynb#df_state_init:-model-initial-states>`. .. deprecated:: 2025.11.20 This function is deprecated and will be removed in a future release. Please migrate to `SUEWSSimulation.update_forcing` or `read_forcing` utilities. Parameters ---------- path_runcontrol : str Path to SUEWS :ref:`RunControl.nml <suews:RunControl.nml>` grid : int Grid number check_input : bool, optional flag for checking validity of input: `df_forcing` and `df_state_init`. If set to `True`, any detected invalid input will stop SuPy simulation; a `False` flag will bypass such validation and may incur kernel error if any invalid input. *Note: such checking procedure may take some time if the input is large.* (the default is `False`, which bypasses the validation). Returns ------- df_forcing: pandas.DataFrame Forcing data. See `df_forcing_var` for details. See Also -------- supy.suews_sim.SUEWSSimulation.update_forcing : Modern interface for loading forcing data (recommended) Examples -------- >>> path_runcontrol = ( ... "~/SUEWS_sims/RunControl.nml" # a valid path to `RunControl.nml` ... ) >>> df_state_init = supy.init_supy(path_runcontrol) # get `df_state_init` >>> grid = df_state_init.index[ ... 0 ... ] # first grid number included in `df_state_init` >>> df_forcing = supy.load_forcing_grid( ... path_runcontrol, grid ... ) # get df_forcing """ _warn_functional_deprecation("load_forcing_grid") return _load_forcing_grid( path_init=path_init, grid=grid, check_input=check_input, force_reload=force_reload, df_state_init=df_state_init, config=config, )
def load_SampleData() -> tuple[pandas.DataFrame, pandas.DataFrame]: logger_supy.warning( "This function name will be deprecated. Please use `load_sample_data()` instead.", stacklevel=2, ) return _load_sample_data() def _load_sample_data() -> tuple[pandas.DataFrame, pandas.DataFrame]: """Load sample data for quickly starting a demo run. Returns ------- df_state_init, df_forcing: Tuple[pandas.DataFrame, pandas.DataFrame] - df_state_init: `initial model states <df_state_var>` - df_forcing: `forcing data <df_forcing_var>` Examples -------- >>> df_state_init, df_forcing = supy.load_sample_data() """ trv_sample_data = trv_supy_module / "sample_data" path_config_default = trv_sample_data / "sample_config.yml" # path_config_default = trv_sample_data / "RunControl.nml" # TODO: to be deprecated - but keep for now to pass tests df_state_init = _init_supy(path_config_default, force_reload=False) df_forcing = _load_forcing_grid( path_config_default, df_state_init.index[0], df_state_init=df_state_init ) return df_state_init, df_forcing
[docs] def load_sample_data() -> tuple[pandas.DataFrame, pandas.DataFrame]: """Load sample data for quickly starting a demo run. .. deprecated:: 2025.11.20 This function is deprecated and will be removed in a future release. Please migrate to `SUEWSSimulation` helpers (e.g., `SUEWSSimulation('sample_config.yml')`). Returns ------- df_state_init, df_forcing: Tuple[pandas.DataFrame, pandas.DataFrame] - df_state_init: `initial model states <df_state_var>` - df_forcing: `forcing data <df_forcing_var>` See Also -------- supy.suews_sim.SUEWSSimulation : Modern object-oriented interface (recommended) Examples -------- >>> df_state_init, df_forcing = supy.load_sample_data() """ _warn_functional_deprecation("load_sample_data") return _load_sample_data()
def load_config_from_df(df_state: pd.DataFrame): """Load SUEWS configuration from `df_state`. Parameters ---------- df_state : pandas.DataFrame DataFrame of model states. Returns ------- config : SUEWSConfig SUEWS configuration. Examples -------- >>> df_state_init, df_forcing = supy.load_sample_data() >>> config = supy.load_config_from_df(df_state_init) """ from .util._config import SUEWSConfig config = SUEWSConfig.from_df_state(df_state) return config def _init_config(df_state: pd.DataFrame = None): """Initialise SUEWS configuration object either from existing df_state dataframe or as the default configuration.""" if df_state is None: from .util._config import SUEWSConfig return SUEWSConfig() return load_config_from_df(df_state) def init_config(df_state: pd.DataFrame = None): """Initialise SUEWS configuration object either from existing df_state dataframe or as the default configuration. .. deprecated:: 2025.11.20 This function is deprecated and will be removed in a future release. Please migrate to `SUEWSSimulation` or `SUEWSConfig` constructors. Parameters ---------- df_state : pandas.DataFrame, optional DataFrame of model states. If None, returns default configuration. Returns ------- config : SUEWSConfig SUEWS configuration object. See Also -------- supy.suews_sim.SUEWSSimulation : Modern object-oriented interface (recommended) supy.util._config.SUEWSConfig : Configuration class """ _warn_functional_deprecation("init_config") return _init_config(df_state) # input processing code end here ############################################################################## ############################################################################## # 2. compact wrapper for running a whole simulation # # main calculation # input as DataFrame def _run_supy( df_forcing: pandas.DataFrame, df_state_init: pandas.DataFrame, save_state=False, chunk_day=3660, logging_level=logging.INFO, check_input=False, serial_mode=False, debug_mode=False, ) -> tuple[pandas.DataFrame, pandas.DataFrame]: """Perform supy simulation. Parameters ---------- df_forcing : pandas.DataFrame forcing data for all grids in `df_state_init`. df_state_init : pandas.DataFrame initial model states; or a collection of model states with multiple timestamps, whose last temporal record will be used as the initial model states. save_state : bool, optional flag for saving model states at each time step, which can be useful in diagnosing model runtime performance or performing a restart run. (the default is False, which instructs supy not to save runtime model states). chunk_day : int, optional chunk size (`chunk_day` days) to split simulation periods so memory usage can be reduced. (the default is 3660, which implies ~10-year forcing chunks used in simulations). logging_level: logging level one of these values [50 (CRITICAL), 40 (ERROR), 30 (WARNING), 20 (INFO), 10 (DEBUG)]. A lower value informs SuPy for more verbose logging info. check_input : bool, optional flag for checking validity of input: `df_forcing` and `df_state_init`. If set to `True`, any detected invalid input will stop SuPy simulation; a `False` flag will bypass such validation and may incur kernel error if any invalid input. *Note: such checking procedure may take some time if the input is large.* (the default is `False`, which bypasses the validation). serial_mode : bool, optional If set to `True`, SuPy simulation will be conducted in serial mode; a `False` flag will try parallel simulation if possible (Windows not supported, i.e., always serial). (the default is `False`). debug_mode : bool, optional If set to `True`, SuPy simulation will be conducted in debug mode, which will write out additional information for debugging purposes. Returns ------- df_output, df_state_final : Tuple[pandas.DataFrame, pandas.DataFrame] - df_output: `output results <df_output_var>` - df_state_final: `final model states <df_state_var>` Examples -------- >>> df_output, df_state_final = supy.run_supy(df_forcing, df_state_init) """ # validate input dataframes if check_input: # forcing: list_issues_forcing = check_forcing(df_forcing) if isinstance(list_issues_forcing, list): logger_supy.critical("`df_forcing` is NOT valid to drive SuPy!") raise RuntimeError( "SuPy stopped entering simulation due to invalid forcing!" ) # initial model states: res_check_state = check_state(df_state_init) if isinstance(res_check_state, list): logger_supy.critical("`df_state_init` is NOT valid to initialise SuPy!") raise RuntimeError( "SuPy stopped entering simulation due to invalid initial states!" ) else: logger_supy.info("SuPy simulation is starting ...") if isinstance(res_check_state, pd.DataFrame): df_state_init = res_check_state # enable debug mode if set if debug_mode: logging_level = logging.DEBUG logger_supy.setLevel(logging_level) # set up a timer for simulation time start = time.time() # adjust logging level logger_supy.setLevel(logging_level) # save df_init without changing its original data # df.copy() in pandas works as a standard python deepcopy # df_init = df_state_init.copy() # print some diagnostic info logger_supy.info("====================") logger_supy.info(f"SUEWS version: {__version__}") logger_supy.info("Simulation period:") logger_supy.info(f" Start: {df_forcing.index[0]}") logger_supy.info(f" End: {df_forcing.index[-1]}") logger_supy.info("") list_grid = df_state_init.index.get_level_values("grid").unique() n_grid = list_grid.size logger_supy.info(f"No. of grids: {n_grid}") # Build config from DataFrame state from .data_model.core import SUEWSConfig from ._run_rust import run_suews_rust_chunked from .util._forcing import convert_observed_soil_moisture config = SUEWSConfig.from_df_state(df_state_init) # Preprocess forcing for observed soil moisture if needed try: # SMDMethod is a physics option in the validated config schema. # Keep a fallback to legacy `model.options` for compatibility. smd_val = getattr(config.model.physics, "smdmethod", None) if smd_val is None: smd_val = getattr(getattr(config.model, "options", None), "smdmethod", None) if hasattr(smd_val, "value"): smd_val = smd_val.value smd_int = int(smd_val) if smd_val is not None else 0 except (AttributeError, TypeError, ValueError): smd_int = 0 df_forcing_processed = df_forcing.copy() if smd_int > 0: df_forcing_processed = convert_observed_soil_moisture( df_forcing_processed, df_state_init ) # Run via Rust bridge df_output, _ = run_suews_rust_chunked(config, df_forcing_processed, chunk_day) # Build state_final: copy initial state + version metadata df_state_final = df_state_init.copy() df_state_final[("version", "0")] = __version__ end = time.time() logger_supy.info(f"Simulation completed. Elapsed: {end - start:.1f}s") return df_output, df_state_final
[docs] def run_supy( df_forcing: pandas.DataFrame, df_state_init: pandas.DataFrame, save_state=False, chunk_day=3660, logging_level=logging.INFO, check_input=False, serial_mode=False, debug_mode=False, ) -> tuple[pandas.DataFrame, pandas.DataFrame]: """Perform supy simulation. .. deprecated:: 2025.11.20 This function is deprecated and will be removed in a future release. Please migrate to `SUEWSSimulation.run`. Parameters ---------- df_forcing : pandas.DataFrame forcing data for all grids in `df_state_init`. df_state_init : pandas.DataFrame initial model states; or a collection of model states with multiple timestamps, whose last temporal record will be used as the initial model states. save_state : bool, optional flag for saving model states at each time step, which can be useful in diagnosing model runtime performance or performing a restart run. (the default is False, which instructs supy not to save runtime model states). chunk_day : int, optional chunk size (`chunk_day` days) to split simulation periods so memory usage can be reduced. (the default is 3660, which implies ~10-year forcing chunks used in simulations). logging_level: logging level one of these values [50 (CRITICAL), 40 (ERROR), 30 (WARNING), 20 (INFO), 10 (DEBUG)]. A lower value informs SuPy for more verbose logging info. check_input : bool, optional flag for checking validity of input: `df_forcing` and `df_state_init`. If set to `True`, any detected invalid input will stop SuPy simulation; a `False` flag will bypass such validation and may incur kernel error if any invalid input. *Note: such checking procedure may take some time if the input is large.* (the default is `False`, which bypasses the validation). serial_mode : bool, optional If set to `True`, SuPy simulation will be conducted in serial mode; a `False` flag will try parallel simulation if possible (Windows not supported, i.e., always serial). (the default is `False`). debug_mode : bool, optional If set to `True`, SuPy simulation will be conducted in debug mode, which will write out additional information for debugging purposes. Returns ------- df_output, df_state_final : Tuple[pandas.DataFrame, pandas.DataFrame] - df_output: `output results <df_output_var>` - df_state_final: `final model states <df_state_var>` See Also -------- supy.suews_sim.SUEWSSimulation.run : Modern interface for running simulations (recommended) Examples -------- >>> df_output, df_state_final = supy.run_supy(df_forcing, df_state_init) """ _warn_functional_deprecation("run_supy") return _run_supy( df_forcing=df_forcing, df_state_init=df_state_init, save_state=save_state, chunk_day=chunk_day, logging_level=logging_level, check_input=check_input, serial_mode=serial_mode, debug_mode=debug_mode, )
############################################################################## # 3. save results of a supy run def _save_supy( df_output: pandas.DataFrame, df_state_final: pandas.DataFrame, freq_s: int = 3600, site: str = "", path_dir_save: str = Path("."), path_runcontrol: Optional[str] = None, save_tstep=False, logging_level=50, output_level=1, debug=False, output_config=None, output_format="txt", ) -> list: """Save SuPy run results to files. Parameters ---------- df_output : pandas.DataFrame DataFrame of output df_state_final : pandas.DataFrame DataFrame of final model states freq_s : int, optional Output frequency in seconds (the default is 3600, which indicates hourly output) site : str, optional Site identifier (the default is '', which indicates site identifier will be left empty) path_dir_save : str, optional Path to directory to saving the files (the default is Path('.'), which indicates the current working directory) path_runcontrol : str, optional Path to SUEWS :ref:`RunControl.nml <suews:RunControl.nml>`, which, if set, will be preferably used to derive `freq_s`, `site` and `path_dir_save`. (the default is None, which is unset) save_tstep : bool, optional whether to save results in temporal resolution as in simulation (which may result very large files and slow progress), by default False. logging_level: logging level one of these values [50 (CRITICAL), 40 (ERROR), 30 (WARNING), 20 (INFO), 10 (DEBUG)]. A lower value informs SuPy for more verbose logging info. output_level : integer, optional option to determine selection of output variables, by default 1. Notes: 0 for all but snow-related; 1 for all; 2 for a minimal set without land cover specific information. debug : bool, optional whether to enable debug mode (e.g., writing out in serial mode, and other debug uses), by default False. output_config : OutputConfig, optional Output configuration object specifying format, frequency, and groups to save. If provided, overrides freq_s parameter. Returns ------- list a list of paths of saved files Examples -------- 1. save results of a supy run to the current working directory with default settings >>> list_path_save = supy.save_supy(df_output, df_state_final) 2. save results according to settings in :ref:`RunControl.nml <suews:RunControl.nml>` >>> list_path_save = supy.save_supy( ... df_output, df_state_final, path_runcontrol="path/to/RunControl.nml" ... ) 3. save results of a supy run at resampling frequency of 1800 s (i.e., half-hourly results) under the site code ``Test`` to a customised location 'path/to/some/dir' >>> list_path_save = supy.save_supy( ... df_output, ... df_state_final, ... freq_s=1800, ... site="Test", ... path_dir_save="path/to/some/dir", ... ) """ # adjust logging level logger_supy.setLevel(logging_level) # get necessary information for saving procedure if path_runcontrol is not None: freq_s, path_dir_save, site, save_tstep, output_level = get_save_info( path_runcontrol ) # Handle output configuration if provided # output_format = "txt" # default - MP: Moved as argument output_groups = None # default will be handled in save_df_output if output_config is not None: from .data_model.core.model import OutputConfig if isinstance(output_config, OutputConfig): # Override frequency if specified in config if output_config.freq is not None: freq_s = output_config.freq # Get format output_format = str(output_config.format) # Get groups for txt format if output_format == "txt" and output_config.groups is not None: output_groups = output_config.groups elif isinstance(output_config, str): # Legacy string format - issue deprecation warning import warnings warnings.warn( "The 'output_file' parameter as a string is deprecated and was never used. " "Please use the new OutputConfig format or remove this parameter. " "Falling back to default text output. " "Example: output_file: {format: 'parquet', freq: 3600}", DeprecationWarning, stacklevel=2, ) # Fall back to default text format output_format = "txt" # determine `save_snow` option snowuse = df_state_final.iloc[-1].loc["snowuse"] # Handle both scalar and array cases safely if hasattr(snowuse, "iloc"): # If it's a Series (multi-level index), get the first value snowuse = snowuse.iloc[0] save_snow = True if snowuse == 1 else False # check if directory for saving results exists; if not, create one. path_dir_save = Path(path_dir_save) if not path_dir_save.exists(): path_dir_save.mkdir(parents=True) # save based on format if output_format == "parquet": # Save as Parquet list_path_save = save_df_output_parquet( df_output, df_state_final, freq_s, site, path_dir_save, save_tstep, ) else: # Save as text files (existing behavior) list_path_save = save_df_output( df_output, freq_s, site, path_dir_save, save_tstep, output_level, save_snow, debug, output_groups=output_groups, ) # MP: Parquet saves this already - breaks the parquet save check # save df_state if path_runcontrol is not None: # save as nml as SUEWS binary list_path_nml = save_initcond_nml(df_state_final, site, path_dir_save) list_path_save += list_path_nml else: # save as supy csv for later use path_state_save = save_df_state(df_state_final, site, path_dir_save) # update list_path_save list_path_save.append(path_state_save) return list_path_save
[docs] def save_supy( df_output: pandas.DataFrame, df_state_final: pandas.DataFrame, freq_s: int = 3600, site: str = "", path_dir_save: str = Path("."), path_runcontrol: Optional[str] = None, save_tstep=False, logging_level=50, output_level=1, debug=False, output_config=None, output_format="txt", ) -> list: """Save SuPy run results to files. .. deprecated:: 2025.11.20 This function is deprecated and will be removed in a future release. Please migrate to `SUEWSSimulation.save`. Parameters ---------- df_output : pandas.DataFrame DataFrame of output df_state_final : pandas.DataFrame DataFrame of final model states freq_s : int, optional Output frequency in seconds (the default is 3600, which indicates hourly output) site : str, optional Site identifier (the default is '', which indicates site identifier will be left empty) path_dir_save : str, optional Path to directory to saving the files (the default is Path('.'), which indicates the current working directory) path_runcontrol : str, optional Path to SUEWS :ref:`RunControl.nml <suews:RunControl.nml>`, which, if set, will be preferably used to derive `freq_s`, `site` and `path_dir_save`. (the default is None, which is unset) save_tstep : bool, optional whether to save results in temporal resolution as in simulation (which may result very large files and slow progress), by default False. logging_level: logging level one of these values [50 (CRITICAL), 40 (ERROR), 30 (WARNING), 20 (INFO), 10 (DEBUG)]. A lower value informs SuPy for more verbose logging info. output_level : integer, optional option to determine selection of output variables, by default 1. Notes: 0 for all but snow-related; 1 for all; 2 for a minimal set without land cover specific information. debug : bool, optional whether to enable debug mode (e.g., writing out in serial mode, and other debug uses), by default False. output_config : OutputConfig, optional Output configuration object specifying format, frequency, and groups to save. If provided, overrides freq_s parameter. Returns ------- list a list of paths of saved files See Also -------- supy.suews_sim.SUEWSSimulation.save : Modern interface for saving results (recommended) Examples -------- 1. save results of a supy run to the current working directory with default settings >>> list_path_save = supy.save_supy(df_output, df_state_final) 2. save results according to settings in :ref:`RunControl.nml <suews:RunControl.nml>` >>> list_path_save = supy.save_supy( ... df_output, df_state_final, path_runcontrol="path/to/RunControl.nml" ... ) 3. save results of a supy run at resampling frequency of 1800 s (i.e., half-hourly results) under the site code ``Test`` to a customised location 'path/to/some/dir' >>> list_path_save = supy.save_supy( ... df_output, ... df_state_final, ... freq_s=1800, ... site="Test", ... path_dir_save="path/to/some/dir", ... ) """ _warn_functional_deprecation("save_supy") return _save_supy( df_output=df_output, df_state_final=df_state_final, freq_s=freq_s, site=site, path_dir_save=path_dir_save, path_runcontrol=path_runcontrol, save_tstep=save_tstep, logging_level=logging_level, output_level=output_level, debug=debug, output_config=output_config, output_format=output_format, )
def _run_supy_sample( start=None, end=None, save_state=False, chunk_day=3660, logging_level=logging.INFO, check_input=False, serial_mode=False, debug_mode=False, ) -> tuple[pandas.DataFrame, pandas.DataFrame, pandas.DataFrame]: """Quickly run SuPy with sample data and return output dataframes. This function loads sample data and runs SuPy simulation in one step, returning the output and final state dataframes. Parameters ---------- save_state : bool, optional Flag for saving model states at each time step. (the default is False) chunk_day : int, optional Chunk size (days) to split simulation periods. (the default is 3660) logging_level : int, optional Logging level for verbosity control. (the default is logging.INFO) check_input : bool, optional Flag for checking validity of input. (the default is False) serial_mode : bool, optional If True, run in serial mode; otherwise try parallel if possible. (the default is False) debug_mode : bool, optional If True, run in debug mode with additional information. (the default is False) Returns ------- df_output, df_state_final : Tuple[pandas.DataFrame, pandas.DataFrame] - df_output: Output results from the simulation - df_state_final: Final model states Examples -------- >>> df_output, df_state_final = supy.run_supy_sample() """ # Load sample data df_state_init, df_forcing = _load_sample_data() # subset forcing data if start is not None: df_forcing = df_forcing[start:] if end is not None: df_forcing = df_forcing[:end] if start is not None and end is not None: df_forcing = df_forcing[start:end] # Run SuPy with the sample data res_supy = _run_supy( df_forcing=df_forcing, df_state_init=df_state_init, save_state=save_state, chunk_day=chunk_day, logging_level=logging_level, check_input=check_input, serial_mode=serial_mode, debug_mode=debug_mode, ) if debug_mode: df_output, df_state_final, df_debug = res_supy return df_output, df_state_final, df_debug else: df_output, df_state_final = res_supy return df_output, df_state_final def run_supy_sample( start=None, end=None, save_state=False, chunk_day=3660, logging_level=logging.INFO, check_input=False, serial_mode=False, debug_mode=False, ) -> tuple[pandas.DataFrame, pandas.DataFrame, pandas.DataFrame]: """Quickly run SuPy with sample data and return output dataframes. .. deprecated:: 2025.11.20 This function is deprecated and will be removed in a future release. Please migrate to `SUEWSSimulation` sample workflows. This function loads sample data and runs SuPy simulation in one step, returning the output and final state dataframes. Parameters ---------- save_state : bool, optional Flag for saving model states at each time step. (the default is False) chunk_day : int, optional Chunk size (days) to split simulation periods. (the default is 3660) logging_level : int, optional Logging level for verbosity control. (the default is logging.INFO) check_input : bool, optional Flag for checking validity of input. (the default is False) serial_mode : bool, optional If True, run in serial mode; otherwise try parallel if possible. (the default is False) debug_mode : bool, optional If True, run in debug mode with additional information. (the default is False) Returns ------- df_output, df_state_final : Tuple[pandas.DataFrame, pandas.DataFrame] - df_output: Output results from the simulation - df_state_final: Final model states See Also -------- supy.suews_sim.SUEWSSimulation : Modern object-oriented interface (recommended) Examples -------- >>> df_output, df_state_final = supy.run_supy_sample() """ _warn_functional_deprecation("run_supy_sample") return _run_supy_sample( start=start, end=end, save_state=save_state, chunk_day=chunk_day, logging_level=logging_level, check_input=check_input, serial_mode=serial_mode, debug_mode=debug_mode, )