Skip to content

XAJ-SLW Model (Songliao)

xaj_slw is a XinAnJiang variant developed for the Songliao basin, using SMS3 and LAG3 routing (ported from hydromodeljava). It is the only registered model whose internal routing works in discharge (m^3/s); UnifiedSimulator converts its qsim back to runoff depth (mm) so results are comparable with other models and observations.

  • Registered name: xaj_slw
  • Parameters: 26
  • Requires: basin_area (km^2) and time_interval_hours at simulation time
  • Routing: SMS3 + LAG3 (storage-lag-weighted)

API Reference

Copyright (c) 2023-2024 Wenyu Ouyang. All rights reserved.

calculate_net_precipitation(precipitation, potential_evapotranspiration, kc)

Calculate net precipitation and actual evapotranspiration.

Parameters:

Name Type Description Default
precipitation ndarray

Precipitation array.

required
potential_evapotranspiration ndarray

Potential evapotranspiration array.

required
kc ndarray

Evapotranspiration coefficient array.

required

Returns:

Type Description
Tuple[numpy.ndarray, numpy.ndarray]

Tuple containing net precipitation and actual evapotranspiration (pe, edt).

Source code in hydromodel/models/xaj_slw.py
@jit(nopython=True)
def calculate_net_precipitation(
    precipitation: np.ndarray,
    potential_evapotranspiration: np.ndarray,
    kc: np.ndarray,
) -> Tuple[np.ndarray, np.ndarray]:
    """Calculate net precipitation and actual evapotranspiration.

    Args:
        precipitation: Precipitation array.
        potential_evapotranspiration: Potential evapotranspiration array.
        kc: Evapotranspiration coefficient array.

    Returns:
        Tuple containing net precipitation and actual evapotranspiration (pe, edt).
    """
    edt = kc * potential_evapotranspiration  # Actual evapotranspiration
    pe = precipitation - edt  # Net precipitation
    return pe, edt

lag3_routing_vectorized(rs, ri, rg, time_interval, basin_area, ci, cg, lag, cs, kk, x, mp, qsp=0.0, qip=0.0, qgp=0.0, qsig_initial=None, qx_initial=None, return_states=False)

Vectorized LAG3 routing model.

Parameters:

Name Type Description Default
rs ndarray

Surface runoff.

required
ri ndarray

Interflow runoff.

required
rg ndarray

Groundwater runoff.

required
time_interval float

Time interval.

required
basin_area float

Basin area.

required
ci float

Interflow recession coefficient.

required
cg float

Groundwater recession coefficient.

required
lag float

Lag time.

required
cs float

Surface runoff recession coefficient.

required
kk float

Muskingum K parameter.

required
x float

Muskingum X parameter.

required
mp int

Number of river reaches.

required
qsp float

Initial surface flow.

0.0
qip float

Initial interflow.

0.0
qgp float

Initial groundwater flow.

0.0
qsig_initial ndarray

Initial qsig state array.

None
qx_initial ndarray

Initial qx state array.

None
return_states bool

Whether to return state variables.

False

Returns:

Type Description
Union[numpy.ndarray, Tuple[numpy.ndarray, Dict[str, numpy.ndarray]]]

If return_states is False, returns final flow array. If return_states is True, returns (flow_array, state_dict) tuple.

Source code in hydromodel/models/xaj_slw.py
def lag3_routing_vectorized(
    rs: np.ndarray,
    ri: np.ndarray,
    rg: np.ndarray,
    time_interval: float,
    basin_area: float,
    ci: float,
    cg: float,
    lag: float,
    cs: float,
    kk: float,
    x: float,
    mp: int,
    qsp: float = 0.0,
    qip: float = 0.0,
    qgp: float = 0.0,
    qsig_initial: np.ndarray = None,
    qx_initial: np.ndarray = None,
    return_states: bool = False,
) -> Union[np.ndarray, Tuple[np.ndarray, Dict[str, np.ndarray]]]:
    """Vectorized LAG3 routing model.

    Args:
        rs: Surface runoff.
        ri: Interflow runoff.
        rg: Groundwater runoff.
        time_interval: Time interval.
        basin_area: Basin area.
        ci: Interflow recession coefficient.
        cg: Groundwater recession coefficient.
        lag: Lag time.
        cs: Surface runoff recession coefficient.
        kk: Muskingum K parameter.
        x: Muskingum X parameter.
        mp: Number of river reaches.
        qsp: Initial surface flow.
        qip: Initial interflow.
        qgp: Initial groundwater flow.
        qsig_initial: Initial qsig state array.
        qx_initial: Initial qx state array.
        return_states: Whether to return state variables.

    Returns:
        If return_states is False, returns final flow array.
        If return_states is True, returns (flow_array, state_dict) tuple.
    """
    # === 诊断日志:LAG3 输入参数 ===
    logger.info(f"=== LAG3 汇流计算开始 ===")
    logger.info(f"rs 类型: {type(rs)}, 形状: {rs.shape if hasattr(rs, 'shape') else 'no shape'}, 值: {rs if not hasattr(rs, 'shape') else rs[:5] if len(rs) > 5 else rs}")
    logger.info(f"ri 类型: {type(ri)}, 形状: {ri.shape if hasattr(ri, 'shape') else 'no shape'}")
    logger.info(f"rg 类型: {type(rg)}, 形状: {rg.shape if hasattr(rg, 'shape') else 'no shape'}")
    logger.info(f"lag 参数: {lag}, cs: {cs}, ci: {ci}, cg: {cg}")

    # 防御性检查:确保 rs, ri, rg 是数组
    if not hasattr(rs, '__len__'):
        raise TypeError(f"rs 必须是数组类型,但收到 {type(rs)}: {rs}")
    if not hasattr(ri, '__len__'):
        raise TypeError(f"ri 必须是数组类型,但收到 {type(ri)}: {ri}")
    if not hasattr(rg, '__len__'):
        raise TypeError(f"rg 必须是数组类型,但收到 {type(rg)}: {rg}")

    time_steps = len(rs)
    t_steps = int(round(lag / time_interval))  # 新的滞时步数计算
    t = max(t_steps, 0)  # 确保非负值

    # 单位转换系数
    cp = basin_area / time_interval / 3.6

    # 参数时段转换
    ci = np.power(ci, time_interval / 24.0)
    cg = np.power(cg, time_interval / 24.0)

    # 初始化输出数组
    qs = np.zeros(time_steps)
    qi = np.zeros(time_steps)
    qg = np.zeros(time_steps)
    qsig = np.zeros(time_steps + t)

    # 初始化QSIG数组
    if qsig_initial is None:
        qsig_initial = np.zeros(max(t, 3))

    if lag <= 1:
        # 防御性检查:避免空数组导致索引越界
        if len(qsig) > 0 and len(qsig_initial) > 0:
            qsig[0] = qsig_initial[0]
    else:
        for i in range(t):
            if len(qsig_initial) >= t:
                qsig[i] = qsig_initial[i]
            else:
                if i < len(qsig_initial):
                    qsig[i] = qsig_initial[i]
                else:
                    qsig[i] = qsig[i - 1]

    # 初始化马斯京根参数
    fkt = kk - kk * x + 0.5 * time_interval
    c0 = (0.5 * time_interval - kk * x) / fkt
    c1 = (kk * x + 0.5 * time_interval) / fkt
    c2 = (kk - kk * x - 0.5 * time_interval) / fkt

    # 初始化QX数组
    qx = np.zeros(mp + 1)
    if qx_initial is not None:
        for i in range(mp + 1):
            if i < len(qx_initial):
                qx[i] = qx_initial[i]
            else:
                qx[i] = qx[i - 1] if i > 0 else 0.0

    # 主循环计算
    qip_curr = qip
    qgp_curr = qgp
    qsig1 = qsig[t - 1] if lag > 1 else qsig[0]

    for i in range(time_steps):
        # 计算三水源汇流
        qgp_curr = qgp_curr * cg + rg[i] * (1.0 - cg) * cp
        qip_curr = qip_curr * ci + ri[i] * (1.0 - ci) * cp
        qsp_curr = rs[i] * cp

        # 存储结果
        qg[i] = qgp_curr
        qi[i] = qip_curr
        qs[i] = qsp_curr

        # 计算总入流并更新QSIG
        qsig1 = qsig1 * cs + (qgp_curr + qip_curr + qsp_curr) * (1.0 - cs)
        qtsig = qsig1

        # 使用LCHCO进行马斯京根演算
        qsig[i + t] = lchco_vectorized(mp, qtsig, qx, c0, c1, c2)

    # 提取最终结果
    q_routing = qsig[:time_steps]

    # 确保非负值
    q_routing = np.maximum(q_routing, 0.0)

    if return_states:
        # 返回最终状态
        final_states = {
            "qsig_final": qsig[time_steps:],
            "qx_final": qx.copy(),
            "qsig1_final": qsig1,
        }
        return q_routing, final_states
    else:
        return q_routing

lchco_vectorized(mp, rq, qx, c0, c1, c2)

Vectorized LCHCO calculation.

Parameters:

Name Type Description Default
mp int

Number of river reaches.

required
rq float

Inflow rate.

required
qx ndarray

Flow array.

required
c0 float

Muskingum coefficient 0.

required
c1 float

Muskingum coefficient 1.

required
c2 float

Muskingum coefficient 2.

required

Returns:

Type Description
float

Calculated flow value.

Source code in hydromodel/models/xaj_slw.py
def lchco_vectorized(
    mp: int, rq: float, qx: np.ndarray, c0: float, c1: float, c2: float
) -> float:
    """Vectorized LCHCO calculation.

    Args:
        mp: Number of river reaches.
        rq: Inflow rate.
        qx: Flow array.
        c0: Muskingum coefficient 0.
        c1: Muskingum coefficient 1.
        c2: Muskingum coefficient 2.

    Returns:
        Calculated flow value.
    """
    im = mp + 1
    if im == 1:
        qx[int(im - 1)] = rq
    else:
        for j in range(1, int(im)):
            q1 = rq
            q2 = qx[j - 1]
            q3 = qx[j]
            qx[j - 1] = rq
            rq = c0 * q1 + c1 * q2 + c2 * q3
        qx[int(im - 1)] = rq
    return rq

load_sms_lag_data_from_json(sms_json_path, lag_json_path, default_evap)

Load XAJ model data from SMS and LAG JSON files.

Parameters:

Name Type Description Default
sms_json_path str

Path to SMS_3 model JSON file.

required
lag_json_path str

Path to LAG_3 model JSON file.

required
default_evap float

Default evapotranspiration value.

required

Returns:

Type Description
Tuple containing
  • p_and_e: Precipitation and evapotranspiration data [time, basin=1, feature=2]
  • parameters: Model parameters [basin=1, parameter=26]
  • time_dates: List of time dates
  • start_time: Start time
  • es: Monthly evapotranspiration array
Source code in hydromodel/models/xaj_slw.py
def load_sms_lag_data_from_json(
    sms_json_path: str,
    lag_json_path: str,
    default_evap: float,
) -> Tuple[np.ndarray, np.ndarray, List[str], str]:
    """Load XAJ model data from SMS and LAG JSON files.

    Args:
        sms_json_path: Path to SMS_3 model JSON file.
        lag_json_path: Path to LAG_3 model JSON file.
        default_evap: Default evapotranspiration value.

    Returns:
        Tuple containing:
        - p_and_e: Precipitation and evapotranspiration data [time, basin=1, feature=2]
        - parameters: Model parameters [basin=1, parameter=26]
        - time_dates: List of time dates
        - start_time: Start time
        - es: Monthly evapotranspiration array
    """
    # 读取SMS JSON文件
    with open(sms_json_path, "r", encoding="utf-8") as f:
        sms_data = json.load(f)

    # 读取LAG JSON文件
    with open(lag_json_path, "r", encoding="utf-8") as f:
        lag_data = json.load(f)

    # 解析时间序列和降雨数据
    dt = sms_data["dt"]
    rain = sms_data["rain"]
    start_time = lag_data.get("start", dt[0] if dt else "")

    # 解析月蒸发量数组
    es = np.array(
        sms_data.get(
            "ES",
            [
                23.8,
                23.2,
                33.7,
                53,
                66.6,
                78.9,
                122.3,
                113.9,
                90.8,
                62.9,
                46.2,
                34.1,
            ],
        )
    )

    # 构建p_and_e数组 [time, basin=1, feature=2]
    time_steps = len(rain)
    p_and_e = np.zeros((time_steps, 1, 2))
    p_and_e[:, 0, 0] = rain  # 降雨数据

    # 根据ES数组计算蒸散发值
    if "ES" in sms_data:
        # 获取时间间隔参数
        time_interval = float(sms_data.get("clen", 1.0))

        # 计算每个时间步的蒸散发值
        evap_values = np.zeros(time_steps)
        for i in range(time_steps):
            # 从时间字符串中提取月份,默认为8月
            try:
                if dt and i < len(dt):
                    time_str = dt[i]
                    # 尝试解析时间字符串获取月份
                    if ":" in time_str:  # 包含时间的格式
                        month = int(time_str.split("-")[1])  # 提取月份
                    else:  # 只有日期的格式
                        month = int(time_str.split("-")[1])  # 提取月份
                else:
                    month = 8  # 默认月份为8
            except:
                month = 8  # 解析失败时使用默认月份

            # 根据月份确定天数
            if month in [4, 6, 9, 11]:
                iday = 30
            elif month == 2:
                iday = 28
            else:
                iday = 31

            # 计算蒸散发值:ES[month-1] / (IDAY * 24.0 / T)
            em = es[month - 1] / (iday * 24.0 / time_interval)
            evap_values[i] = em
    else:
        # 如果没有ES数组,使用输入的蒸散发数值
        evap_values = np.full(time_steps, default_evap)

    p_and_e[:, 0, 1] = evap_values

    # 构建参数数组 [basin=1, parameter=26]
    # 参数顺序: [WUP, WLP, WDP, SP, FRP, WM, WUMx, WLMx, KC, B, C, IM,
    #          SM, EX, KG, KI, CS, CI, CG, LAG, KK, X, MP, QSP, QIP, QGP]
    parameters = np.array(
        [
            [
                float(sms_data["WUP"]),  # 0: Initial upper layer tension water
                float(sms_data["WLP"]),  # 1: Initial lower layer tension water
                float(sms_data["WDP"]),  # 2: Initial deep layer tension water
                float(sms_data["SP"]),  # 3: Initial free water storage
                float(sms_data["FRP"]),  # 4: Initial runoff basin_area ratio
                float(sms_data["WM"]),  # 5: Total tension water capacity
                float(sms_data["WUMx"]),  # 6: Upper layer capacity ratio
                float(sms_data["WLMx"]),  # 7: Lower layer capacity ratio
                float(sms_data["K"]),  # 8: Evaporation coefficient (KC)
                float(
                    sms_data["B"]
                ),  # 9: Exponent of tension water capacity curve
                float(
                    sms_data["C"]
                ),  # 10: Deep evapotranspiration coefficient
                float(sms_data["IM"]),  # 11: Impervious basin_area ratio
                float(sms_data["SM"]),  # 12: Average free water capacity
                float(
                    sms_data["EX"]
                ),  # 13: Exponent of free water capacity curve
                float(sms_data["KG"]),  # 14: Groundwater outflow coefficient
                float(sms_data["KI"]),  # 15: Interflow outflow coefficient
                float(lag_data["CS"]),  # 16: Channel system recession constant
                float(
                    lag_data["CI"]
                ),  # 17: Lower interflow recession constant
                float(
                    lag_data["CG"]
                ),  # 18: Groundwater storage recession constant
                float(lag_data["LAG"]),  # 19: Lag time
                float(lag_data["KK"]),  # 20: Muskingum K parameter
                float(lag_data["X"]),  # 21: Muskingum X parameter
                float(lag_data["MP"]),  # 22: Number of Muskingum reaches
                float(lag_data["QSP"]),  # 23: Initial surface flow
                float(lag_data["QIP"]),  # 24: Initial interflow
                float(lag_data["QGP"]),  # 25: Initial groundwater flow
            ]
        ]
    )

    return p_and_e, parameters, dt, start_time, es

sms3_runoff_generation_vectorized(precipitation, evapotranspiration, wu, wl, wd, s, fr, wm, wumx, wlmx, kc, b, c, im, sm, ex, kg, ki, time_interval, time_steps)

Vectorized SMS3 runoff generation model.

Parameters:

Name Type Description Default
precipitation ndarray

Precipitation time series.

required
evapotranspiration ndarray

Evapotranspiration time series.

required
wu ndarray

Initial upper layer tension water content.

required
wl ndarray

Initial lower layer tension water content.

required
wd ndarray

Initial deep layer tension water content.

required
s ndarray

Initial free water storage.

required
fr ndarray

Initial runoff area ratio.

required
wm float

Total tension water capacity.

required
wumx float

Upper layer capacity ratio.

required
wlmx float

Lower layer capacity ratio.

required
kc float

Evapotranspiration coefficient.

required
b float

Exponent of tension water capacity curve.

required
c float

Deep evapotranspiration coefficient.

required
im float

Impervious area ratio.

required
sm float

Average free water capacity.

required
ex float

Exponent of free water capacity curve.

required
kg float

Groundwater outflow coefficient.

required
ki float

Interflow outflow coefficient.

required
time_interval float

Time interval.

required
time_steps int

Number of time steps.

required

Returns:

Type Description
Tuple containing runoff results and state variables

(wu_out, wl_out, wd_out, s_out, fr_out, rs, ri, rg, runoff_total)

Source code in hydromodel/models/xaj_slw.py
def sms3_runoff_generation_vectorized(
    precipitation: np.ndarray,
    evapotranspiration: np.ndarray,
    wu: np.ndarray,
    wl: np.ndarray,
    wd: np.ndarray,
    s: np.ndarray,
    fr: np.ndarray,
    wm: float,
    wumx: float,
    wlmx: float,
    kc: float,
    b: float,
    c: float,
    im: float,
    sm: float,
    ex: float,
    kg: float,
    ki: float,
    time_interval: float,
    time_steps: int,
) -> Tuple[
    np.ndarray,
    np.ndarray,
    np.ndarray,
    np.ndarray,
    np.ndarray,
    np.ndarray,
    np.ndarray,
    np.ndarray,
    np.ndarray,
]:
    """Vectorized SMS3 runoff generation model.

    Args:
        precipitation: Precipitation time series.
        evapotranspiration: Evapotranspiration time series.
        wu: Initial upper layer tension water content.
        wl: Initial lower layer tension water content.
        wd: Initial deep layer tension water content.
        s: Initial free water storage.
        fr: Initial runoff area ratio.
        wm: Total tension water capacity.
        wumx: Upper layer capacity ratio.
        wlmx: Lower layer capacity ratio.
        kc: Evapotranspiration coefficient.
        b: Exponent of tension water capacity curve.
        c: Deep evapotranspiration coefficient.
        im: Impervious area ratio.
        sm: Average free water capacity.
        ex: Exponent of free water capacity curve.
        kg: Groundwater outflow coefficient.
        ki: Interflow outflow coefficient.
        time_interval: Time interval.
        time_steps: Number of time steps.

    Returns:
        Tuple containing runoff results and state variables:
        (wu_out, wl_out, wd_out, s_out, fr_out, rs, ri, rg, runoff_total)
    """
    # Calculate derived parameters
    wum = wumx * wm
    wlm = (1.0 - wumx) * wlmx * wm
    wdm = wm - wum - wlm
    wmm = (1.0 + b) * wm / (1.0 - im)
    smm = (1.0 + ex) * sm

    # Adjust KG and KI
    if kg + ki > 0.9:
        tmp = (kg + ki - 0.9) / (kg + ki)
        kg = kg - kg * tmp
        ki = ki - ki * tmp

    # Calculate HGI and adjust KG and KI
    hgi = (1.0 - np.power((1.0 - kg - ki), (time_interval / 24.0))) / (kg + ki)
    kg = hgi * kg
    ki = hgi * ki

    # 初始化输出数组
    wu_out = np.zeros(time_steps)
    wl_out = np.zeros(time_steps)
    wd_out = np.zeros(time_steps)
    s_out = np.zeros(time_steps)
    fr_out = np.zeros(time_steps)
    rs = np.zeros(time_steps)  # 产流数组长度不变
    ri = np.zeros(time_steps)
    rg = np.zeros(time_steps)
    runoff_total = np.zeros(time_steps)

    # 设置初始值
    wu_curr = wu[0]
    wl_curr = wl[0]
    wd_curr = wd[0]
    s_curr = s[0]
    fr_curr = fr[0]

    # 存储初始值
    wu_out[0] = wu_curr
    wl_out[0] = wl_curr
    wd_out[0] = wd_curr
    s_out[0] = s_curr
    fr_out[0] = fr_curr

    div = 5.0

    # 主循环
    for i in range(time_steps - 1):
        ek = kc * evapotranspiration[i]
        pe = precipitation[i] - ek

        # 产流计算
        w_curr = wu_curr + wl_curr + wd_curr

        if pe >= 2 * div:
            nd = int(np.floor(pe / div))
            ped = np.full(nd, div)
            ped[-1] = pe - (nd - 1) * div
        else:
            nd = 1
            ped = np.array([pe])

        rd = np.zeros(nd)

        # YIELD1计算
        if pe <= 0.0:
            r = 0.0
            if wu_curr + pe >= 0.0:
                wu_curr = wu_curr + pe
            else:
                eu = wu_curr + ek + pe
                wu_curr = 0.0
                el = (ek - eu) * wl_curr / wlm
                if wl_curr < c * wlm:
                    el = c * (ek - eu)
                if wl_curr - el < 0.0:
                    ed = el - wl_curr
                    el = wl_curr
                    wl_curr = 0.0
                    wd_curr = wd_curr - ed
                else:
                    wl_curr = wl_curr - el
            w_curr = wu_curr + wl_curr + wd_curr
        else:
            a = 0.0
            if wm - w_curr < 0.0001:
                a = wmm
            else:
                a = wmm * (
                    1.0 - np.power((1.0 - w_curr / wm), (1.0 / (1.0 + b)))
                )

            r = 0.0
            peds = 0.0
            for j in range(nd):
                a = a + ped[j]
                peds = peds + ped[j]
                ri_temp = r
                r = peds - wm + w_curr
                if a < wmm:
                    r = r + wm * np.power((1.0 - a / wmm), (1.0 + b))
                rd[j] = r - ri_temp

            if wu_curr + pe - r <= wum:
                wu_curr = wu_curr + pe - r
            else:
                if wu_curr + wl_curr + pe - r - wum >= wlm:
                    wu_curr = wum
                    wl_curr = wlm
                    wd_curr = w_curr + peds - r - wu_curr - wl_curr
                    if wd_curr > wdm:
                        wd_curr = wdm
                else:
                    wl_curr = wu_curr + wl_curr + pe - r - wum
                    wu_curr = wum
            w_curr = wu_curr + wl_curr + wd_curr

        # DIVI31计算
        if pe <= 0.0:
            rs_curr = 0.0
            rg_curr = s_curr * kg * fr_curr
            ri_curr = s_curr * ki * fr_curr
            s_curr = s_curr * (1.0 - kg - ki)
        else:
            rb = im * pe
            kid = (1.0 - np.power((1.0 - (kg + ki)), (1.0 / nd))) / (kg + ki)
            kgd = kid * kg
            kid = kid * ki

            rs_curr = 0.0
            ri_curr = 0.0
            rg_curr = 0.0

            for j in range(nd):
                td = rd[j] - im * ped[j]
                x = fr_curr
                if ped[j] > 0:
                    fr_curr = td / ped[j]
                    s_curr = x * s_curr / max(fr_curr, 0.0001)

                rr = 0
                if s_curr >= sm:
                    rr = (ped[j] + s_curr - sm) * fr_curr
                else:
                    au = smm * (
                        1.0 - np.power((1.0 - s_curr / sm), (1.0 / (1.0 + ex)))
                    )
                    if au + ped[j] < smm:
                        rr = (
                            ped[j]
                            - sm
                            + s_curr
                            + sm
                            * np.power((1.0 - (ped[j] + au) / smm), (1.0 + ex))
                        ) * fr_curr
                    else:
                        rr = (ped[j] + s_curr - sm) * fr_curr

                rs_curr = rr + rs_curr
                s_curr = ped[j] - rr / max(fr_curr, 0.0001) + s_curr
                rg_curr = s_curr * kgd * fr_curr + rg_curr
                ri_curr = s_curr * kid * fr_curr + ri_curr
                s_curr = s_curr * (1.0 - kid - kgd)

            rs_curr = rs_curr + rb

        # 确保非负值
        rs_curr = max(0.0, rs_curr)
        ri_curr = max(0.0, ri_curr)
        rg_curr = max(0.0, rg_curr)

        # 存储结果
        wu_out[i + 1] = wu_curr
        wl_out[i + 1] = wl_curr
        wd_out[i + 1] = wd_curr
        s_out[i + 1] = s_curr
        fr_out[i + 1] = fr_curr
        rs[i] = rs_curr
        ri[i] = ri_curr
        rg[i] = rg_curr
        runoff_total[i] = rs_curr + ri_curr + rg_curr

    return wu_out, wl_out, wd_out, s_out, fr_out, rs, ri, rg, runoff_total

xaj_slw(p_and_e, parameters, warmup_length=365, return_state=False, return_warmup_states=False, normalized_params='auto', **kwargs)

Vectorized XinAnJiang Songliao hydrological model using SMS3 and LAG3 algorithms.

Parameters:

Name Type Description Default
p_and_e ndarray

Precipitation and potential evapotranspiration data, 3D array [time, basin, feature=2]. Feature 0 is precipitation, feature 1 is potential evapotranspiration.

required
parameters ndarray

Model parameters, 2D array [basin, parameter]. Parameter order: [WUP, WLP, WDP, SP, FRP, WM, WUMx, WLMx, KC, B, C, IM, SM, EX, KG, KI, CS, CI, CG, LAG, KK, X, MP, QSP, QIP, QGP].

required
warmup_length int

Warmup period length, default is 365.

365
return_state bool

If True, returns internal state variables, default is False.

False
return_warmup_states bool

If True, returns initial states after warmup, default is False.

False
normalized_params Union[bool, str]

Parameter format specification: - "auto": Automatically detect parameter format (default) - True: Parameters are normalized (0-1 range), convert to original scale - False: Parameters are in original scale, use directly

'auto'
**kwargs

Other keyword arguments: - time_interval_hours (float): Time interval in hours, default is 3.0 - basin_area (float): REQUIRED - Basin area in km² Used for converting runoff depth (mm) to discharge (m³/s) - initial_states (dict): Initial state values to override after warmup

{}

Returns:

Type Description
Results in different formats based on parameters
  • If return_state is False and return_warmup_states is False: Returns QSim array [time, basin, 1]
  • If return_state is False and return_warmup_states is True: Returns (QSim, warmup_states) tuple, where warmup_states contains all state variables
  • If return_state is True and return_warmup_states is False: Returns (QSim, runoffSim, rs, ri, rg, pe, wu, wl, wd) tuple
  • If return_state is True and return_warmup_states is True: Returns (QSim, runoffSim, rs, ri, rg, pe, wu, wl, wd, warmup_states) tuple

Exceptions:

Type Description
ValueError

If basin_area is not provided in kwargs.

Note

The basin_area parameter is REQUIRED for XAJ-SLW model because it needs to convert runoff depth (mm) to discharge (m³/s) internally. For CAMELS datasets, basin_area is automatically retrieved from basin attributes. For custom datasets, ensure basin_area is included in your basin configuration or passed explicitly.

Source code in hydromodel/models/xaj_slw.py
def xaj_slw(
    p_and_e: np.ndarray,
    parameters: np.ndarray,
    warmup_length: int = 365,
    return_state: bool = False,
    return_warmup_states: bool = False,
    normalized_params: Union[bool, str] = "auto",
    **kwargs,
) -> Union[
    np.ndarray,
    Tuple[
        np.ndarray,
        np.ndarray,
        np.ndarray,
        np.ndarray,
        np.ndarray,
        np.ndarray,
        np.ndarray,
        np.ndarray,
        np.ndarray,
    ],
    Tuple[
        np.ndarray,
        Dict[str, np.ndarray],
    ],
]:
    """Vectorized XinAnJiang Songliao hydrological model using SMS3 and LAG3 algorithms.

    Args:
        p_and_e: Precipitation and potential evapotranspiration data, 3D array [time, basin, feature=2].
            Feature 0 is precipitation, feature 1 is potential evapotranspiration.
        parameters: Model parameters, 2D array [basin, parameter].
            Parameter order: [WUP, WLP, WDP, SP, FRP, WM, WUMx, WLMx, KC, B, C, IM,
                             SM, EX, KG, KI, CS, CI, CG, LAG, KK, X, MP, QSP, QIP, QGP].
        warmup_length: Warmup period length, default is 365.
        return_state: If True, returns internal state variables, default is False.
        return_warmup_states: If True, returns initial states after warmup, default is False.
        normalized_params: Parameter format specification:
            - "auto": Automatically detect parameter format (default)
            - True: Parameters are normalized (0-1 range), convert to original scale
            - False: Parameters are in original scale, use directly
        **kwargs: Other keyword arguments:
            - time_interval_hours (float): Time interval in hours, default is 3.0
            - basin_area (float): **REQUIRED** - Basin area in km²
                Used for converting runoff depth (mm) to discharge (m³/s)
            - initial_states (dict): Initial state values to override after warmup

    Returns:
        Results in different formats based on parameters:
        - If return_state is False and return_warmup_states is False:
          Returns QSim array [time, basin, 1]
        - If return_state is False and return_warmup_states is True:
          Returns (QSim, warmup_states) tuple, where warmup_states contains all state variables
        - If return_state is True and return_warmup_states is False:
          Returns (QSim, runoffSim, rs, ri, rg, pe, wu, wl, wd) tuple
        - If return_state is True and return_warmup_states is True:
          Returns (QSim, runoffSim, rs, ri, rg, pe, wu, wl, wd, warmup_states) tuple

    Raises:
        ValueError: If basin_area is not provided in kwargs.

    Note:
        The basin_area parameter is REQUIRED for XAJ-SLW model because it needs to
        convert runoff depth (mm) to discharge (m³/s) internally. For CAMELS datasets,
        basin_area is automatically retrieved from basin attributes. For custom datasets,
        ensure basin_area is included in your basin configuration or passed explicitly.
    """
    time_steps, num_basins, _ = p_and_e.shape
    time_interval = kwargs.get("time_interval_hours", 3.0)
    basin_area = kwargs.get("basin_area", None)  # km^2

    # === 诊断日志:xaj_slw 函数入口 ===
    logger.info(f"=== XAJ_SLW 模型计算开始 ===")
    logger.info(f"p_and_e 形状: {p_and_e.shape}, 类型: {type(p_and_e)}")
    logger.info(f"parameters 形状: {parameters.shape}, 类型: {type(parameters)}")
    logger.info(f"warmup_length: {warmup_length}")
    logger.info(f"basin_area: {basin_area}")
    logger.info(f"kwargs 参数: {list(kwargs.keys())}")
    if "initial_states" in kwargs and kwargs["initial_states"] is not None:
        logger.info(f"initial_states: {kwargs['initial_states']}")

    if basin_area is None:
        raise ValueError(
            "basin_area must be provided for XAJ-SLW model. "
            "It is required for converting runoff depth (mm) to discharge (m³/s). "
            "Please ensure basin_area is included in basin_config when initializing UnifiedSimulator, "
            "or pass it explicitly in kwargs."
        )

    # Process parameters using unified parameter handling
    processed_parameters = parameters.copy()
    if normalized_params != False:
        model_param_dict = get_model_param_config("xaj_slw", kwargs)
        param_ranges = model_param_dict["param_range"]
        processed_parameters = process_parameters(
            parameters, param_ranges, normalized=normalized_params
        )

    # Extract parameters - all are [basin] arrays
    wup = processed_parameters[:, 0]  # Initial upper layer tension water
    wlp = processed_parameters[:, 1]  # Initial lower layer tension water
    wdp = processed_parameters[:, 2]  # Initial deep layer tension water
    sp = processed_parameters[:, 3]  # Initial free water storage
    frp = processed_parameters[:, 4]  # Initial runoff basin_area ratio
    wm = processed_parameters[:, 5]  # Total tension water capacity
    wumx = processed_parameters[:, 6]  # Upper layer capacity ratio
    wlmx = processed_parameters[:, 7]  # Lower layer capacity ratio
    kc = processed_parameters[:, 8]  # Evaporation coefficient
    b = processed_parameters[:, 9]  # Exponent of tension water capacity curve
    c = processed_parameters[:, 10]  # Deep evapotranspiration coefficient
    im = processed_parameters[:, 11]  # Impervious basin_area ratio
    sm = processed_parameters[:, 12]  # Average free water capacity
    ex = processed_parameters[:, 13]  # Exponent of free water capacity curve
    kg = processed_parameters[:, 14]  # Groundwater outflow coefficient
    ki = processed_parameters[:, 15]  # Interflow outflow coefficient
    cs = processed_parameters[:, 16]  # Channel system recession constant
    ci = processed_parameters[:, 17]  # Lower interflow recession constant
    cg = processed_parameters[:, 18]  # Groundwater storage recession constant
    lag = processed_parameters[:, 19]  # Lag time
    kk = processed_parameters[:, 20]  # Muskingum K parameter
    x = processed_parameters[:, 21]  # Muskingum X parameter
    mp = processed_parameters[:, 22]  # Number of Muskingum reaches
    qsp = processed_parameters[:, 23]  # Initial surface flow
    qip = processed_parameters[:, 24]  # Initial interflow
    qgp = processed_parameters[:, 25]  # Initial groundwater flow

    # Handle warmup period
    if warmup_length > 0:
        p_and_e_warmup = p_and_e[0:warmup_length, :, :]
        # Remove initial_states from kwargs for warmup period to avoid applying override during warmup
        warmup_kwargs = {
            k: v for k, v in kwargs.items() if k != "initial_states"
        }

        # 初始化LAG状态变量用于预热期
        warmup_lag_states = {}
        for basin_idx in range(num_basins):
            warmup_lag_states[f"qsig_initial_{basin_idx}"] = np.zeros(
                max(int(lag[basin_idx]), 3)
            )
            warmup_lag_states[f"qx_initial_{basin_idx}"] = np.zeros(
                int(mp[basin_idx]) + 1
            )
        warmup_kwargs["lag_initial_states"] = warmup_lag_states

        # 运行预热期,获取所有状态变量(包括LAG状态)
        warmup_result = xaj_slw(
            p_and_e_warmup,
            processed_parameters,
            warmup_length=0,
            return_state=True,
            normalized_params=False,
            **warmup_kwargs,
        )

        # 解析预热期结果
        if len(warmup_result) >= 12:
            (
                _,
                _,
                _,
                _,
                _,
                _,
                wu_final,
                wl_final,
                wd_final,
                s_final,
                fr_final,
                warmup_lag_final_states,
            ) = warmup_result[:12]
        else:
            # 如果没有LAG状态,使用默认值
            _, _, _, _, _, _, wu_final, wl_final, wd_final = warmup_result[:9]
            warmup_lag_final_states = {}
            for basin_idx in range(num_basins):
                warmup_lag_final_states[f"qsig_initial_{basin_idx}"] = (
                    np.zeros(max(int(lag[basin_idx]), 3))
                )
                warmup_lag_final_states[f"qx_initial_{basin_idx}"] = np.zeros(
                    int(mp[basin_idx]) + 1
                )

        # 获取s和fr的最终状态
        warmup_s_final = np.zeros(num_basins)
        warmup_fr_final = np.zeros(num_basins)
        for basin_idx in range(num_basins):
            # Handle both [time, basin] and [time, basin, feature] shapes.
            warmup_s_final[basin_idx] = np.asarray(s_final[-1, basin_idx]).item()
            warmup_fr_final[basin_idx] = np.asarray(fr_final[-1, basin_idx]).item()

        # 使用预热期结果作为初始条件
        wu0 = wu_final[-1, :, 0]  # [basin]
        wl0 = wl_final[-1, :, 0]  # [basin]
        wd0 = wd_final[-1, :, 0]  # [basin]
        s0 = warmup_s_final  # [basin]
        fr0 = warmup_fr_final  # [basin]
        kwargs["lag_initial_states"] = warmup_lag_final_states

    else:
        # 默认状态从参数中获取
        wu0 = wup.copy()  # 上层张力水含量
        wl0 = wlp.copy()  # 下层张力水含量
        wd0 = wdp.copy()  # 深层张力水含量
        s0 = sp.copy()  # 自由水蓄量
        fr0 = frp.copy()  # 产流面积系数

    # Apply initial state overrides if provided (only after warmup in main call)
    initial_states = kwargs.get("initial_states", None)
    if initial_states is not None:
        # SMS states - 标量状态
        if "wu0" in initial_states:
            wu0[0] = float(initial_states["wu0"])
        if "wl0" in initial_states:
            wl0[0] = float(initial_states["wl0"])
        if "wd0" in initial_states:
            wd0[0] = float(initial_states["wd0"])
        if "s0" in initial_states:
            s0[0] = float(initial_states["s0"])
        if "fr0" in initial_states:
            fr0[0] = float(initial_states["fr0"])

        # LAG states - 数组状态
        if "qsig" in initial_states:
            qsig_value = initial_states["qsig"]
            # 检查是否为数组类型(避免对标量调用 len())
            if hasattr(qsig_value, '__len__') and not isinstance(qsig_value, (str, float, int)):
                qsig_len = max(int(lag[0]), 6)
                if len(qsig_value) >= qsig_len:
                    kwargs["lag_initial_states"] = {
                        "qsig_initial_0": qsig_value[:qsig_len].copy()
                    }

        if "qx" in initial_states:
            qx_value = initial_states["qx"]
            # 检查是否为数组类型(避免对标量调用 len())
            if hasattr(qx_value, '__len__') and not isinstance(qx_value, (str, float, int)):
                qx_len = int(mp[0]) + 1
                if len(qx_value) >= qx_len:
                    if "lag_initial_states" not in kwargs:
                        kwargs["lag_initial_states"] = {}
                    kwargs["lag_initial_states"]["qx_initial_0"] = qx_value[:qx_len].copy()

    # Save warmup states before applying overrides (for return_warmup_states)
    warmup_states = None
    if return_warmup_states:
        warmup_states = {
            # SMS states (scalar)
            "wu0": float(wu0[0]),
            "wl0": float(wl0[0]),
            "wd0": float(wd0[0]),
            "s0": float(s0[0]),
            "fr0": float(fr0[0]),
            # LAG states (array)
            "qsig": warmup_lag_final_states["qsig_initial_0"],  # 保持数组形式
            "qx": warmup_lag_final_states["qx_initial_0"],
        }

    inputs = p_and_e[warmup_length:, :, :]
    actual_time_steps = inputs.shape[0]

    # Initialize output arrays
    q_sim = np.zeros((actual_time_steps, num_basins))
    runoff_sim = np.zeros((actual_time_steps, num_basins))
    rs_out = np.zeros((actual_time_steps, num_basins))
    ri_out = np.zeros((actual_time_steps, num_basins))
    rg_out = np.zeros((actual_time_steps, num_basins))
    pe_out = np.zeros((actual_time_steps, num_basins))
    wu_out = np.zeros((actual_time_steps, num_basins))
    wl_out = np.zeros((actual_time_steps, num_basins))
    wd_out = np.zeros((actual_time_steps, num_basins))
    s_out = np.zeros((actual_time_steps, num_basins))
    fr_out = np.zeros((actual_time_steps, num_basins))

    # Initialize LAG final states dictionary
    final_lag_states = {}

    # Process each basin
    for basin_idx in range(num_basins):
        # Extract time series for this basin
        prcp = inputs[:, basin_idx, 0]
        pet = inputs[:, basin_idx, 1]

        # Calculate net precipitation
        pe = prcp - pet

        # Initial states for this basin
        wu_init = np.array([wu0[basin_idx]])
        wl_init = np.array([wl0[basin_idx]])
        wd_init = np.array([wd0[basin_idx]])
        s_init = np.array([s0[basin_idx]])
        fr_init = np.array([fr0[basin_idx]])

        # === 诊断日志:SMS3 调用前 ===
        logger.info(f"=== Basin {basin_idx}: 调用 SMS3 产流计算 ===")
        logger.info(f"  prcp 形状: {prcp.shape}, 类型: {type(prcp)}")
        logger.info(f"  pet 形状: {pet.shape}, 类型: {type(pet)}")
        logger.info(f"  pe 形状: {pe.shape}, 类型: {type(pe)}")
        logger.info(f"  初始状态: wu_init={wu_init}, wl_init={wl_init}, wd_init={wd_init}")
        logger.info(f"  s_init={s_init}, fr_init={fr_init}")

        # Run SMS3 runoff generation
        (
            wu_new,
            wl_new,
            wd_new,
            s_new,
            fr_new,
            rs_basin,
            ri_basin,
            rg_basin,
            runoff_basin,
        ) = sms3_runoff_generation_vectorized(
            prcp,
            pet,
            wu_init,
            wl_init,
            wd_init,
            s_init,
            fr_init,
            wm[basin_idx],
            wumx[basin_idx],
            wlmx[basin_idx],
            kc[basin_idx],
            b[basin_idx],
            c[basin_idx],
            im[basin_idx],
            sm[basin_idx],
            ex[basin_idx],
            kg[basin_idx],
            ki[basin_idx],
            time_interval,
            actual_time_steps,
        )

        # 诊断日志:检查 SMS3 输出
        if len(rs_basin) == 0:
            logger.warning(
                f"Basin {basin_idx}: SMS3 returned empty array! "
                f"actual_time_steps={actual_time_steps}, "
                f"rs_basin.shape={rs_basin.shape}"
            )

        # === 诊断日志:LAG3 调用前 ===
        logger.info(f"=== Basin {basin_idx}: 调用 LAG3 汇流计算 ===")
        logger.info(f"  rs_basin 形状: {rs_basin.shape}, 类型: {type(rs_basin)}")
        logger.info(f"  ri_basin 形状: {ri_basin.shape}, 类型: {type(ri_basin)}")
        logger.info(f"  rg_basin 形状: {rg_basin.shape}, 类型: {type(rg_basin)}")
        logger.info(f"  LAG3 参数: lag={lag[basin_idx]}, cs={cs[basin_idx]}, ci={ci[basin_idx]}, cg={cg[basin_idx]}")
        logger.info(f"  basin_area={basin_area}, time_interval={time_interval}")

        # Run LAG3 routing
        lag_initial_states = kwargs.get("lag_initial_states", None)
        if lag_initial_states is not None:
            qsig_initial = lag_initial_states.get(
                f"qsig_initial_{basin_idx}",
                np.zeros(max(int(lag[basin_idx]), 3)),
            )
            qx_initial = lag_initial_states.get(
                f"qx_initial_{basin_idx}", np.zeros(int(mp[basin_idx]) + 1)
            )
        else:
            qsig_initial = np.zeros(max(int(lag[basin_idx]), 3))
            qx_initial = np.zeros(int(mp[basin_idx]) + 1)

        # Run LAG3 routing and collect final states
        q_basin, lag_final_states = lag3_routing_vectorized(
            rs_basin,
            ri_basin,
            rg_basin,
            time_interval,
            basin_area,
            ci[basin_idx],
            cg[basin_idx],
            lag[basin_idx],
            cs[basin_idx],
            kk[basin_idx],
            x[basin_idx],
            int(mp[basin_idx]),
            qsp[basin_idx],
            qip[basin_idx],
            qgp[basin_idx],
            qsig_initial,
            qx_initial,
            return_states=True,
        )

        # Store LAG final states for this basin
        final_lag_states[f"qsig_initial_{basin_idx}"] = lag_final_states[
            "qsig_final"
        ]
        final_lag_states[f"qx_initial_{basin_idx}"] = lag_final_states[
            "qx_final"
        ]

        # Store results
        q_sim[:, basin_idx] = q_basin
        runoff_sim[:, basin_idx] = runoff_basin
        rs_out[:, basin_idx] = rs_basin
        ri_out[:, basin_idx] = ri_basin
        rg_out[:, basin_idx] = rg_basin
        pe_out[:, basin_idx] = pe
        wu_out[:, basin_idx] = wu_new
        wl_out[:, basin_idx] = wl_new
        wd_out[:, basin_idx] = wd_new
        s_out[:, basin_idx] = s_new
        fr_out[:, basin_idx] = fr_new

    # Ensure non-negative discharge
    q_sim = np.maximum(q_sim, 0.0)

    # Format outputs to match DHF interface: [seq, batch, feature]
    q_sim = np.expand_dims(q_sim, axis=2)
    runoff_sim = np.expand_dims(runoff_sim, axis=2)
    rs_out = np.expand_dims(rs_out, axis=2)
    ri_out = np.expand_dims(ri_out, axis=2)
    rg_out = np.expand_dims(rg_out, axis=2)
    pe_out = np.expand_dims(pe_out, axis=2)
    wu_out = np.expand_dims(wu_out, axis=2)
    wl_out = np.expand_dims(wl_out, axis=2)
    wd_out = np.expand_dims(wd_out, axis=2)
    s_out = np.expand_dims(s_out, axis=2)
    fr_out = np.expand_dims(fr_out, axis=2)

    if return_state:
        result = (
            q_sim,
            runoff_sim,
            rs_out,
            ri_out,
            rg_out,
            pe_out,
            wu_out,
            wl_out,
            wd_out,
            s_out,
            fr_out,
            final_lag_states,  # 返回最终的LAG状态
        )
        if return_warmup_states and warmup_states is not None:
            return result + (warmup_states,)
        else:
            return result
    else:
        if return_warmup_states and warmup_states is not None:
            return q_sim, warmup_states
        else:
            return q_sim