Skip to content

hydro_units

The hydro_units module provides comprehensive unit conversion functionality for hydrological data.

Core Functions

streamflow_unit_conv

1
2
3
4
5
6
7
def streamflow_unit_conv(
    streamflow_data: Union[xr.Dataset, pint.Quantity, np.ndarray, pd.DataFrame, pd.Series],
    source_unit: str = None,
    target_unit: str = "m³/s",
    basin_area: float = None,
    time_interval: str = None
) -> Union[xr.Dataset, pint.Quantity, np.ndarray, pd.DataFrame, pd.Series]

Converts streamflow data between different units (depth-based to volume-based and vice versa).

Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import hydroutils as hu
import numpy as np

# Convert from mm/h to m³/s
flow_mmh = np.array([10, 20, 15])  # mm/h
flow_m3s = hu.streamflow_unit_conv(
    flow_mmh, 
    source_unit="mm/h", 
    target_unit="m³/s",
    basin_area=1000  # km²
)

detect_time_interval

1
def detect_time_interval(time_series: Union[pd.Series, np.ndarray, list]) -> str

Automatically detects the time interval from a time series.

get_time_interval_info

1
def get_time_interval_info(time_interval: str) -> dict

Returns detailed information about a time interval.

validate_unit_compatibility

1
def validate_unit_compatibility(unit1: str, unit2: str) -> bool

Validates if two units are compatible for conversion.

API Reference

Hydrological unit conversion utilities.

This module provides comprehensive unit conversion functionality for hydrological data, including streamflow unit conversions between depth units (mm/time) and volume units (m³/s).

detect_time_interval(time_series)

Detect the time interval between points in a time series.

This function analyzes a time series to determine the most common time interval between consecutive points. It handles various input formats and converts the interval to a standardized string format.

Parameters:

Name Type Description Default
time_series Union[DatetimeIndex, list, ndarray]

Time series data containing datetime information. Can be: - pandas DatetimeIndex - List of datetime-like objects - NumPy array of datetime-like objects

required

Returns:

Name Type Description
str str

Detected time interval in format suitable for unit conversion: - For hourly data: "Nh" where N is number of hours (e.g., "3h") - For daily data: "Nd" where N is number of days (e.g., "1d")

Raises:

Type Description
ValueError

If time series has fewer than 2 points.

Note
  • Uses most common time difference (mode) for irregular intervals
  • Rounds non-integer hours to nearest hour
  • Prefers hours for intervals < 24h, days for intervals ≥ 24h
  • Automatically converts various datetime formats to pandas DatetimeIndex
Example

import pandas as pd

Regular 3-hourly data

time_index = pd.date_range("2024-01-01", periods=8, freq="3h") detect_time_interval(time_index) '3h'

Daily data

dates = ["2024-01-01", "2024-01-02", "2024-01-03"] detect_time_interval(dates) '1d'

Mixed intervals (most common is 6h)

times = pd.to_datetime([ ... "2024-01-01 00:00", ... "2024-01-01 06:00", ... "2024-01-01 12:00", ... "2024-01-01 18:00" ... ]) detect_time_interval(times) '6h'

Source code in hydroutils/hydro_units.py
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
def detect_time_interval(
    time_series: Union[pd.DatetimeIndex, list, np.ndarray],
) -> str:
    """Detect the time interval between points in a time series.

    This function analyzes a time series to determine the most common time
    interval between consecutive points. It handles various input formats and
    converts the interval to a standardized string format.

    Args:
        time_series (Union[pd.DatetimeIndex, list, np.ndarray]): Time series
            data containing datetime information. Can be:
            - pandas DatetimeIndex
            - List of datetime-like objects
            - NumPy array of datetime-like objects

    Returns:
        str: Detected time interval in format suitable for unit conversion:
            - For hourly data: "Nh" where N is number of hours (e.g., "3h")
            - For daily data: "Nd" where N is number of days (e.g., "1d")

    Raises:
        ValueError: If time series has fewer than 2 points.

    Note:
        - Uses most common time difference (mode) for irregular intervals
        - Rounds non-integer hours to nearest hour
        - Prefers hours for intervals < 24h, days for intervals ≥ 24h
        - Automatically converts various datetime formats to pandas DatetimeIndex

    Example:
        >>> import pandas as pd
        >>> # Regular 3-hourly data
        >>> time_index = pd.date_range("2024-01-01", periods=8, freq="3h")
        >>> detect_time_interval(time_index)
        '3h'

        >>> # Daily data
        >>> dates = ["2024-01-01", "2024-01-02", "2024-01-03"]
        >>> detect_time_interval(dates)
        '1d'

        >>> # Mixed intervals (most common is 6h)
        >>> times = pd.to_datetime([
        ...     "2024-01-01 00:00",
        ...     "2024-01-01 06:00",
        ...     "2024-01-01 12:00",
        ...     "2024-01-01 18:00"
        ... ])
        >>> detect_time_interval(times)
        '6h'
    """
    if isinstance(time_series, list):
        time_series = pd.to_datetime(time_series)
    elif isinstance(time_series, np.ndarray):
        time_series = pd.to_datetime(time_series)
    elif not isinstance(time_series, pd.DatetimeIndex):
        time_series = pd.DatetimeIndex(time_series)

    if len(time_series) < 2:
        raise ValueError(
            "Time series must have at least 2 time points to detect interval"
        )

    # Calculate time differences
    time_diffs = time_series[1:] - time_series[:-1]

    # Get the most common time difference (mode)
    most_common_diff = time_diffs.value_counts().index[0]

    # Convert to hours and days
    total_seconds = most_common_diff.total_seconds()
    hours = total_seconds / 3600
    days = total_seconds / (3600 * 24)

    # Determine the appropriate unit
    if hours == int(hours) and hours < 24:
        return f"{int(hours)}h"
    elif days == int(days):
        return f"{int(days)}d"
    else:
        # For non-integer hours, round to nearest hour
        rounded_hours = round(hours)
        return f"{rounded_hours}h"

get_time_interval_info(time_interval)

Parse a time interval string into its numeric value and unit.

This function extracts the numeric value and unit from a time interval string using regular expressions. It supports hourly and daily intervals in a standardized format.

Parameters:

Name Type Description Default
time_interval str

Time interval string in format "Nh" or "Nd" where N is a positive integer. Examples: "1h", "3h", "1d", "5d".

required

Returns:

Type Description
Tuple[int, str]

Tuple[int, str]: Two-element tuple containing: - number (int): The numeric value from the interval - unit (str): The unit, either 'h' for hours or 'd' for days

Raises:

Type Description
ValueError

If time_interval doesn't match expected format.

Note
  • Only supports hours ('h') and days ('d') units
  • Number must be a positive integer
  • Format is case-sensitive ('h' and 'd' must be lowercase)
  • No spaces allowed in the interval string
Example

Hourly intervals

get_time_interval_info("3h") (3, 'h') get_time_interval_info("24h") (24, 'h')

Daily intervals

get_time_interval_info("1d") (1, 'd') get_time_interval_info("7d") (7, 'd')

Invalid format raises error

get_time_interval_info("3hours") # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... ValueError: Invalid time interval format: 3hours

Source code in hydroutils/hydro_units.py
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
def get_time_interval_info(time_interval: str) -> Tuple[int, str]:
    """Parse a time interval string into its numeric value and unit.

    This function extracts the numeric value and unit from a time interval
    string using regular expressions. It supports hourly and daily intervals
    in a standardized format.

    Args:
        time_interval (str): Time interval string in format "Nh" or "Nd" where
            N is a positive integer. Examples: "1h", "3h", "1d", "5d".

    Returns:
        Tuple[int, str]: Two-element tuple containing:
            - number (int): The numeric value from the interval
            - unit (str): The unit, either 'h' for hours or 'd' for days

    Raises:
        ValueError: If time_interval doesn't match expected format.

    Note:
        - Only supports hours ('h') and days ('d') units
        - Number must be a positive integer
        - Format is case-sensitive ('h' and 'd' must be lowercase)
        - No spaces allowed in the interval string

    Example:
        >>> # Hourly intervals
        >>> get_time_interval_info("3h")
        (3, 'h')
        >>> get_time_interval_info("24h")
        (24, 'h')

        >>> # Daily intervals
        >>> get_time_interval_info("1d")
        (1, 'd')
        >>> get_time_interval_info("7d")
        (7, 'd')

        >>> # Invalid format raises error
        >>> get_time_interval_info("3hours")  # doctest: +IGNORE_EXCEPTION_DETAIL
        Traceback (most recent call last):
            ...
        ValueError: Invalid time interval format: 3hours
    """
    match = re.match(r"^(\d+)([hd])$", time_interval)
    if not match:
        raise ValueError(f"Invalid time interval format: {time_interval}")

    number, unit = match.groups()
    return int(number), unit

streamflow_unit_conv(data, area, target_unit, source_unit=None, area_unit='km^2')

Convert streamflow data units between depth units (mm/time) and volume units (m³/s).

This function automatically detects conversion direction based on source and target units, removing the need for an explicit inverse parameter.

Parameters

data : numpy.ndarray, pandas.Series, pandas.DataFrame, or xarray.Dataset Streamflow data. Can include unit information in attributes (xarray) or requires source_unit parameter for numpy/pandas data. area : numpy.ndarray, pandas.Series, pandas.DataFrame, xarray.Dataset, or pint.Quantity Basin area data. Units will be detected from data attributes or pint units. If no units detected, area_unit parameter will be used. target_unit : str Target unit for conversion. Examples: "mm/d", "mm/h", "mm/3h", "m^3/s". source_unit : str, optional Source unit of streamflow data. Required if data has no unit information. If provided and data has units, they must match or ValueError is raised. area_unit : str, optional Unit for area when area data has no unit information. Default is "km^2".

Returns

Converted data in the same type as input data. Unit information is preserved in xarray attributes when applicable.

Raises

ValueError If no unit information can be determined for data or area. If source_unit conflicts with detected data units. If units are incompatible for conversion.

Examples

import numpy as np import pandas as pd

Convert m³/s to mm/day

flow = np.array([10.5, 15.2, 8.1]) basin_area = np.array([1000]) # km² result = streamflow_unit_conv(flow, basin_area, "mm/d", source_unit="m^3/s")

Convert mm/h to m³/s

flow_mm = np.array([2.1, 3.5, 1.8]) result = streamflow_unit_conv(flow_mm, basin_area, "m^3/s", source_unit="mm/h")

Source code in hydroutils/hydro_units.py
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
def streamflow_unit_conv(
    data,
    area,
    target_unit,
    source_unit=None,
    area_unit="km^2",
):
    """Convert streamflow data units between depth units (mm/time) and volume units (m³/s).

    This function automatically detects conversion direction based on source and target units,
    removing the need for an explicit inverse parameter.

    Parameters
    ----------
    data : numpy.ndarray, pandas.Series, pandas.DataFrame, or xarray.Dataset
        Streamflow data. Can include unit information in attributes (xarray) or
        requires source_unit parameter for numpy/pandas data.
    area : numpy.ndarray, pandas.Series, pandas.DataFrame, xarray.Dataset, or pint.Quantity
        Basin area data. Units will be detected from data attributes or pint units.
        If no units detected, area_unit parameter will be used.
    target_unit : str
        Target unit for conversion. Examples: "mm/d", "mm/h", "mm/3h", "m^3/s".
    source_unit : str, optional
        Source unit of streamflow data. Required if data has no unit information.
        If provided and data has units, they must match or ValueError is raised.
    area_unit : str, optional
        Unit for area when area data has no unit information. Default is "km^2".

    Returns
    -------
    Converted data in the same type as input data.
    Unit information is preserved in xarray attributes when applicable.

    Raises
    ------
    ValueError
        If no unit information can be determined for data or area.
        If source_unit conflicts with detected data units.
        If units are incompatible for conversion.

    Examples
    --------
    >>> import numpy as np
    >>> import pandas as pd
    >>> # Convert m³/s to mm/day
    >>> flow = np.array([10.5, 15.2, 8.1])
    >>> basin_area = np.array([1000])  # km²
    >>> result = streamflow_unit_conv(flow, basin_area, "mm/d", source_unit="m^3/s")

    >>> # Convert mm/h to m³/s
    >>> flow_mm = np.array([2.1, 3.5, 1.8])
    >>> result = streamflow_unit_conv(flow_mm, basin_area, "m^3/s", source_unit="mm/h")
    """
    # Step 1: Detect and validate source unit
    detected_source_unit = _detect_data_unit(data, source_unit)

    # Step 2: Detect and validate area unit
    detected_area_unit = _detect_area_unit(area, area_unit)

    # Step 3: Determine conversion direction and validate compatibility
    is_depth_to_volume = _determine_conversion_direction(
        detected_source_unit, target_unit
    )

    # Step 4: Early return if no conversion needed
    if _normalize_unit(detected_source_unit) == _normalize_unit(target_unit):
        return data

    # Step 5: Perform the actual conversion based on data type
    return _perform_conversion(
        data,
        area,
        detected_source_unit,
        detected_area_unit,
        target_unit,
        is_depth_to_volume,
    )

validate_unit_compatibility(source_unit, target_unit)

Check if two hydrological units can be converted between each other.

This function determines whether two units are compatible for hydrological unit conversion. It supports depth units (mm/time) and volume units (m³/s), and checks if the conversion between them is possible.

Parameters:

Name Type Description Default
source_unit str

Source unit string. Examples: - Depth units: "mm/h", "mm/3h", "mm/d", "in/d" - Volume units: "m^3/s", "ft^3/s", "l/s"

required
target_unit str

Target unit string (same format as source_unit).

required

Returns:

Name Type Description
bool bool

True if units are compatible for conversion, False otherwise.

Note
  • Supports various time intervals for depth units
  • Recognizes multiple formats for volume units
  • Case-sensitive unit matching
  • Compatible conversions:
    • depth -> volume (e.g., mm/h -> m³/s)
    • volume -> depth (e.g., m³/s -> mm/d)
    • depth -> depth (e.g., mm/h -> mm/d)
    • volume -> volume (e.g., m³/s -> ft³/s)
Example

Compatible conversions

validate_unit_compatibility("mm/3h", "m^3/s") True validate_unit_compatibility("m^3/s", "mm/d") True validate_unit_compatibility("mm/h", "mm/d") True

Incompatible conversions

validate_unit_compatibility("mm/h", "celsius") False validate_unit_compatibility("m^3/s", "kg/m^3") False

Source code in hydroutils/hydro_units.py
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
def validate_unit_compatibility(source_unit: str, target_unit: str) -> bool:
    """Check if two hydrological units can be converted between each other.

    This function determines whether two units are compatible for hydrological
    unit conversion. It supports depth units (mm/time) and volume units (m³/s),
    and checks if the conversion between them is possible.

    Args:
        source_unit (str): Source unit string. Examples:
            - Depth units: "mm/h", "mm/3h", "mm/d", "in/d"
            - Volume units: "m^3/s", "ft^3/s", "l/s"
        target_unit (str): Target unit string (same format as source_unit).

    Returns:
        bool: True if units are compatible for conversion, False otherwise.

    Note:
        - Supports various time intervals for depth units
        - Recognizes multiple formats for volume units
        - Case-sensitive unit matching
        - Compatible conversions:
            - depth -> volume (e.g., mm/h -> m³/s)
            - volume -> depth (e.g., m³/s -> mm/d)
            - depth -> depth (e.g., mm/h -> mm/d)
            - volume -> volume (e.g., m³/s -> ft³/s)

    Example:
        >>> # Compatible conversions
        >>> validate_unit_compatibility("mm/3h", "m^3/s")
        True
        >>> validate_unit_compatibility("m^3/s", "mm/d")
        True
        >>> validate_unit_compatibility("mm/h", "mm/d")
        True

        >>> # Incompatible conversions
        >>> validate_unit_compatibility("mm/h", "celsius")
        False
        >>> validate_unit_compatibility("m^3/s", "kg/m^3")
        False
    """
    # Define unit categories
    depth_units = re.compile(r"mm/\d*[hd]")
    volume_units = re.compile(r"m\^?3/s")

    source_is_depth = bool(depth_units.match(source_unit))
    source_is_volume = bool(volume_units.match(source_unit))
    target_is_depth = bool(depth_units.match(target_unit))
    target_is_volume = bool(volume_units.match(target_unit))

    # Compatible if both are depth units, both are volume units, or one of each
    return (source_is_depth or source_is_volume) and (
        target_is_depth or target_is_volume
    )