Source code for pywatershed.hydrology.prms_soilzone_ag_obs_et

"""PRMS Soilzone with agricultural area and iterative AET matching.

This module implements PRMSSoilzoneAg, a soil zone process that extends the
base PRMS soilzone with:

1. Dual area treatment (pervious and agricultural areas with separate
   accounting)
2. Iterative AET matching using observed AET data
3. Agricultural-specific parameters and irrigation estimation

Fortran Source Reference
------------------------
Based on GSFLOW 2.4.0 (currently unreleased) soilzone_ag.f90 with the following
simplifications:

**Implemented:**
- Dual soil moisture accounting (pervious and agricultural areas)
- Iterative loop to match observed AET via irrigation adjustment
- Preferential flow reservoir (same as base soilzone)
- Slow (gravity) reservoir with interflow and groundwater flow
- Agricultural-specific soil properties and cover types
- Transpiration and evaporation from soil zones

**Neglected functionality:**
- GLACIER HRU type logic
- GSFLOW_flag==ACTIVE logic (gravity reservoir interaction with MODFLOW)
- Cascade_flag > CASCADE_OFF logic (cascading flow between HRUs)
- compute_lateral==OFF logic (swales with lateral flow)
- Basin-wide aggregation variables (Basin_*)
- MODSIM_PRMS and MODSIM_PRMS_LOOSE model integration
- Frozen HRU logic (CFGI - Conditional Frozen Ground Index)

**Main Fortran Cross-References in code:**
- Main loop: szrun_ag() lines ~428-1219
- Iteration control: DO WHILE (keep_iterating==ACTIVE) lines ~442-1187
- HRU loop: DO k = 1, Active_hrus lines ~552-1178
- Pervious soil moisture: compute_soilmoist() lines ~752-758
- Agricultural soil moisture: compute_soilmoist() lines ~760-767
- Agricultural ET: compute_szactet() lines ~990-1005
- Pervious ET: compute_szactet() lines ~1011-1017
- Irrigation estimation: lines ~1127-1160
- Convergence check: lines ~1180-1187

Implementation Notes
--------------------
- The iteration loop adjusts `ag_irrigation_add` to minimize the difference
  between computed agricultural ET and observed AET
- Convergence criteria: unsatisfied_ag_et <= soilzone_aet_converge
- Maximum iterations: max_soilzone_ag_iter (default 10)
- After 20 iterations, doubles the irrigation increment to speed convergence
- Only iterates when iter_aet_flag=True and transp_on=True
- Agricultural area fraction (ag_frac) can be zero for non-agricultural HRUs

Iteration Diagnostics
---------------------
The following diagnostic variables track iteration convergence per HRU:

- `iter_count`: Number of iterations performed (same for all HRUs per timestep)
- `iter_end_status`: Convergence reason code per HRU:
  * -1 = Not an agricultural HRU or no iteration needed
  *  0 = Converged (unsatisfied_ag_et <= soilzone_aet_converge)
  *  1 = Soil deficit too small (limited by ag_soilwater_deficit_min)
  *  2 = Max iterations reached (non-convergence)
  *  3 = No transpiration active
  *  4 = iter_aet_flag disabled

These diagnostics help identify problematic HRUs, understand convergence
patterns, and validate irrigation requirements across the model domain.

"""

import pathlib as pl
from typing import Literal
from warnings import warn

import numba as nb
import numpy as np

from ..base.adapter import adaptable
from ..base.conservative_process import ConservativeProcess
from ..base.control import Control
from ..constants import (
    HruType,
    nan,
    numba_num_threads,
    one,
    zero,
)
from ..parameters import Parameters

ONETHIRD = 1 / 3
TWOTHIRDS = 2 / 3


[docs] class PRMSSoilzoneAgObsET(ConservativeProcess): """PRMS soil zone with agricultural area and iterative AET matching. This is an agricultural variant of PRMSSoilzone based on GSFLOW 2.4.0 soilzone_ag.f90. The key differences from the base PRMSSoilzone are: 1. **Dual Area Treatment**: Each HRU is divided into pervious and agricultural/irrigated areas, each with separate soil moisture accounting. 2. **Iterative AET Matching**: When observed actual ET (AET_external) is provided, the code iteratively adjusts irrigation to match the observed AET within a convergence tolerance. 3. **Agricultural Parameters**: Additional parameters for agricultural soil properties, cover density, and irrigation thresholds. Implementation based on GSFLOW 2.4.0 soilzone_ag.f90 with theoretical documentation given in the PRMS-IV documentation: `Markstrom, S. L., Regan, R. S., Hay, L. E., Viger, R. J., Webb, R. M., Payn, R. A., & LaFontaine, J. H. (2015). PRMS-IV, the precipitation-runoff modeling system, version 4. US Geological Survey Techniques and Methods, 6, B7. <https://pubs.usgs.gov/tm/6b7/pdf/tm6-b7.pdf>`__ Args: control: a Control object discretization: a discretization of class Parameters parameters: a parameter object of class Parameters dprst_evap_hru: Evaporation from surface-depression storage for each HRU dprst_seep_hru: Seepage from surface-depression storage to associated GWR for each HRU hru_impervevap: HRU area-weighted average evaporation from impervious area for each HRU hru_intcpevap: HRU area-weighted average evaporation from the canopy for each HRU infil: Infiltration to the capillary and preferential-flow reservoirs, depth on pervious area infil_ag: Infiltration to the capillary reservoir for agricultural area, depth on HRU agricultural area sroff: Surface runoff to the stream network for each HRU sroff_vol: Surface runoff volume to the stream network for each HRU potet: Potential ET for each HRU transp_on: Flag indicating whether transpiration is occurring (0=no;1=yes) snow_evap: Evaporation and sublimation from snowpack on each HRU snowcov_area: Snow-covered area on each HRU prior to melt and sublimation unless snowpack ag_frac: Fraction of HRU that is agricultural/irrigated area aet_observed: Observed actual ET from CBH file for each HRU (when iter_aet_flag is True). Used to calculate AET_external. Negative values are considered missing and set to zero, diabling AET matching. dprst_flag: use depression storage or not? None uses value in control file, which otherwise defaults to True. iter_aet_flag: Flag to enable iterative AET matching. If None, uses value from control.options["iter_aet_flag"] if available, otherwise defaults to False. imbalance_behavior: one of ["defer", None, "warn", "error"] with "defer" being the default and defering to control.options["imbalance_behavior"] when available. When control.options["imbalance_behavior"] is not avaiable, imbalance_behavior is set to "warn". calc_method: one of ["numpy", "numba"]. None defaults to "numpy". The "numba" option provides significant performance improvements, especially for the iterative AET matching loop. Parallelism is controlled via the NUMBA_NUM_THREADS environment variable. adjust_parameters: one of ["warn", "error", "no"]. Default is "warn", the code edits the parameters and issues a warning. If "error" is selected the the code issues warnings about all edited parameters before raising the error to give you information. If "no" is selected then no parameters are adjusted and there will be no warnings or errors. verbose: Print extra information or not? restart_read: May be boolean or a Pathlib.Path. If False, control.options will be examined for this key. If True, the working directory is searched for restart files. If a Pathlib.Path, this specifies an alternative directory to search for restart files. restart_write: As for restart_read but for writing. The directory in either case will be attempted to be created if it does not exist. restart_write_freq: If False, then control.options is examined for this key. The follwing values set the frequency of restart output with "y" for yearly, "m" for monthly, "d" for daily, or "f" for final. """
[docs] def __init__( self, control: Control, discretization: Parameters, parameters: Parameters, dprst_evap_hru: adaptable, dprst_seep_hru: adaptable, hru_impervevap: adaptable, hru_intcpevap: adaptable, infil: adaptable, infil_ag: adaptable, sroff: adaptable, sroff_vol: adaptable, potet: adaptable, transp_on: adaptable, snow_evap: adaptable, snowcov_area: adaptable, ag_frac: adaptable, aet_observed: adaptable, dprst_flag: bool | None = None, imbalance_behavior: Literal["defer", None, "warn", "error"] = "defer", calc_method: Literal["numpy", None] = None, adjust_parameters: Literal["warn", "error", "no"] = "warn", input_aliases: dict = None, verbose: bool | None = None, restart_read: pl.Path | bool = False, restart_write: pl.Path | bool = False, restart_write_freq: Literal["y", "m", "d", "f", False] = False, ): super().__init__( control=control, discretization=discretization, parameters=parameters, input_aliases=input_aliases, restart_read=restart_read, restart_write=restart_write, restart_write_freq=restart_write_freq, ) self.name = "PRMSSoilzoneAgObsET" self._set_inputs(locals()) self._set_options(locals()) self._iter_aet_flag = True if type(self) is PRMSSoilzoneAgObsET: if ( "iter_aet_flag" in self.control.options and not self.control.options["iter_aet_flag"] ): raise ValueError( "iter_aet_flag=False in the control file is inconsistent " "with PRMSSoilzoneAgObsET, which requires " "iter_aet_flag=True. Use PRMSSoilzoneAg if you do not " "need observed ET iteration." ) if self._dprst_flag is None: self._dprst_flag = True # Handle missing inputs when dprst_flag == False if not self._dprst_flag: raise ValueError("Not currently supporting non-dprst configs") # This uses options # if this uses inputs/dynamic parameters, they are not available until # advance, after model has solved inputs. self._set_budget() self._init_calc_method() return
[docs] @staticmethod def get_dimensions() -> tuple: return ("nhru",)
[docs] @staticmethod def get_parameters() -> tuple: return ( "dprst_frac", "cov_type", "fastcoef_lin", "fastcoef_sq", "hru_area", "hru_in_to_cf", "hru_percent_imperv", "hru_type", "pref_flow_den", "pref_flow_infil_frac", "sat_threshold", "slowcoef_lin", "slowcoef_sq", "soil_moist_max", "soil_moist_init_frac", "soil_rechr_init_frac", "soil_rechr_max_frac", "soil_type", "soil2gw_max", "ssr2gw_exp", "ssr2gw_rate", "ssstor_init_frac", # Agricultural parameters "ag_soil_type", "ag_soil_moist_max", "ag_soil_moist_init_frac", "ag_soil_rechr_max_frac", "ag_soil_rechr_init_frac", "ag_cov_type", "ag_covden_sum", "ag_covden_win", "ag_soil2gw_max", "ag_soilwater_deficit_min", "max_soilzone_ag_iter", "soilzone_aet_converge", )
[docs] @staticmethod def get_inputs() -> tuple: return ( "dprst_evap_hru", "dprst_seep_hru", "hru_impervevap", "hru_intcpevap", "infil", "infil_ag", "sroff", "sroff_vol", "potet", "transp_on", "snow_evap", "snowcov_area", "ag_frac", "aet_observed", )
[docs] @staticmethod def get_init_values() -> dict: return { # Mass budget input terms (whole HRU basis) "perv_infil_hru": zero, "ag_infil_hru": zero, "ag_irrigation_hru_source": zero, # Pervious area variables (whole HRU basis) "cap_infil_tot": zero, "cap_waterin": zero, "dunnian_flow": zero, "hru_actet": zero, "perv_actet": zero, "perv_actet_hru": zero, "potet_lower": zero, "potet_rechr": zero, "pref_flow": zero, "pref_flow_in": zero, "pref_flow_infil": zero, "pref_flow_max": zero, "pref_flow_stor": zero, "pref_flow_stor_change": zero, "pref_flow_stor_prev": nan, "pref_flow_thrsh": zero, "recharge": zero, "slow_flow": zero, "slow_stor": zero, "slow_stor_change": zero, "slow_stor_prev": nan, "soil_lower": zero, "soil_lower_change": zero, "soil_lower_change_hru": zero, "soil_lower_prev": zero, "soil_lower_ratio": zero, "soil_lower_max": nan, "soil_moist": nan, "soil_moist_tot": nan, "soil_rechr": zero, "soil_rechr_change": zero, "soil_rechr_change_hru": zero, "soil_rechr_prev": zero, "soil_saturated": zero, "soil_to_gw": zero, "soil_to_ssr": zero, "soil_zone_max": nan, "ssr_to_gw": zero, "ssres_flow": zero, "ssres_flow_vol": nan, "ssres_in": zero, "ssres_stor": nan, "swale_actet": zero, "unused_potet": zero, "perv_soil_to_gw": zero, "perv_soil_to_gvr": zero, # Agricultural area variables (whole HRU basis) "ag_cap_infil_tot": zero, "ag_soil_moist": zero, "ag_soil_moist_prev": zero, "ag_soil_moist_change": zero, "ag_soil_moist_change_hru": zero, "ag_soil_rechr": zero, "ag_soil_rechr_prev": zero, "ag_soil_rechr_change": zero, "ag_soil_rechr_change_hru": zero, "ag_soil_rechr_max": nan, "ag_soil_lower": nan, "ag_soil_lower_change": zero, "ag_soil_lower_change_hru": zero, "ag_soil_lower_stor_max": nan, "ag_actet": zero, "hru_ag_actet": zero, "ag_potet_rechr": zero, "ag_potet_lower": zero, "ag_soil_to_gw": zero, "ag_soil_to_gvr": zero, "ag_hortonian": zero, "ag_soil_saturated": zero, "unused_ag_et": zero, "ag_soilwater_deficit": zero, "ag_irrigation_add": zero, "ag_irrigation_add_vol": zero, "ag_aet_external_vol": zero, "AET_external": zero, # Redistribution tracking (for ag_frac changes) "ag_soil_moist_redistribution": zero, "ag_soil_rechr_redistribution": zero, "soil_rechr_redistribution": zero, "soil_lower_redistribution": zero, "slow_stor_redistribution": zero, # Iteration convergence diagnostics "iter_end_status": zero, "iter_count": zero, }
[docs] @staticmethod def get_restart_variables() -> list: return [ "soil_moist", "soil_rechr", "slow_stor", "ag_soil_moist", "ag_soil_rechr", "soil_lower", ]
[docs] @staticmethod def get_mass_budget_terms() -> dict: return { "inputs": [ "perv_infil_hru", "ag_infil_hru", "ag_irrigation_hru_source", ], "outputs": [ "perv_actet_hru", "hru_ag_actet", "perv_soil_to_gw", "ag_soil_to_gw", "ssr_to_gw", "slow_flow", "dunnian_flow", "pref_flow", ], "storage_changes": [ "soil_rechr_change_hru", "soil_lower_change_hru", "slow_stor_change", "pref_flow_stor_change", "ag_soil_rechr_change_hru", "ag_soil_lower_change_hru", ], }
def _set_initial_conditions(self): # Called in super before options are set pass def _init_diagnostic_vars(self) -> None: return def _initialize_soilzone_ag_data(self): """Initialize soilzone_ag data structures. Fortran reference: szinit_ag() and initialization in szdecl_ag() """ # self.ag_cap_infil_tot = self.hru_percent_imperv * np.nan # Derived parameters for impervious and pervious areas self.hru_area_imperv = self.hru_percent_imperv * self.hru_area self.hru_area_perv = self.hru_area - self.hru_area_imperv # Agricultural area self.ag_area = self.ag_frac * self.hru_area # Adjust pervious area to exclude agricultural area wh_active = np.where(self.hru_type != HruType.INACTIVE.value) if self._dprst_flag: dprst_area_max = self.dprst_frac * self.hru_area self.hru_area_perv[wh_active] = ( self.hru_area_perv[wh_active] - dprst_area_max[wh_active] - self.ag_area[wh_active] ) else: self.hru_area_perv[wh_active] = ( self.hru_area_perv[wh_active] - self.ag_area[wh_active] ) # Recompute pervious fraction self.hru_frac_perv = np.zeros(self.nhru) wh_active = np.where(self.hru_type != HruType.INACTIVE.value) self.hru_frac_perv[wh_active] = ( self.hru_area_perv[wh_active] / self.hru_area[wh_active] ) # Pervious soil parameters self.soil_rechr_max = self.soil_rechr_max_frac * self.soil_moist_max # Agricultural soil parameters self.ag_soil_rechr_max = ( self.ag_soil_rechr_max_frac * self.ag_soil_moist_max ) # self._snow_free = one - self.snowcov_area # Edit parameters for inactive/lake HRUs wh_inactive_or_lake = np.where( (self.hru_type == HruType.INACTIVE.value) | (self.hru_type == HruType.LAKE.value) ) self._sat_threshold = self.sat_threshold.copy() self._sat_threshold[wh_inactive_or_lake] = zero wh_not_land = np.where(self.hru_type != HruType.LAND.value) self._pref_flow_den = self.pref_flow_den.copy() self._pref_flow_den[wh_not_land] = zero # Validate pref_flow_infil_frac if (self.pref_flow_infil_frac.min() < zero).any() or ( self.pref_flow_infil_frac.max() > one ).any(): msg = ( "Values of pref_flow_infil_frac outside of [0,1]. " "If values are all -1, you must set pref_flow_infil_frac to " "pref_flow_den in the parameter file yourself." ) raise ValueError(msg) # Initialize soil moisture states if not self._restart_read: # Pervious area self.soil_moist[:] = ( self.soil_moist_init_frac * self.soil_moist_max ) self.soil_rechr[:] = ( self.soil_rechr_init_frac * self.soil_rechr_max ) # Agricultural area self.ag_soil_moist[:] = ( self.ag_soil_moist_init_frac * self.ag_soil_moist_max ) self.ag_soil_rechr[:] = ( self.ag_soil_rechr_init_frac * self.ag_soil_rechr_max ) # Initialize subsurface reservoir storage if not self._restart_read: self.ssres_stor = self.ssstor_init_frac * self._sat_threshold wh_inactive_or_lake = np.where( (self.hru_type == HruType.INACTIVE.value) | (self.hru_type == HruType.LAKE.value) ) self.ssres_stor[wh_inactive_or_lake] = zero del self.ssstor_init_frac # Ensure LAKE and INACTIVE HRUs have zero agricultural area wh_no_ag = np.where( (self.hru_type == HruType.LAKE.value) | (self.hru_type == HruType.INACTIVE.value) ) for vv in ["ag_frac", "ag_area"]: # GSFLOW sets the values on the mask to zero, but we will NOT # edit parameters. So this could be a point of divergence in # solution, though it will raise an error here. assert (self[vv][wh_no_ag] == zero).all() self.ag_soil_moist[wh_no_ag] = zero self.ag_soil_rechr[wh_no_ag] = zero # Calculate agricultural soil lower storage max for ihru in range(self.nhru): self.ag_soil_lower_stor_max[ihru] = ( self.ag_soil_moist_max[ihru] - self.ag_soil_rechr_max[ihru] ) # Parameter validation and adjustment throw_error = False mask = self.soil_moist_max < 1.0e-5 if mask.any() and self._adjust_parameters != "no": if self._adjust_parameters == "error": throw_error = True msg = ( "soil_moist_max < 1.0e-5, set to 1.0e-5 at indices: " f"{np.where(mask)[0]}" ) warn(msg, UserWarning) self.soil_moist_max[mask] = 1.0e-5 mask = self.ag_soil_moist_max < 1.0e-5 if mask.any() and self._adjust_parameters != "no": if self._adjust_parameters == "error": throw_error = True msg = ( "ag_soil_moist_max < 1.0e-5, set to 1.0e-5 at indices: " f"{np.where(mask)[0]}" ) warn(msg, UserWarning) self.ag_soil_moist_max[mask] = 1.0e-5 if throw_error: raise ValueError("Parameter adjustment errors, see warnings above") # Additional parameter validation similar to base soilzone mask = self.soil_rechr_max < 1.0e-5 if mask.any() and self._adjust_parameters != "no": if self._adjust_parameters == "error": throw_error = True msg = ( "soil_rechr_max < 1.0e-5, set to 1.0e-5 at indices: " f"{np.where(mask)[0]}" ) warn(msg, UserWarning) self.soil_rechr_max[mask] = 1.0e-5 mask = self.ag_soil_rechr_max < 1.0e-5 if mask.any() and self._adjust_parameters != "no": if self._adjust_parameters == "error": throw_error = True msg = ( "ag_soil_rechr_max < 1.0e-5, set to 1.0e-5 at indices: " f"{np.where(mask)[0]}" ) warn(msg, UserWarning) self.ag_soil_rechr_max[mask] = 1.0e-5 mask = self.soil_rechr_max > self.soil_moist_max if mask.any() and self._adjust_parameters != "no": if self._adjust_parameters == "error": throw_error = True msg = ( "soil_rechr_max > soil_moist_max, " "soil_rechr_max set to soil_moist_max at indices: " f"{np.where(mask)[0]}" ) warn(msg, UserWarning) self.soil_rechr_max = np.where( mask, self.soil_moist_max, self.soil_rechr_max ) mask = self.ag_soil_rechr_max > self.ag_soil_moist_max if mask.any() and self._adjust_parameters != "no": if self._adjust_parameters == "error": throw_error = True msg = ( "ag_soil_rechr_max > ag_soil_moist_max, " "ag_soil_rechr_max set to ag_soil_moist_max at indices: " f"{np.where(mask)[0]}" ) warn(msg, UserWarning) self.ag_soil_rechr_max = np.where( mask, self.ag_soil_moist_max, self.ag_soil_rechr_max ) mask = self.soil_rechr > self.soil_rechr_max if mask.any() and self._adjust_parameters != "no": if self._adjust_parameters == "error": throw_error = True msg = ( "soil_rechr_init > soil_rechr_max, " "setting soil_rechr_init to soil_rechr_max at indices: " f"{np.where(mask)[0]}" ) warn(msg, UserWarning) self.soil_rechr = np.where( mask, self.soil_rechr_max, self.soil_rechr ) mask = self.ag_soil_rechr > self.ag_soil_rechr_max if mask.any() and self._adjust_parameters != "no": if self._adjust_parameters == "error": throw_error = True msg = ( "ag_soil_rechr_init > ag_soil_rechr_max, " "setting ag_soil_rechr_init to ag_soil_rechr_max at indices: " f"{np.where(mask)[0]}" ) warn(msg, UserWarning) self.ag_soil_rechr = np.where( mask, self.ag_soil_rechr_max, self.ag_soil_rechr ) mask = self.soil_moist > self.soil_moist_max if mask.any() and self._adjust_parameters != "no": if self._adjust_parameters == "error": throw_error = True msg = ( "soil_moist_init > soil_moist_max, " "setting soil_moist to soil_moist max at indices: " f"{np.where(mask)[0]}" ) warn(msg, UserWarning) self.soil_moist = np.where( mask, self.soil_moist_max, self.soil_moist ) mask = self.ag_soil_moist > self.ag_soil_moist_max if mask.any() and self._adjust_parameters != "no": if self._adjust_parameters == "error": throw_error = True msg = ( "ag_soil_moist_init > ag_soil_moist_max, " "setting ag_soil_moist to ag_soil_moist max at indices: " f"{np.where(mask)[0]}" ) warn(msg, UserWarning) self.ag_soil_moist = np.where( mask, self.ag_soil_moist_max, self.ag_soil_moist ) mask = self.soil_rechr > self.soil_moist if mask.any() and self._adjust_parameters != "no": if self._adjust_parameters == "error": throw_error = True msg = ( "soil_rechr > soil_moist, " "setting soil_rechr to soil_moist at indices: " f"{np.where(mask)[0]}" ) warn(msg, UserWarning) self.soil_rechr = np.where(mask, self.soil_moist, self.soil_rechr) mask = self.ag_soil_rechr > self.ag_soil_moist if mask.any() and self._adjust_parameters != "no": if self._adjust_parameters == "error": throw_error = True msg = ( "ag_soil_rechr > ag_soil_moist, " "setting ag_soil_rechr to ag_soil_moist at indices: " f"{np.where(mask)[0]}" ) warn(msg, UserWarning) self.ag_soil_rechr = np.where( mask, self.ag_soil_moist, self.ag_soil_rechr ) mask = self.ssres_stor > self._sat_threshold if mask.any() and self._adjust_parameters != "no": if self._adjust_parameters == "error": throw_error = True msg = ( "ssres_stor > _sat_threshold, " "setting ssres_stor to _sat_threshold at indices: " f"{np.where(mask)[0]}" ) warn(msg, UserWarning) self.ssres_stor = np.where( mask, self._sat_threshold, self.ssres_stor ) if throw_error: raise ValueError( "Some parameter values were edited and an error was requested." " See warnings for additional details." ) # Set up preferential flow thresholds and max wh_swale = np.where(self.hru_type == HruType.SWALE.value) self.pref_flow_thrsh[wh_swale] = self._sat_threshold[wh_swale] wh_land = np.where(self.hru_type == HruType.LAND.value) self.pref_flow_thrsh[wh_land] = self._sat_threshold[wh_land] * ( one - self._pref_flow_den[wh_land] ) self.pref_flow_max[wh_land] = ( self._sat_threshold[wh_land] - self.pref_flow_thrsh[wh_land] ) # Initialize slow_stor and pref_flow_stor from ssres_stor if not self._restart_read: wh_land_or_swale = np.where( (self.hru_type == HruType.LAND.value) | (self.hru_type == HruType.SWALE.value) ) self.slow_stor[wh_land_or_swale] = np.minimum( self.ssres_stor[wh_land_or_swale], self.pref_flow_thrsh[wh_land_or_swale], ) self.pref_flow_stor[wh_land_or_swale] = ( self.ssres_stor[wh_land_or_swale] - self.slow_stor[wh_land_or_swale] ) # Set flags for preferential flow and soil to gw self._pref_flow_flag = (self._pref_flow_den > zero).any() self._soil2gw_flag = (self.soil2gw_max > zero).any() # Calculate derived soil zone variables self.soil_zone_max = ( self._sat_threshold + self.soil_moist_max * self.hru_frac_perv + self.ag_soil_moist_max * self.ag_frac ) self.soil_moist_tot = ( self.ssres_stor + self.soil_moist * self.hru_frac_perv + self.ag_soil_moist * self.ag_frac ) if not self._restart_read: self.soil_lower = self.soil_moist - self.soil_rechr self.soil_lower_max = self.soil_moist_max - self.soil_rechr_max wh_soil_lower_stor = np.where(self.soil_lower_max > zero) self.soil_lower_ratio[wh_soil_lower_stor] = ( self.soil_lower[wh_soil_lower_stor] / self.soil_lower_max[wh_soil_lower_stor] ) # Iteration counter for non-convergence tracking self._iter_nonconverge = 0 # Initialize _prev variables for timestep 0 (when not reading restart) if not self._restart_read: self.soil_rechr_prev[:] = self.soil_rechr self.soil_lower_prev[:] = self.soil_lower self.ag_soil_moist_prev[:] = self.ag_soil_moist self.ag_soil_rechr_prev[:] = self.ag_soil_rechr self.pref_flow_stor_prev[:] = self.pref_flow_stor self.slow_stor_prev[:] = self.slow_stor return def _init_calc_method(self): """Initialize calculation method.""" if self._calc_method is None: self._calc_method = "numpy" avail_methods = ["numpy", "numba"] if self._calc_method.lower() not in avail_methods: msg = ( f"Invalid calc_method={self._calc_method} for {self.name}. " f"Setting calc_method to 'numpy' for {self.name}" ) warn(msg) self._calc_method = "numpy" if self._calc_method.lower() == "numba": import numba as nb numba_msg = f"{self.name} jit compiling with numba " nb_parallel = (numba_num_threads is not None) and ( numba_num_threads > 1 ) if nb_parallel: numba_msg += f"and using {numba_num_threads} threads" else: numba_msg += "in sequential mode" print(numba_msg, flush=True) # Use static methods directly - they will be passed to calculate # functions which can be JIT compiled self._compute_soilmoist = nb.njit( PRMSSoilzoneAgObsET._compute_soilmoist ) self._compute_interflow = nb.njit( PRMSSoilzoneAgObsET._compute_interflow ) self._compute_gwflow = nb.njit(PRMSSoilzoneAgObsET._compute_gwflow) self._compute_szactet = nb.njit( PRMSSoilzoneAgObsET._compute_szactet ) self._calculate_soilzone_ag = nb.njit(parallel=nb_parallel)( self._calculate_numpy ) else: # Use numpy static methods directly self._compute_soilmoist = PRMSSoilzoneAgObsET._compute_soilmoist self._compute_interflow = PRMSSoilzoneAgObsET._compute_interflow self._compute_gwflow = PRMSSoilzoneAgObsET._compute_gwflow self._compute_szactet = PRMSSoilzoneAgObsET._compute_szactet self._calculate_soilzone_ag = self._calculate_numpy return def _advance_variables(self) -> None: """Save previous timestep values. Fortran reference: variables saved at start of szrun_ag() """ self.pref_flow_stor_prev[:] = self.pref_flow_stor self.soil_rechr_prev[:] = self.soil_rechr self.soil_lower_prev[:] = self.soil_lower self.slow_stor_prev[:] = self.slow_stor # Agricultural soil moisture previous values self.ag_soil_moist_prev[:] = self.ag_soil_moist self.ag_soil_rechr_prev[:] = self.ag_soil_rechr return def _update_ag_areas(self): """Update ag_area and related quantities when ag_frac changes. This method recalculates derived areas that depend on ag_frac, which may change dynamically via an AdapterDynamicParameter. When ag_frac changes, the behavior follows Fortran logic in dynamic_soil_param_read.f90 lines 503-528: - If ag_frac DECREASES: keep ag_soil_moist unchanged, send excess water volume to slow_stor (GVR storage) - If ag_frac INCREASES: scale down ag_soil_moist to conserve depth (ag_soil_moist = ag_soil_moist * old_frac / new_frac) - If ag_frac goes to ZERO: transfer water to slow_stor Fortran reference: dynamic_soil_param_read.f90 """ # Store old values for scaling calculations old_ag_frac = ( self.ag_area / self.hru_area ) # old ag_frac from old ag_area old_hru_area_perv = self.hru_area_perv.copy() # Recalculate agricultural area new_ag_area = self.ag_frac * self.hru_area # Recalculate pervious area to exclude agricultural area new_hru_area_perv = self.hru_area - self.hru_area_imperv wh_active = np.where(self.hru_type != HruType.INACTIVE.value) if self._dprst_flag: dprst_area_max = self.dprst_frac * self.hru_area new_hru_area_perv[wh_active] = ( new_hru_area_perv[wh_active] - dprst_area_max[wh_active] - new_ag_area[wh_active] ) else: new_hru_area_perv[wh_active] = ( new_hru_area_perv[wh_active] - new_ag_area[wh_active] ) # Track water to transfer to slow_stor to_slow_stor = np.zeros(self.nhru) # Handle ag_frac changes following Fortran logic # Use np.isclose to avoid false positives from floating-point noise for ihru in range(self.nhru): if np.isclose(self.ag_frac[ihru], old_ag_frac[ihru]): # No change continue if self.ag_soil_moist[ihru] > 0.0: if self.ag_frac[ihru] > 0.0: if self.ag_frac[ihru] < old_ag_frac[ihru]: # ag_frac DECREASED: keep ag_soil_moist unchanged, # send excess water volume to slow_stor # Fortran: to_slow_stor = # Ag_soil_moist(i) * (Ag_frac(i) - frac_ag) excess_volume = self.ag_soil_moist[ihru] * ( old_ag_frac[ihru] - self.ag_frac[ihru] ) to_slow_stor[ihru] = excess_volume # ag_soil_moist and ag_soil_rechr remain unchanged elif old_ag_frac[ihru] < self.ag_frac[ihru]: # ag_frac INCREASED: scale down ag_soil_moist # Fortran: Ag_soil_moist(i) = # Ag_soil_moist(i)*Ag_frac(i)/frac_ag scale = old_ag_frac[ihru] / self.ag_frac[ihru] self.ag_soil_moist[ihru] = ( self.ag_soil_moist[ihru] * scale ) self.ag_soil_rechr[ihru] = ( self.ag_soil_rechr[ihru] * scale ) else: # ag_frac went to ZERO: transfer all water to slow_stor # Fortran: tmp = Ag_soil_moist(i)*Ag_frac(i) to_slow_stor[ihru] = ( self.ag_soil_moist[ihru] * old_ag_frac[ihru] ) self.ag_soil_moist[ihru] = 0.0 self.ag_soil_rechr[ihru] = 0.0 # Add excess water to slow_stor (GVR storage) # This matches Fortran behavior of sending excess to gravity reservoir self.slow_stor[:] = self.slow_stor + to_slow_stor # Scale pervious soil moisture when pervious area changes # Fortran: Soil_moist(i) = Soil_moist(i)*tmp where # tmp = Hru_perv(i)/hruperv # Use np.isclose to avoid false positives from floating-point noise wh_perv_changed = np.where( (~np.isclose(old_hru_area_perv, new_hru_area_perv)) & (new_hru_area_perv > 0.0) ) if len(wh_perv_changed[0]) > 0: scale_perv = ( old_hru_area_perv[wh_perv_changed] / new_hru_area_perv[wh_perv_changed] ) self.soil_moist[wh_perv_changed] = ( self.soil_moist[wh_perv_changed] * scale_perv ) self.soil_rechr[wh_perv_changed] = ( self.soil_rechr[wh_perv_changed] * scale_perv ) # Note: soil_lower is NOT updated here - it will be recomputed # as soil_moist - soil_rechr in the main HRU loop # Handle case where pervious area goes to zero wh_perv_to_zero = np.where( (old_hru_area_perv > 0.0) & np.isclose(new_hru_area_perv, 0.0) ) if len(wh_perv_to_zero[0]) > 0: self.soil_moist[wh_perv_to_zero] = 0.0 self.soil_rechr[wh_perv_to_zero] = 0.0 # Note: soil_lower will be recomputed in the main HRU loop # Update the stored area values self.ag_area[:] = new_ag_area self.hru_area_perv[:] = new_hru_area_perv # Recompute pervious fraction self.hru_frac_perv[wh_active] = ( self.hru_area_perv[wh_active] / self.hru_area[wh_active] ) # Recalculate soil_zone_max since it depends on hru_frac_perv and # ag_frac self.soil_zone_max[:] = ( self._sat_threshold + self.soil_moist_max * self.hru_frac_perv + self.ag_soil_moist_max * self.ag_frac ) return def _calculate(self, simulation_time): """Main calculation routine with iterative AET matching. Fortran reference: szrun_ag() This method implements the iterative loop that adjusts irrigation to match observed AET when iter_aet_flag is True. """ if self.control._itime_step == 0: self._initialize_soilzone_ag_data() # Update ag_area and related quantities in case ag_frac changed self._update_ag_areas() # After ag_frac changes, recompute soil_lower from the scaled values # (Note: _update_ag_areas scales soil_moist and soil_rechr but doesn't # update soil_lower, so we need to do it here) self.soil_lower[:] = self.soil_moist - self.soil_rechr # Calculate how much water was redistributed due to ag_frac changes. # # MASS BUDGET APPROACH: # When ag_frac changes (e.g., on Jan 1 annually), water is # redistributed: # - Agricultural soil water may be transferred to slow_stor or scaled # - Pervious soil water is scaled to conserve volume over new area # These redistributions are area-accounting adjustments, not hydrologic # processes, so they must be excluded from mass budget calculations. # # To maintain correct mass balance while preserving the semantic # meaning of "_prev" variables (= truly previous timestep values), we: # 1. Keep _prev values as they were at end of previous timestep # 2. Track how much was redistributed in these _redistribution # variables # 3. Calculate _change = current - prev - redistribution # # This gives us: # - Correct _prev semantics (match Fortran postprocessed _prev values) # - Correct mass budget (_change excludes area redistributions) # # NOTE: The _change variables calculated this way will NOT match simple # Fortran postprocessing (current - previous) because Fortran includes # the redistributions. This is intentional - the mass budget validates # these variables, not external comparison. self.ag_soil_moist_redistribution[:] = ( self.ag_soil_moist - self.ag_soil_moist_prev ) self.ag_soil_rechr_redistribution[:] = ( self.ag_soil_rechr - self.ag_soil_rechr_prev ) self.soil_rechr_redistribution[:] = ( self.soil_rechr - self.soil_rechr_prev ) self.soil_lower_redistribution[:] = ( self.soil_lower - self.soil_lower_prev ) self.slow_stor_redistribution[:] = self.slow_stor - self.slow_stor_prev # Store initial values for iteration (Fortran: It0 variables) it0_soil_moist = self.soil_moist.copy() it0_soil_rechr = self.soil_rechr.copy() it0_ssres_stor = self.ssres_stor.copy() it0_slow_stor = self.slow_stor.copy() it0_ag_soil_moist = self.ag_soil_moist.copy() it0_ag_soil_rechr = self.ag_soil_rechr.copy() if self._pref_flow_flag: it0_pref_flow_stor = self.pref_flow_stor.copy() # Initialize iteration variables keep_iterating = True soil_iter = 1 # Initialize ObsET variables only when iter_aet_flag is True if self._iter_aet_flag: self.ag_irrigation_add[:] = zero self.ag_irrigation_add_vol[:] = zero self.ag_aet_external_vol[:] = zero # Compute and validate AET_external from observed values # (per climate_hru_debug.f90 lines 113-127) # Copy observed values to working variables self.AET_external[:] = self.aet_observed # Where AET < 0 (but not -1.0 missing value) for ag HRUs, set to 0 mask = ( (self.AET_external < 0.0) & (self.AET_external != -1.0) & (self.ag_frac > 0.0) ) if np.any(mask) and self._verbose: warn( f"AET_external < 0.0 for {mask.sum()} HRUs; setting to 0.0" ) self.AET_external[mask] = 0.0 # Use actual ObsET arrays aet_external_arr = self.AET_external ag_irrigation_add_arr = self.ag_irrigation_add else: # Create dummy zero arrays for non-iterative case aet_external_arr = np.zeros(self.nhru, dtype=float) ag_irrigation_add_arr = np.zeros(self.nhru, dtype=float) # Fortran: DO WHILE ( keep_iterating==ACTIVE ) while keep_iterating: # Restore initial conditions for iteration if soil_iter > 1: self.soil_moist[:] = it0_soil_moist self.soil_rechr[:] = it0_soil_rechr self.ssres_stor[:] = it0_ssres_stor self.slow_stor[:] = it0_slow_stor self.ag_soil_moist[:] = it0_ag_soil_moist self.ag_soil_rechr[:] = it0_ag_soil_rechr if self._pref_flow_flag: self.pref_flow_stor[:] = it0_pref_flow_stor result = self._calculate_soilzone_ag( soil_iter=soil_iter, iter_aet_flag=self._iter_aet_flag, pref_flow_flag=self._pref_flow_flag, snow_free=1.0 - self.snowcov_area, # Parameters max_soilzone_ag_iter=self.max_soilzone_ag_iter, cov_type=self.cov_type, ag_cov_type=self.ag_cov_type, fastcoef_lin=self.fastcoef_lin, fastcoef_sq=self.fastcoef_sq, hru_frac_perv=self.hru_frac_perv, hru_type=self.hru_type, ag_frac=self.ag_frac, ag_area=self.ag_area, hru_area=self.hru_area, hru_area_perv=self.hru_area_perv, pref_flow_den=self._pref_flow_den, pref_flow_infil_frac=self.pref_flow_infil_frac, sat_threshold=self._sat_threshold, slowcoef_lin=self.slowcoef_lin, slowcoef_sq=self.slowcoef_sq, soil_moist_max=self.soil_moist_max, soil_rechr_max=self.soil_rechr_max, soil_type=self.soil_type, ag_soil_type=self.ag_soil_type, ag_soil_moist_max=self.ag_soil_moist_max, ag_soil_rechr_max=self.ag_soil_rechr_max, soil2gw_max=self.soil2gw_max, ag_soil2gw_max=self.ag_soil2gw_max, ssr2gw_exp=self.ssr2gw_exp, ssr2gw_rate=self.ssr2gw_rate, ag_soilwater_deficit_min=self.ag_soilwater_deficit_min, soilzone_aet_converge=self.soilzone_aet_converge, # Inputs dprst_evap_hru=self.dprst_evap_hru, dprst_seep_hru=self.dprst_seep_hru, hru_impervevap=self.hru_impervevap, hru_intcpevap=self.hru_intcpevap, infil=self.infil, infil_ag=self.infil_ag, potet=self.potet, transp_on=self.transp_on, snow_evap=self.snow_evap, snowcov_area=self.snowcov_area, aet_external=aet_external_arr, ag_irrigation_add=ag_irrigation_add_arr, # State variables soil_moist=self.soil_moist, soil_rechr=self.soil_rechr, ag_soil_moist=self.ag_soil_moist, ag_soil_rechr=self.ag_soil_rechr, pref_flow_stor=self.pref_flow_stor, slow_stor=self.slow_stor, ssres_stor=self.ssres_stor, # Previous timestep values for storage changes pref_flow_stor_prev=self.pref_flow_stor_prev, soil_rechr_prev=self.soil_rechr_prev, soil_lower_prev=self.soil_lower_prev, slow_stor_prev=self.slow_stor_prev, ag_soil_moist_prev=self.ag_soil_moist_prev, ag_soil_rechr_prev=self.ag_soil_rechr_prev, # Output variables soil_to_gw=self.soil_to_gw, soil_to_ssr=self.soil_to_ssr, perv_soil_to_gw=self.perv_soil_to_gw, perv_soil_to_gvr=self.perv_soil_to_gvr, ag_soil_to_gw=self.ag_soil_to_gw, ag_soil_to_gvr=self.ag_soil_to_gvr, ssr_to_gw=self.ssr_to_gw, slow_flow=self.slow_flow, ssres_flow=self.ssres_flow, potet_rechr=self.potet_rechr, potet_lower=self.potet_lower, ag_potet_rechr=self.ag_potet_rechr, ag_potet_lower=self.ag_potet_lower, cap_waterin=self.cap_waterin, cap_infil_tot=self.cap_infil_tot, ag_cap_infil_tot=self.ag_cap_infil_tot, # ag_water_in=self.ag_water_in, pref_flow_in=self.pref_flow_in, pref_flow_infil=self.pref_flow_infil, pref_flow=self.pref_flow, perv_actet=self.perv_actet, perv_actet_hru=self.perv_actet_hru, ag_actet=self.ag_actet, hru_ag_actet=self.hru_ag_actet, hru_actet=self.hru_actet, soil_lower=self.soil_lower, ag_soil_lower=self.ag_soil_lower, dunnian_flow=self.dunnian_flow, pref_flow_max=self.pref_flow_max, pref_flow_thrsh=self.pref_flow_thrsh, soil_lower_max=self.soil_lower_max, ag_soil_lower_stor_max=self.ag_soil_lower_stor_max, soil_zone_max=self.soil_zone_max, soil_moist_tot=self.soil_moist_tot, ssres_in=self.ssres_in, recharge=self.recharge, unused_potet=self.unused_potet, unused_ag_et=self.unused_ag_et, ag_hortonian=self.ag_hortonian, ag_soil_saturated=self.ag_soil_saturated, ag_soilwater_deficit=self.ag_soilwater_deficit, swale_actet=self.swale_actet, soil_saturated=self.soil_saturated, sroff=self.sroff, soil_lower_ratio=self.soil_lower_ratio, pref_flow_stor_change=self.pref_flow_stor_change, soil_lower_change=self.soil_lower_change, soil_lower_change_hru=self.soil_lower_change_hru, soil_rechr_change=self.soil_rechr_change, soil_rechr_change_hru=self.soil_rechr_change_hru, slow_stor_change=self.slow_stor_change, ag_soil_moist_change=self.ag_soil_moist_change, ag_soil_moist_change_hru=self.ag_soil_moist_change_hru, ag_soil_rechr_change=self.ag_soil_rechr_change, ag_soil_rechr_change_hru=self.ag_soil_rechr_change_hru, ag_soil_lower_change=self.ag_soil_lower_change, ag_soil_lower_change_hru=self.ag_soil_lower_change_hru, ag_soil_moist_redistribution=self.ag_soil_moist_redistribution, ag_soil_rechr_redistribution=self.ag_soil_rechr_redistribution, soil_rechr_redistribution=self.soil_rechr_redistribution, soil_lower_redistribution=self.soil_lower_redistribution, slow_stor_redistribution=self.slow_stor_redistribution, # functions compute_soilmoist=self._compute_soilmoist, compute_szactet=self._compute_szactet, compute_interflow=self._compute_interflow, compute_gwflow=self._compute_gwflow, ) # Unpack results ( add_estimated_irrigation, num_hrus_ag_iter, unsatisfied_big, ) = result # Update keep_iterating based on result if not add_estimated_irrigation: keep_iterating = False # Check convergence soil_iter += 1 if not self._iter_aet_flag or not self.transp_on.any(): keep_iterating = False if soil_iter > self.max_soilzone_ag_iter: keep_iterating = False # Store final iteration count soil_iter -= 1 # Report convergence status if self._iter_aet_flag and self._verbose: if soil_iter == self.max_soilzone_ag_iter: self._iter_nonconverge += 1 msg = ( f"WARNING: ag AET did not converge at " f"{simulation_time.isoformat()}, " f"largest unsatisfied AET: {unsatisfied_big:.4f}, " f"iterations: {soil_iter}, " f"non-convergence count: {self._iter_nonconverge}" ) warn(msg, UserWarning) # Populate iteration convergence diagnostics # Only populate when iter_aet_flag is True if self._iter_aet_flag: # Store final iteration count (same for all HRUs at this timestep) self.iter_count[:] = soil_iter # Populate per-HRU convergence diagnostics # Note: unused_ag_et contains the final unsatisfied_ag_et per HRU # ag_soilwater_deficit already contains the final deficit values for ihru in range(self.nhru): # Determine convergence reason for each HRU # Convergence reason codes: # -1 = Not an agricultural HRU or no iteration needed # 0 = Converged (unsatisfied_ag_et <= soilzone_aet_converge) # 1 = Soil deficit too small (limited by # ag_soilwater_deficit_min) # 2 = Max iterations reached (non-convergence) # 3 = No transpiration active if self.ag_frac[ihru] <= 0.0: self.iter_end_status[ihru] = -1 elif not self.transp_on[ihru]: self.iter_end_status[ihru] = 3 elif self.unused_ag_et[ihru] <= self.soilzone_aet_converge: self.iter_end_status[ihru] = 0 # Converged elif ( self.ag_soilwater_deficit[ihru] <= self.ag_soilwater_deficit_min[ihru] ): self.iter_end_status[ihru] = 1 # Deficit limiting elif soil_iter >= self.max_soilzone_ag_iter: self.iter_end_status[ihru] = 2 # Max iterations else: # Necessary as an else? self.iter_end_status[ihru] = -9 # should never happen? # Calculate volume-based outputs self.sroff_vol[:] = self.sroff * self.hru_in_to_cf self.ssres_flow_vol[:] = self.ssres_flow * self.hru_in_to_cf # Calculate ObsET volume outputs only when iter_aet_flag is True if self._iter_aet_flag: self.ag_irrigation_add_vol[:] = ( self.ag_irrigation_add * self.ag_area ) self.ag_aet_external_vol[:] = self.ag_actet * self.ag_area # Calculate mass budget input terms (all on HRU basis) self.perv_infil_hru[:] = self.infil * self.hru_frac_perv self.ag_infil_hru[:] = self.infil_ag * self.ag_frac # Only calculate ag_irrigation_hru_source when iter_aet_flag is set # (PRMSSoilzoneAg doesn't have this variable in get_variables) if self._iter_aet_flag: self.ag_irrigation_hru_source[:] = ( self.ag_irrigation_add * self.ag_frac ) elif hasattr(self, "ag_irrigation_hru_source"): self.ag_irrigation_hru_source[:] = zero return @staticmethod def _compute_soilmoist( perv_frac, soil_moist_max, soil_rechr_max, soil2gw_max, infil, soil_moist, soil_rechr, soil_to_gw, soil_to_ssr, ): """Soil moisture accounting.""" # PRMSIV Step 4 (eqn 1-125) soil_rechr = min(soil_rechr + infil, soil_rechr_max) # PRMSIV Step 5 excess = soil_moist + infil soil_moist = min(excess, soil_moist_max) # PRMSIV Step 6 excess = (excess - soil_moist_max) * perv_frac if excess > 0.0: if soil2gw_max > 0.0: soil_to_gw = min(soil2gw_max, excess) excess = excess - soil_to_gw if excess > (infil * perv_frac): infil = 0.0 else: infil = infil - (excess / perv_frac) soil_to_ssr = max(0.0, excess) return ( infil, soil_moist, soil_rechr, soil_to_gw, soil_to_ssr, ) @staticmethod def _compute_interflow(coef_lin, coef_sq, ssres_in, storage, inter_flow): """Interflow computation.""" if (coef_lin <= 0.0) and (ssres_in <= 0.0): c1 = coef_sq * storage inter_flow = storage * (c1 / (1.0 + c1)) elif (coef_lin > 0.0) and (coef_sq <= 0.0): c2 = 1.0 - np.exp(-coef_lin) inter_flow = ssres_in * (1.0 - c2 / coef_lin) + storage * c2 elif coef_sq > 0.0: c3 = np.sqrt(coef_lin**2.0 + 4.0 * coef_sq * ssres_in) sos = storage - ((c3 - coef_lin) / (2.0 * coef_sq)) c1 = coef_sq * sos / c3 c2 = 1.0 - np.exp(-c3) if (1.0 + c1 * c2) > 0.0: inter_flow = ssres_in + ( (sos * (1.0 + c1) * c2) / (1.0 + c1 * c2) ) else: inter_flow = ssres_in else: inter_flow = 0.0 if inter_flow < 0.0: inter_flow = 0.0 elif inter_flow > storage: inter_flow = storage storage = storage - inter_flow return storage, inter_flow @staticmethod def _compute_gwflow(ssr2gw_rate, ssr2gw_exp, slow_stor): """Groundwater flow computation.""" ssr_to_gw = max(0.0, ssr2gw_rate * slow_stor**ssr2gw_exp) ssr_to_gw = min(ssr_to_gw, slow_stor) slow_stor = slow_stor - ssr_to_gw return ssr_to_gw, slow_stor @staticmethod def _compute_szactet( transp_on, cov_type, soil_type, soil_moist_max, soil_rechr_max, snow_free, soil_moist, soil_rechr, avail_potet, potet_rechr, potet_lower, ): """Actual ET from soil zone computation.""" NEARZERO = 1e-6 ONETHIRD = 1.0 / 3.0 TWOTHIRDS = 2.0 / 3.0 # Determine type of evapotranspiration if avail_potet < NEARZERO: et_type = 1 # ET_DEFAULT avail_potet = 0.0 elif not transp_on: if snow_free < 0.01: et_type = 1 # ET_DEFAULT else: et_type = 2 # EVAP_ONLY elif cov_type > 0: et_type = 3 # EVAP_PLUS_TRANSP elif snow_free < 0.01: et_type = 1 # ET_DEFAULT else: et_type = 2 # EVAP_ONLY # Compute ET based on type if et_type in [2, 3]: # EVAP_ONLY or EVAP_PLUS_TRANSP pcts = soil_moist / soil_moist_max pctr = soil_rechr / soil_rechr_max potet_lower = avail_potet potet_rechr = avail_potet if soil_type == 1: # SAND if pcts < 0.25: potet_lower = 0.5 * pcts * avail_potet if pctr < 0.25: potet_rechr = 0.5 * pctr * avail_potet elif soil_type == 2: # LOAM if pcts < 0.5: potet_lower = pcts * avail_potet if pctr < 0.5: potet_rechr = pctr * avail_potet elif soil_type == 3: # CLAY if (pcts < TWOTHIRDS) and (pcts > ONETHIRD): potet_lower = pcts * avail_potet elif pcts <= ONETHIRD: potet_lower = 0.5 * pcts * avail_potet if (pctr < TWOTHIRDS) and (pctr > ONETHIRD): potet_rechr = pctr * avail_potet elif pctr <= ONETHIRD: potet_rechr = 0.5 * pctr * avail_potet # Soil moisture accounting if et_type == 2: # EVAP_ONLY potet_rechr = potet_rechr * snow_free if potet_rechr > soil_rechr: potet_rechr = soil_rechr soil_rechr = 0.0 else: soil_rechr = soil_rechr - potet_rechr if (et_type == 2) or (potet_rechr >= potet_lower): # EVAP_ONLY if potet_rechr > soil_moist: potet_rechr = soil_moist soil_moist = 0.0 else: soil_moist = soil_moist - potet_rechr et = potet_rechr elif potet_lower > soil_moist: et = soil_moist soil_moist = 0.0 else: soil_moist = soil_moist - potet_lower et = potet_lower if soil_rechr > soil_moist: soil_rechr = soil_moist else: et = 0.0 return ( soil_moist, soil_rechr, avail_potet, potet_rechr, potet_lower, et, ) @staticmethod def _calculate_numpy( soil_iter, iter_aet_flag, pref_flow_flag, snow_free, max_soilzone_ag_iter, # Parameters cov_type, ag_cov_type, fastcoef_lin, fastcoef_sq, hru_frac_perv, hru_type, ag_frac, ag_area, hru_area, hru_area_perv, pref_flow_den, pref_flow_infil_frac, sat_threshold, slowcoef_lin, slowcoef_sq, soil_moist_max, soil_rechr_max, soil_type, ag_soil_type, ag_soil_moist_max, ag_soil_rechr_max, soil2gw_max, ag_soil2gw_max, ssr2gw_exp, ssr2gw_rate, ag_soilwater_deficit_min, soilzone_aet_converge, # Inputs dprst_evap_hru, dprst_seep_hru, hru_impervevap, hru_intcpevap, infil, infil_ag, potet, transp_on, snow_evap, snowcov_area, aet_external, ag_irrigation_add, # State variables soil_moist, soil_rechr, ag_soil_moist, ag_soil_rechr, pref_flow_stor, slow_stor, ssres_stor, # Previous timestep values for storage changes pref_flow_stor_prev, soil_rechr_prev, soil_lower_prev, slow_stor_prev, ag_soil_moist_prev, ag_soil_rechr_prev, # Output variables soil_to_gw, soil_to_ssr, perv_soil_to_gw, perv_soil_to_gvr, ag_soil_to_gw, ag_soil_to_gvr, ssr_to_gw, slow_flow, ssres_flow, potet_rechr, potet_lower, ag_potet_rechr, ag_potet_lower, cap_waterin, cap_infil_tot, ag_cap_infil_tot, pref_flow_in, pref_flow_infil, pref_flow, perv_actet, perv_actet_hru, ag_actet, hru_ag_actet, hru_actet, soil_lower, ag_soil_lower, dunnian_flow, pref_flow_max, pref_flow_thrsh, soil_lower_max, ag_soil_lower_stor_max, soil_zone_max, soil_moist_tot, ssres_in, recharge, unused_potet, unused_ag_et, ag_hortonian, ag_soil_saturated, ag_soilwater_deficit, swale_actet, soil_saturated, sroff, soil_lower_ratio, pref_flow_stor_change, soil_lower_change, soil_lower_change_hru, soil_rechr_change, soil_rechr_change_hru, slow_stor_change, ag_soil_moist_change, ag_soil_moist_change_hru, ag_soil_rechr_change, ag_soil_rechr_change_hru, ag_soil_lower_change, ag_soil_lower_change_hru, ag_soil_moist_redistribution, ag_soil_rechr_redistribution, soil_rechr_redistribution, soil_lower_redistribution, slow_stor_redistribution, compute_soilmoist, compute_szactet, compute_interflow, compute_gwflow, ): """Numba-optimized calculation for agricultural soilzone. This implements the soil water balance for both pervious and agricultural areas of each HRU with significant performance improvements through numba JIT compilation. """ nhru = len(hru_type) # Initialize outputs for this iteration add_estimated_irrigation = False num_hrus_ag_iter = 0 unsatisfied_big = 0.0 # Initialize per-timestep variables soil_to_gw[:] = 0.0 soil_to_ssr[:] = 0.0 perv_soil_to_gw[:] = 0.0 perv_soil_to_gvr[:] = 0.0 ag_soil_to_gw[:] = 0.0 ag_soil_to_gvr[:] = 0.0 ssr_to_gw[:] = 0.0 slow_flow[:] = 0.0 ssres_flow[:] = 0.0 potet_rechr[:] = 0.0 potet_lower[:] = 0.0 ag_potet_rechr[:] = 0.0 ag_potet_lower[:] = 0.0 ag_actet[:] = 0.0 hru_ag_actet[:] = 0.0 ag_soil_saturated[:] = 0.0 ag_hortonian[:] = 0.0 unused_ag_et[:] = 0.0 ag_soilwater_deficit[:] = 0.0 ag_cap_infil_tot[:] = 0.0 ag_soil_lower[:] = 0.0 swale_actet[:] = 0.0 soil_saturated[:] = 0.0 dunnian_flow[:] = 0.0 pref_flow_infil[:] = 0.0 pref_flow_in[:] = 0.0 pref_flow[:] = 0.0 # Arrays to track which HRUs need irrigation (for post-loop reduction) # Prevents race conditions when multiple threads try to update shared # variables hru_needs_irrigation = np.empty(nhru, dtype=np.int32) hru_needs_irrigation[:] = 0 hru_unsatisfied_max = np.empty(nhru, dtype=np.float64) hru_unsatisfied_max[:] = 0.0 # HRU loop - uses prange for parallel execution when # numba_num_threads > 1 for ihru in nb.prange(nhru): # Skip inactive HRUs if hru_type[ihru] == 0: # INACTIVE continue # Initial AET from impervious, interception, and snow hruactet = ( hru_impervevap[ihru] + hru_intcpevap[ihru] + snow_evap[ihru] ) hruactet += dprst_evap_hru[ihru] # Handle LAKE HRUs separately if hru_type[ihru] == 2: # LAKE unused_potet[ihru] = potet[ihru] - hruactet hru_actet[ihru] = hruactet continue # Get area fractions perv_frac = hru_frac_perv[ihru] agfrac = ag_frac[ihru] perv_area = hru_area_perv[ihru] agarea = ag_area[ihru] perv_on_flag = perv_area > 0.0 ag_on_flag = agarea > 0.0 # Available PET after impervious, interception, snow evap avail_potet = potet[ihru] - hruactet if avail_potet < 0.0: avail_potet = 0.0 hruactet = potet[ihru] # Initialize flow components dunnianflw = 0.0 dunnianflw_pfr = 0.0 # ****** Add infiltration to soil # Fortran: capwater_maxin = Infil(i) capwater_maxin = infil[ihru] ag_water_maxin = infil_ag[ihru] # Add irrigation if iterating (Fortran line ~591) if iter_aet_flag: ag_water_maxin = ag_water_maxin + ag_irrigation_add[ihru] # Compute preferential flow (if enabled) # Fortran: IF ( Pref_flag == ACTIVE ) THEN prefflow = 0.0 if pref_flow_flag: if pref_flow_infil_frac[ihru] > 0.0: ag_pref_flow_maxin = 0.0 cap_pref_flow_maxin = 0.0 if ag_water_maxin > 0.0: ag_pref_flow_maxin = ( ag_water_maxin * pref_flow_infil_frac[ihru] ) ag_water_maxin = ag_water_maxin - ag_pref_flow_maxin ag_pref_flow_maxin = ag_pref_flow_maxin * agfrac if capwater_maxin > 0.0: cap_pref_flow_maxin = ( capwater_maxin * pref_flow_infil_frac[ihru] ) capwater_maxin = capwater_maxin - cap_pref_flow_maxin cap_pref_flow_maxin = cap_pref_flow_maxin * perv_frac pref_flow_maxin = cap_pref_flow_maxin + ag_pref_flow_maxin # Add to preferential flow storage pref_flow_stor[ihru] = ( pref_flow_stor[ihru] + pref_flow_maxin ) dunnianflw_pfr = max( 0.0, pref_flow_stor[ihru] - pref_flow_max[ihru] ) if dunnianflw_pfr > 0.0: pref_flow_stor[ihru] = pref_flow_max[ihru] pref_flow_infil[ihru] = pref_flow_maxin - dunnianflw_pfr # Set cap_infil_tot and ag_cap_infil_tot BEFORE compute_soilmoist # (Fortran lines 737-744, before line 753) if perv_on_flag: cap_infil_tot[ihru] = capwater_maxin * perv_frac if ag_on_flag: ag_cap_infil_tot[ihru] = ag_water_maxin * agfrac # ****** Compute soil moisture for pervious area # Fortran: CALL compute_soilmoist (lines ~752-758) # Note: compute_soilmoist modifies infil (capwater_maxin) in # Fortran perv_soil_to_gvr[ihru] = 0.0 if perv_on_flag: if capwater_maxin + soil_moist[ihru] > 0.0: ( capwater_maxin, soil_moist[ihru], soil_rechr[ihru], perv_soil_to_gw[ihru], perv_soil_to_gvr[ihru], ) = compute_soilmoist( perv_frac, soil_moist_max[ihru], soil_rechr_max[ihru], soil2gw_max[ihru], capwater_maxin, soil_moist[ihru], soil_rechr[ihru], perv_soil_to_gw[ihru], perv_soil_to_gvr[ihru], ) # ****** Compute soil moisture for agricultural area # Fortran: CALL compute_soilmoist for ag (lines ~760-767) # Note: compute_soilmoist modifies infil (ag_water_maxin) in # Fortran if ag_on_flag: if ag_water_maxin + ag_soil_moist[ihru] > 0.0: ( ag_water_maxin, ag_soil_moist[ihru], ag_soil_rechr[ihru], ag_soil_to_gw[ihru], ag_soil_to_gvr[ihru], ) = compute_soilmoist( agfrac, ag_soil_moist_max[ihru], ag_soil_rechr_max[ihru], # NOTE: Fortran uses soil2gw_max here, not # ag_soil2gw_max (likely a bug in Fortran line 763) soil2gw_max[ihru], ag_water_maxin, ag_soil_moist[ihru], ag_soil_rechr[ihru], ag_soil_to_gw[ihru], ag_soil_to_gvr[ihru], ) # ag_water_in[ihru] = ag_water_maxin # Combine soil to gw and ssr # Fortran: Soil_to_gw(i) = perv_soil_to_gw(i) + ag_soil_to_gw(i) soil_to_gw[ihru] = perv_soil_to_gw[ihru] + ag_soil_to_gw[ihru] soil_to_ssr[ihru] = perv_soil_to_gvr[ihru] + ag_soil_to_gvr[ihru] cap_waterin[ihru] = capwater_maxin * perv_frac # ****** Compute slow interflow and ssr_to_gw # Fortran: compute_interflow and compute_gwflow (simplified, no # GSFLOW) availh2o = slow_stor[ihru] + soil_to_ssr[ihru] topfr = 0.0 # Compute for non-swale HRUs # Fortran: compute_lateral==ACTIVE if hru_type[ihru] != 3: # SWALE if pref_flow_flag: topfr = max(0.0, availh2o - pref_flow_thrsh[ihru]) ssresin = soil_to_ssr[ihru] - topfr slow_stor[ihru] = availh2o - topfr # Compute slow contribution to interflow if slow_stor[ihru] > 0.0: slow_stor[ihru], slow_flow[ihru] = compute_interflow( slowcoef_lin[ihru], slowcoef_sq[ihru], ssresin, slow_stor[ihru], slow_flow[ihru], ) else: # Swale - no lateral flow slow_stor[ihru] = availh2o # Compute flow to groundwater if slow_stor[ihru] > 0.0 and ssr2gw_rate[ihru] > 0.0: ssr_to_gw[ihru], slow_stor[ihru] = compute_gwflow( ssr2gw_rate[ihru], ssr2gw_exp[ihru], slow_stor[ihru], ) # Compute contribution to Dunnian flow from PFR # Fortran: IF ( Pref_flag==ACTIVE ) THEN (lines ~909-938) dunnianflw_gvr = 0.0 if pref_flow_flag: if pref_flow_max[ihru] > 0.0: availh2o = pref_flow_stor[ihru] + topfr dunnianflw_gvr = max(0.0, availh2o - pref_flow_max[ihru]) if dunnianflw_gvr > 0.0: topfr = topfr - dunnianflw_gvr if topfr < 0.0: topfr = 0.0 pref_flow_in[ihru] = pref_flow_infil[ihru] + topfr pref_flow_stor[ihru] = pref_flow_stor[ihru] + topfr if pref_flow_stor[ihru] > 0.0: pref_flow_stor[ihru], prefflow = compute_interflow( fastcoef_lin[ihru], fastcoef_sq[ihru], pref_flow_in[ihru], pref_flow_stor[ihru], prefflow, ) elif not (pref_flow_max[ihru] > 0.0): if hru_type[ihru] != 3: # SWALE dunnianflw_gvr = topfr # ****** Compute actual evapotranspiration # Fortran: compute_szactet (lines ~952-1023) pervactet = 0.0 agactet = 0.0 ag_hruactet = 0.0 unsatisfied_ag_et = 0.0 # Agricultural ET if ag_on_flag: if iter_aet_flag: # Use observed AET as target ag_AETtarget = aet_external[ihru] else: # Use PET as target ag_AETtarget = potet[ihru] # Subtract only canopy interception ET # Fortran assumes impervious, snow, and dprst evap not in ag # fraction # Fortran: ag_avail_targetAET = ag_AETtarget - Hru_intcpevap(i) ag_avail_targetAET = ag_AETtarget - hru_intcpevap[ihru] if ag_avail_targetAET < 0.0: ag_avail_targetAET = 0.0 if ag_avail_targetAET > 0.0: # Set ag_soil_saturated flag if ag soil moisture ratio > # 0.9999 # Fortran reference: compute_szactet line ~1448 # Must check BEFORE ET is subtracted from ag_soil_moist # Only set when Et_type > 1 (evap or transp active) # Fortran Et_type logic: # Et_type = 1 if Avail_potet < NEARZERO # Et_type = 1 if Transp_on==OFF and Snow_free < 0.01 # Et_type = 3 if Transp_on==ON and Cov_type > BARESOIL # Et_type = 1 if Transp_on==ON and Cov_type==BARESOIL and # Snow_free < 0.01 # Et_type = 2 if Transp_on==ON and Cov_type==BARESOIL and # Snow_free >= 0.01 # So Et_type > 1 when: (transp_on AND cov_type > 0) OR # snow_free >= 0.01 ag_et_type_gt_1 = ( transp_on[ihru] and ag_cov_type[ihru] > 0 ) or snow_free[ihru] >= 0.01 if ag_et_type_gt_1: ag_pcts = ag_soil_moist[ihru] / ag_soil_moist_max[ihru] if ag_pcts > 0.9999: ag_soil_saturated[ihru] = 1 # Call compute_szactet for ag area ( ag_soil_moist[ihru], ag_soil_rechr[ihru], _, ag_potet_rechr[ihru], ag_potet_lower[ihru], agactet, ) = compute_szactet( transp_on[ihru], ag_cov_type[ihru], ag_soil_type[ihru], ag_soil_moist_max[ihru], ag_soil_rechr_max[ihru], snow_free[ihru], ag_soil_moist[ihru], ag_soil_rechr[ihru], ag_avail_targetAET, ag_potet_rechr[ihru], ag_potet_lower[ihru], ) ag_hruactet = agactet * agfrac unsatisfied_ag_et = ag_avail_targetAET - agactet unused_ag_et[ihru] = unsatisfied_ag_et # Add back canopy interception (GSFLOW 2.4.1 behavior) # Fortran: Ag_actet(i) = agactet + Hru_intcpevap(i) ag_actet[ihru] = agactet + hru_intcpevap[ihru] # Pervious ET avail_potet = potet[ihru] - hruactet - ag_hruactet if avail_potet < 0.0: avail_potet = 0.0 if soil_moist[ihru] > 0.0 and avail_potet > 0.0: # Set soil_saturated flag if soil moisture ratio > 0.9999 # Fortran reference: compute_szactet line ~1448 # Must check BEFORE ET is subtracted from soil_moist # Only set when Et_type > 1 (evap or transp active) # Fortran Et_type logic: # Et_type = 1 if Avail_potet < NEARZERO # Et_type = 1 if Transp_on==OFF and Snow_free < 0.01 # Et_type = 3 if Transp_on==ON and Cov_type > BARESOIL # Et_type = 1 if Transp_on==ON and Cov_type==BARESOIL and # Snow_free < 0.01 # Et_type = 2 if Transp_on==ON and Cov_type==BARESOIL and # Snow_free >= 0.01 # So Et_type > 1 when: (transp_on AND cov_type > 0) OR # snow_free >= 0.01 # IF ( pcts>0.9999 ) Soil_saturated = 1 et_type_gt_1 = ( transp_on[ihru] and cov_type[ihru] > 0 ) or snow_free[ihru] >= 0.01 if et_type_gt_1: pcts = soil_moist[ihru] / soil_moist_max[ihru] if pcts > 0.9999: soil_saturated[ihru] = 1 ( soil_moist[ihru], soil_rechr[ihru], _, potet_rechr[ihru], potet_lower[ihru], pervactet, ) = compute_szactet( transp_on[ihru], cov_type[ihru], soil_type[ihru], soil_moist_max[ihru], soil_rechr_max[ihru], snow_free[ihru], soil_moist[ihru], soil_rechr[ihru], avail_potet, potet_rechr[ihru], potet_lower[ihru], ) # Combine ET components perv_actet_hru[ihru] = pervactet * perv_frac hru_ag_actet[ihru] = ag_hruactet hru_actet[ihru] = hruactet + perv_actet_hru[ihru] + ag_hruactet perv_actet[ihru] = pervactet # Soil moisture accounting # Fortran: Soil_lower(i) = Soil_moist(i) - Soil_rechr(i) soil_lower[ihru] = soil_moist[ihru] - soil_rechr[ihru] # Compute interflow and excess flow # Fortran: IF ( compute_lateral==ACTIVE ) THEN (lines ~1009-1048) if hru_type[ihru] != 3: # SWALE dunnianflw = dunnianflw_gvr + dunnianflw_pfr dunnian_flow[ihru] = dunnianflw # Treat pref_flow as interflow ssres_flow[ihru] = slow_flow[ihru] if pref_flow_flag: if pref_flow_max[ihru] > 0.0: pref_flow[ihru] = prefflow ssres_flow[ihru] = ssres_flow[ihru] + prefflow # Treat dunnianflw as surface runoff sroff[ihru] = sroff[ihru] + dunnian_flow[ihru] ssres_stor[ihru] = slow_stor[ihru] if pref_flow_flag: ssres_stor[ihru] = ssres_stor[ihru] + pref_flow_stor[ihru] else: # Swale HRU availh2o = slow_stor[ihru] - sat_threshold[ihru] swale_actet[ihru] = 0.0 if availh2o > 0.0: unsatisfied_et = potet[ihru] - hru_actet[ihru] if unsatisfied_et > 0.0: availh2o = min(availh2o, unsatisfied_et) swale_actet[ihru] = availh2o hru_actet[ihru] = hru_actet[ihru] + swale_actet[ihru] slow_stor[ihru] = slow_stor[ihru] - swale_actet[ihru] ssres_stor[ihru] = slow_stor[ihru] # Soil lower ratio if soil_lower_max[ihru] > 0.0: soil_lower_ratio[ihru] = ( soil_lower[ihru] / soil_lower_max[ihru] ) # Cap ratio at 1.0 if excess is due to rounding error (< 1e-4) # This handles accumulated floating-point differences between # Python (double precision) and Fortran (single precision) if soil_lower_ratio[ihru] > 1.0: excess = soil_lower_ratio[ihru] - 1.0 if excess < 1e-4: # Cap only small excesses due to floating-point error soil_lower_ratio[ihru] = 1.0 soil_lower[ihru] = soil_lower_max[ihru] # else: # # Fortran prints error but does NOT cap when excess # # >= 1e-4. This can happen in reanalysis settings # # with dynamic parameters (Fortran STOP is commented) # warnings.warn( # f"HRU {ihru}: soil_lower exceeds soil_lower_max " # f"by {excess:.2e} (ratio = " # f"{soil_lower_ratio[ihru]:.6f}). " # f"This may indicate a mass balance error.", # UserWarning, # ) ssres_in[ihru] = soil_to_ssr[ihru] if pref_flow_flag: ssres_in[ihru] = ssres_in[ihru] + pref_flow_infil[ihru] unused_potet[ihru] = potet[ihru] - hru_actet[ihru] # Total soil moisture soil_moist_tot[ihru] = ( ssres_stor[ihru] + soil_moist[ihru] * perv_frac + ag_soil_moist[ihru] * agfrac ) # Recharge recharge[ihru] = soil_to_gw[ihru] + ssr_to_gw[ihru] recharge[ihru] = recharge[ihru] + dprst_seep_hru[ihru] # Agricultural soil lower if ag_on_flag: ag_soil_lower[ihru] = ag_soil_moist[ihru] - ag_soil_rechr[ihru] # Check for irrigation need (Fortran lines ~1127-1160) if iter_aet_flag and transp_on[ihru]: if unsatisfied_ag_et > soilzone_aet_converge: ag_soilwater_deficit[ihru] = ( ag_soil_moist_max[ihru] - ag_soil_moist[ihru] ) / ag_soil_moist_max[ihru] if ( ag_soilwater_deficit[ihru] > ag_soilwater_deficit_min[ihru] ): # Only add irrigation if we'll iterate again if soil_iter < max_soilzone_ag_iter: unsatisfied_max = unsatisfied_ag_et if unsatisfied_ag_et > ag_soil_moist_max[ihru]: unsatisfied_max = ag_soil_moist_max[ihru] else: # Speed up convergence after 20 iterations if soil_iter > 20: unsatisfied_max = ( unsatisfied_max + unsatisfied_ag_et ) # Mark HRU needs irrigation hru_needs_irrigation[ihru] = 1 ag_irrigation_add[ihru] = ( ag_irrigation_add[ihru] + unsatisfied_max ) # Store for reduction after loop (thread-safe) hru_unsatisfied_max[ihru] = unsatisfied_max # End HRU loop # Compute iteration control variables after parallel loop (avoid race # conditions) add_estimated_irrigation = False num_hrus_ag_iter = 0 unsatisfied_big = 0.0 for ihru in range(nhru): if hru_needs_irrigation[ihru] > 0: add_estimated_irrigation = True num_hrus_ag_iter += 1 if hru_unsatisfied_max[ihru] > unsatisfied_big: unsatisfied_big = hru_unsatisfied_max[ihru] # Calculate storage changes for mass budget # Fortran reference: Not explicitly in szrun_ag, but needed for budget # # Subtract redistributions to get only hydrologic process changes. # This ensures mass balance is correct by excluding area-accounting # adjustments that occur when ag_frac changes dynamically. pref_flow_stor_change[:] = pref_flow_stor - pref_flow_stor_prev soil_lower_change[:] = ( soil_lower - soil_lower_prev - soil_lower_redistribution ) soil_rechr_change[:] = ( soil_rechr - soil_rechr_prev - soil_rechr_redistribution ) slow_stor_change[:] = ( slow_stor - slow_stor_prev - slow_stor_redistribution ) # Convert to HRU basis (multiply by area fractions) soil_lower_change_hru[:] = soil_lower_change * hru_frac_perv soil_rechr_change_hru[:] = soil_rechr_change * hru_frac_perv # Agricultural soil moisture storage changes # Subtract redistributions to get only hydrologic process changes ag_soil_moist_change[:] = ( ag_soil_moist - ag_soil_moist_prev - ag_soil_moist_redistribution ) ag_soil_rechr_change[:] = ( ag_soil_rechr - ag_soil_rechr_prev - ag_soil_rechr_redistribution ) ag_soil_lower_change[:] = ag_soil_moist_change - ag_soil_rechr_change # Convert to HRU basis (multiply by area fractions) ag_soil_moist_change_hru[:] = ag_soil_moist_change * ag_frac ag_soil_rechr_change_hru[:] = ag_soil_rechr_change * ag_frac ag_soil_lower_change_hru[:] = ag_soil_lower_change * ag_frac # Return iteration control variables return (add_estimated_irrigation, num_hrus_ag_iter, unsatisfied_big)