From fe8aa45dd84db0ea72185a214a143fcb88def5b2 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Fri, 10 Apr 2026 00:00:07 +0300 Subject: [PATCH 001/113] Add NVP control switches --- src/main/clm_varctl.F90 | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/main/clm_varctl.F90 b/src/main/clm_varctl.F90 index 83133acf2b..509c38374c 100644 --- a/src/main/clm_varctl.F90 +++ b/src/main/clm_varctl.F90 @@ -379,7 +379,23 @@ module clm_varctl logical, public :: use_fates_bgc = .false. ! true => use FATES along with CLM soil biogeochemistry !---------------------------------------------------------- - ! LUNA switches + ! [PORTED by Hui Tang: moss/lichen (nvp) control variables] + ! NVP (moss/lichen) switches + !---------------------------------------------------------- + + ! true => activate nvp model + logical, public :: use_nvp = .true. + + ! true => nvp can photosynthesize under snow + logical, public :: use_nvp_undersnow = .true. + + ! [PORTED by Hui Tang: NVP radiation model switch] + ! true => Approach A: NVP as ground boundary contribute to ground albedo, Beer's law PAR, + ! false => Approach B: NVP as leaf layer in Norman solver, soil albedo as ground albedo + logical, public :: nvp_rad_model_ground = .true. + + !---------------------------------------------------------- + ! LUNA switches !---------------------------------------------------------- logical, public :: use_luna = .false. ! true => use LUNA From b635d4e724ac625a7e44cc12f74fa2310f123b26 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Fri, 10 Apr 2026 00:00:55 +0300 Subject: [PATCH 002/113] Add control switches in namelist definition. --- bld/namelist_files/namelist_defaults_ctsm.xml | 6 ++++++ .../namelist_definition_ctsm.xml | 21 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/bld/namelist_files/namelist_defaults_ctsm.xml b/bld/namelist_files/namelist_defaults_ctsm.xml index f8b880caf0..1ca69adfcd 100644 --- a/bld/namelist_files/namelist_defaults_ctsm.xml +++ b/bld/namelist_files/namelist_defaults_ctsm.xml @@ -638,6 +638,12 @@ attributes from the config_cache.xml file (with keys converted to upper-case). .false. .false. + + +.false. +.false. +.false. + .true. .true. diff --git a/bld/namelist_files/namelist_definition_ctsm.xml b/bld/namelist_files/namelist_definition_ctsm.xml index 11c232615c..939c03ca5a 100644 --- a/bld/namelist_files/namelist_definition_ctsm.xml +++ b/bld/namelist_files/namelist_definition_ctsm.xml @@ -946,6 +946,27 @@ LUNA operates on C3 and non-crop vegetation (see vcmax_opt for how other veg is LUNA: Leaf Utilization of Nitrogen for Assimilation + + + +Toggle to activate the NVP (non-vascular plant: moss/lichen) ground layer. +Requires use_fates=".true.". + + + +Allow the NVP layer to remain active and exchange fluxes when covered by snow. +Only relevant when use_nvp=".true.". + + + +Use FATES Beer's law radiation model for the NVP ground layer instead of +the CLM bare-ground scheme. +Only relevant when use_nvp=".true.". + + Toggle to turn on the hillslope model From f53ed369aa5e7d680f0fa27d8a8b5426360ee9fa Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Fri, 10 Apr 2026 11:06:10 +0300 Subject: [PATCH 003/113] Initialize a module for NVP layer dynamics and water balance. --- src/biogeophys/NVPLayerDynamicsMod.F90 | 608 +++++++++++++++++++++++++ 1 file changed, 608 insertions(+) create mode 100644 src/biogeophys/NVPLayerDynamicsMod.F90 diff --git a/src/biogeophys/NVPLayerDynamicsMod.F90 b/src/biogeophys/NVPLayerDynamicsMod.F90 new file mode 100644 index 0000000000..3c6a525ee0 --- /dev/null +++ b/src/biogeophys/NVPLayerDynamicsMod.F90 @@ -0,0 +1,608 @@ +module NVPLayerDynamicsMod + + ! --------------------------------------------------------------------------- + ! DESCRIPTION: + ! [PORTED by Hui Tang: NVP (non-vascular plant, i.e. moss/lichen) layer dynamics] + ! + ! Updates CLM's vertical column geometry for the NVP layer at index 0, + ! based on FATES-derived coverage (col%frac_nvp) and thickness (col%dz_nvp). + ! + ! Also provides subroutines for NVP water physics: + ! NVPWaterRetentionCurve — van Genuchten water potential [mm] + ! NVPHydraulicConductivity — Mualem-van Genuchten hydraulic conductivity [m/s] + ! NVPEvaporation — NVP surface evaporation flux [kg m-2 s-1] + ! + ! Called from clmfates_interfaceMod%wrap_update_hlmfates_dyn each FATES + ! dynamics timestep, after bc_out%nvp_dz_pa and bc_out%nvp_frac_pa have + ! been aggregated to the column. + ! + ! Layer design: + ! - NVP occupies vertical index 0 (the slot below the bottom-most snow layer). + ! - col%jbot_sno = -1 when NVP is active (snow loops stop at index -1). + ! - col%jbot_sno = 0 when NVP is inactive (standard CLM snow behaviour). + ! - col%dz(c,0) and col%z(c,0) are set from col%dz_nvp(c). + ! - col%zi(c,0) = 0 (soil surface) is unchanged. + ! - col%zi(c,-1) = -dz_nvp (top interface of NVP layer). + ! + ! Energy and mass conservation on layer activation/deactivation: + ! - Activation (inactive→active): initialise t_soisno(c,0) = t_soisno(c,1), + ! h2osoi_liq(c,0) = 0, h2osoi_ice(c,0) = 0. The NVP layer starts + ! isothermal with the surface soil and dry; FATES will hydrate it over + ! subsequent timesteps. + ! - Deactivation (active→inactive): residual water in layer 0 is merged into + ! layer 1 with energy-weighted temperature; layer 0 state is then zeroed. + ! - Resize (thickness change while active): temperature and h2osoi are + ! unchanged — both are intensive quantities (T in K; h2osoi in kg m-2 + ! of ground area independent of layer thickness). + ! --------------------------------------------------------------------------- + + use shr_kind_mod , only : r8 => shr_kind_r8 + use decompMod , only : bounds_type + use ColumnType , only : col + use TemperatureType , only : temperature_type + use WaterStateType , only : waterstate_type + use WaterFluxBulkType , only : waterfluxbulk_type + use WaterDiagnosticBulkType, only : waterdiagnosticbulk_type + ! [PORTED by Hui Tang: soilstate for bidirectional NVP-soil Darcy flux] + use SoilStateType , only : soilstate_type + use clm_varcon , only : cpliq, cpice, denh2o, roverg + ! [PORTED by Hui Tang: use_nvp_undersnow flag to deactivate NVP when snow present] + use clm_varctl , only : use_nvp_undersnow + use QSatMod , only : QSat + ! [PORTED by Hui Tang: runtime-tunable NVP physics parameters] + use NVPParamsMod + + implicit none + private + + public :: UpdateNVPLayer + public :: NVPWaterRetentionCurve + public :: NVPHydraulicConductivity + public :: NVPEvaporation + public :: NVPWaterBalance_Column + public :: NVPLayerRestart + + character(len=*), parameter, private :: sourcefile = __FILE__ + +contains + + ! =========================================================================== + + subroutine UpdateNVPLayer(c, temperature_inst, waterstate_inst) + ! ------------------------------------------------------------------------- + ! Update NVP layer presence and geometry for column c, maintaining energy + ! and water conservation across activation / deactivation transitions. + ! + ! Inputs (from col): + ! col%dz_nvp(c) — FATES-derived column-effective NVP thickness [m] + ! col%frac_nvp(c) — FATES-derived column NVP fractional coverage [0-1] + ! + ! Outputs (written to col, and optionally temperature_inst, waterstate_inst): + ! col%nvp_layer_active(c) — .true. when NVP layer is active + ! col%jbot_sno(c) — -1 (NVP active) or 0 (inactive) + ! col%dz(c,0) — NVP layer thickness [m] + ! col%z(c,0) — NVP layer centre depth [m] + ! col%zi(c,-1) — NVP layer top interface [m] + ! + ! Conservation (applied only when both optional args are present): + ! temperature_inst%t_soisno_col(c,0) — initialised on activation + ! waterstate_inst%h2osoi_liq_col(c,0/1) — conserved on deactivation + ! waterstate_inst%h2osoi_ice_col(c,0/1) — conserved on deactivation + ! + ! Pass temperature_inst and waterstate_inst during normal timestep calls. + ! Omit them during restart / cold-start where thermodynamic state is set + ! independently (restart file or initTemperatureMod). + ! ------------------------------------------------------------------------- + integer, intent(in) :: c ! column index + type(temperature_type), optional, intent(inout) :: temperature_inst + class(waterstate_type), optional, intent(inout) :: waterstate_inst + + real(r8) :: dz_nvp + logical :: was_active ! NVP layer state at entry (before this update) + logical :: now_active ! NVP layer state after geometry check + real(r8) :: cv0 ! heat capacity of NVP layer [J m-2 K-1] + real(r8) :: cv1 ! heat capacity of soil layer 1 before merge [J m-2 K-1] + real(r8) :: cv1_new ! heat capacity of soil layer 1 after merge [J m-2 K-1] + + dz_nvp = col%dz_nvp(c) + was_active = col%nvp_layer_active(c) + + if (col%frac_nvp(c) > nvp_frac_min .and. dz_nvp > 0._r8) then + + ! --- Active (Appear or Grow/shrink) --- + now_active = .true. + col%nvp_layer_active(c) = .true. + col%jbot_sno(c) = -1 + col%dz(c,0) = dz_nvp + ! Layer centre is half the thickness above the soil surface (zi(c,0)=0) + col%z(c,0) = -0.5_r8 * dz_nvp + ! Top interface of NVP layer = bottom of the snow layer above + col%zi(c,-1) = -dz_nvp + + else + + ! --- Inactive (Disappear or Absent) --- + now_active = .false. + col%nvp_layer_active(c) = .false. + col%jbot_sno(c) = 0 + col%dz(c,0) = 0._r8 + col%z(c,0) = 0._r8 + ! Restore zi(c,-1) to soil surface when NVP is absent + col%zi(c,-1) = 0._r8 + + end if + + ! [PORTED by Hui Tang: deactivate NVP under snow when use_nvp_undersnow=.false.] + ! When snow is present (snl < 0), NVP cannot exchange energy or moisture with the + ! atmosphere directly. Override the FATES-based activation to treat the column as + ! standard CLM (jbot_sno=0) so snow layers couple directly to soil. + if (.not. use_nvp_undersnow .and. col%snl(c) < 0 .and. now_active) then + now_active = .false. + col%nvp_layer_active(c) = .false. + col%jbot_sno(c) = 0 + col%dz(c,0) = 0._r8 + col%z(c,0) = 0._r8 + col%zi(c,-1) = 0._r8 + end if + + ! [PORTED by Hui Tang: energy and mass conservation on NVP layer state transitions] + ! Conservation is only applied when temperature_inst and waterstate_inst are + ! provided (normal timestep path). During restart / cold-start the thermo- + ! dynamic state is restored separately and conservation is skipped. + if (present(temperature_inst) .and. present(waterstate_inst)) then + + if (.not. was_active .and. now_active) then + ! --- Appear: initialise layer-0 thermodynamic state from soil layer 1 --- + ! Temperature inherits from layer 1 to avoid spurious heat flux. + ! h2osoi starts at zero; FATES provides moisture via its own hydrology. + temperature_inst%t_soisno_col(c,0) = temperature_inst%t_soisno_col(c,1) + waterstate_inst%h2osoi_liq_col(c,0) = 0._r8 + waterstate_inst%h2osoi_ice_col(c,0) = 0._r8 + + else if (was_active .and. .not. now_active) then + ! --- Disappear: merge layer 0 into layer 1 conserving energy and water --- + ! Compute heat capacities (cv = cpliq*liq + cpice*ice, units J m-2 K-1) + cv0 = cpliq * waterstate_inst%h2osoi_liq_col(c,0) + & + cpice * waterstate_inst%h2osoi_ice_col(c,0) + cv1 = cpliq * waterstate_inst%h2osoi_liq_col(c,1) + & + cpice * waterstate_inst%h2osoi_ice_col(c,1) + ! Transfer water mass to layer 1 + waterstate_inst%h2osoi_liq_col(c,1) = waterstate_inst%h2osoi_liq_col(c,1) + & + waterstate_inst%h2osoi_liq_col(c,0) + waterstate_inst%h2osoi_ice_col(c,1) = waterstate_inst%h2osoi_ice_col(c,1) + & + waterstate_inst%h2osoi_ice_col(c,0) + ! Energy-weighted temperature for layer 1 after merge + cv1_new = cpliq * waterstate_inst%h2osoi_liq_col(c,1) + & + cpice * waterstate_inst%h2osoi_ice_col(c,1) + if (cv1_new > 0._r8) then + temperature_inst%t_soisno_col(c,1) = & + (cv0 * temperature_inst%t_soisno_col(c,0) + & + cv1 * temperature_inst%t_soisno_col(c,1)) / cv1_new + end if + ! Zero layer 0; set T to layer-1 value to avoid stale data on reactivation + waterstate_inst%h2osoi_liq_col(c,0) = 0._r8 + waterstate_inst%h2osoi_ice_col(c,0) = 0._r8 + temperature_inst%t_soisno_col(c,0) = temperature_inst%t_soisno_col(c,1) + + end if + ! --- Grow/shrink (active→active): T and h2osoi are per unit ground area, + ! so no adjustment is needed when dz_nvp changes. --- + + end if + + end subroutine UpdateNVPLayer + + ! =========================================================================== + + subroutine NVPWaterRetentionCurve(vol_liq, n_van, alpha_van, watsat, watres, smp) + ! ------------------------------------------------------------------------- + ! Convert NVP volumetric liquid water content to soil water potential [mm] + ! using the van Genuchten (1980) retention curve formulation. + ! + ! Ported from Python moss_water_code.py::water_retention_curve. + ! + ! Arguments: + ! vol_liq — volumetric liquid water content [m3 m-3] + ! n_van — van Genuchten shape parameter n [-] (> 1) + ! alpha_van — van Genuchten inverse air-entry pressure [cm-1 * 10] + ! watsat — saturated volumetric water content (= theta_nvp_max) [m3 m-3] + ! watres — residual volumetric water content [m3 m-3] + ! smp — soil/NVP matric potential [mm] (negative; more negative = drier) + ! ------------------------------------------------------------------------- + real(r8), intent(in) :: vol_liq + real(r8), intent(in) :: n_van + real(r8), intent(in) :: alpha_van + real(r8), intent(in) :: watsat + real(r8), intent(in) :: watres + real(r8), intent(out) :: smp + + real(r8) :: m_van ! van Genuchten m = 1 - 1/n + real(r8) :: eff_porosity ! effective porosity [m3 m-3] + real(r8) :: satfrac ! effective saturation fraction [-] + + m_van = 1.0_r8 - 1.0_r8 / n_van + ! NVP is assumed to have no ice fraction (layer-0 is always above 0 C in practice) + eff_porosity = max(0.01_r8, watsat) + + satfrac = (vol_liq - watres) / (eff_porosity - watres) + satfrac = max(0.0_r8, min(1.0_r8, satfrac)) ! clamp to [0, 1] + + ! van Genuchten retention curve: psi in units of (1/alpha_van) + smp = -(1.0_r8 / alpha_van) * & + (satfrac**( 1.0_r8 / (-m_van) ) - 1.0_r8)**(1.0_r8 / n_van) + smp = smp * 102.0_r8 ! convert to mm + + end subroutine NVPWaterRetentionCurve + + ! =========================================================================== + + subroutine NVPHydraulicConductivity(vol_liq, n_van, watsat, watres, ksat, khydr) + ! ------------------------------------------------------------------------- + ! Compute NVP hydraulic conductivity [m s-1] using the Mualem-van Genuchten + ! (1976/1980) formulation. + ! + ! Ported from Python moss_water_code.py::hydraulic_conductivity. + ! + ! Arguments: + ! vol_liq — volumetric liquid water content [m3 m-3] + ! n_van — van Genuchten shape parameter n [-] + ! watsat — saturated volumetric water content [m3 m-3] + ! watres — residual volumetric water content [m3 m-3] + ! ksat — saturated hydraulic conductivity [m s-1] + ! khydr — unsaturated hydraulic conductivity [m s-1] + ! ------------------------------------------------------------------------- + real(r8), intent(in) :: vol_liq + real(r8), intent(in) :: n_van + real(r8), intent(in) :: watsat + real(r8), intent(in) :: watres + real(r8), intent(in) :: ksat + real(r8), intent(out) :: khydr + + real(r8) :: m_van ! van Genuchten m = 1 - 1/n + real(r8) :: eff_porosity ! effective porosity [m3 m-3] + real(r8) :: satfrac ! effective saturation fraction [-] + + m_van = 1.0_r8 - 1.0_r8 / n_van + ! NVP is assumed to have no ice + eff_porosity = max(0.01_r8, watsat) + + satfrac = (vol_liq - watres) / (eff_porosity - watres) + satfrac = max(0.0_r8, min(1.0_r8, satfrac)) ! clamp to [0, 1] + + ! Mualem-van Genuchten: khydr = ksat * Se^0.5 * (1 - (1 - Se^(1/m))^m)^2 + khydr = ksat & + * satfrac**0.5_r8 & + * (1.0_r8 - (1.0_r8 - satfrac**(1.0_r8 / m_van))**m_van)**2.0_r8 + + end subroutine NVPHydraulicConductivity + + ! =========================================================================== + + subroutine NVPEvaporation(theta_nvp, t_nvp, forc_pbot, rho_atm, q_atm, raw, & + n_van, alpha_van, watsat, watres, & + evap_nvp, rnvp, psi_nvp, alpha_nvp, q_nvp) + ! ------------------------------------------------------------------------- + ! Compute NVP surface evaporation flux [kg m-2 s-1]. + ! + ! Ported from the hourly loop in Python moss_water_code.py. + ! + ! Physics: + ! 1. Surface resistance rnvp increases cubically as NVP dries: + ! rnvp = rnvp_min + rnvp_amp * (1 - satfrac)^rnvp_exp + ! 2. Water potential psi_nvp [mm] from van Genuchten retention curve. + ! 3. Activity correction alpha_nvp [-] from the Kelvin equation: + ! alpha_nvp = exp(psi_nvp / (roverg * t_nvp)) + ! where roverg = R_w/g * 1000 [mm K] is the CLM constant from clm_varcon. + ! 4. Specific humidity at NVP surface: q_nvp = alpha_nvp * qs(t_nvp, pbot) + ! 5. Evaporation: E = max(-rho_atm * (q_atm - q_nvp) / (raw + rnvp), 0) + ! (positive = evaporation from NVP; condensation onto NVP is excluded) + ! + ! Arguments: + ! theta_nvp — volumetric liquid water content of NVP [m3 m-3] + ! t_nvp — NVP surface temperature [K] + ! forc_pbot — atmospheric pressure [Pa] + ! rho_atm — air density [kg m-3] + ! q_atm — specific humidity of overlying air [kg kg-1] + ! raw — aerodynamic resistance to water vapour transfer [s m-1] + ! n_van — van Genuchten n [-] + ! alpha_van — van Genuchten alpha [cm-1 * 10] + ! watsat — saturated volumetric water content [m3 m-3] + ! watres — residual volumetric water content [m3 m-3] + ! evap_nvp — NVP evaporation flux [kg m-2 s-1] (out, >= 0) + ! rnvp — NVP surface resistance [s m-1] (out, diagnostic) + ! psi_nvp — NVP matric potential [mm] (out, diagnostic) + ! alpha_nvp — Kelvin activity correction [-] (out, diagnostic) + ! q_nvp — specific humidity at NVP surface [kg kg-1] (out, diagnostic) + ! ------------------------------------------------------------------------- + real(r8), intent(in) :: theta_nvp + real(r8), intent(in) :: t_nvp + real(r8), intent(in) :: forc_pbot + real(r8), intent(in) :: rho_atm + real(r8), intent(in) :: q_atm + real(r8), intent(in) :: raw + real(r8), intent(in) :: n_van + real(r8), intent(in) :: alpha_van + real(r8), intent(in) :: watsat + real(r8), intent(in) :: watres + real(r8), intent(out) :: evap_nvp + real(r8), intent(out) :: rnvp + real(r8), intent(out) :: psi_nvp + real(r8), intent(out) :: alpha_nvp + real(r8), intent(out) :: q_nvp + + real(r8) :: eff_porosity ! effective porosity [m3 m-3] + real(r8) :: satfrac ! effective saturation fraction [-] + real(r8) :: qs_nvp ! saturation specific humidity at t_nvp [kg kg-1] + + ! --- 1. Effective saturation fraction --- + eff_porosity = max(0.01_r8, watsat) + satfrac = (theta_nvp - watres) / (eff_porosity - watres) + satfrac = max(0.0_r8, min(1.0_r8, satfrac)) + + ! --- 2. Surface resistance (high when dry, low when saturated) --- + rnvp = rnvp_min + rnvp_amp * (1.0_r8 - satfrac)**rnvp_exp + + ! --- 3. Van Genuchten matric potential [mm] --- + call NVPWaterRetentionCurve(theta_nvp, n_van, alpha_van, watsat, watres, psi_nvp) + + ! --- 4. Kelvin activity correction: alpha = exp(psi / (roverg * T)) --- + ! roverg = R_w/g * 1000 [mm K] so that psi [mm] / (roverg [mm K] * T [K]) is + ! dimensionless. + alpha_nvp = exp(psi_nvp / (roverg * t_nvp)) + + ! --- 5. Specific humidity at NVP surface --- + call QSat(t_nvp, forc_pbot, qs_nvp) + q_nvp = alpha_nvp * qs_nvp + + ! --- 6. Evaporation flux (no condensation: capped at zero) --- + ! Convention: positive evap_nvp means water leaves NVP surface. + ! Flux = -rho * (q_atm - q_nvp) / (raw + rnvp) + ! = rho * (q_nvp - q_atm) / (raw + rnvp) + evap_nvp = max(-rho_atm * (q_atm - q_nvp) / (raw + rnvp), 0.0_r8) + + end subroutine NVPEvaporation + + ! =========================================================================== + + ! [PORTED by Hui Tang: NVP column water balance — gravity drainage from layer 0 to soil layer 1] + ! [PORTED by Hui Tang: bidirectional Darcy flux between NVP layer 0 and soil layer 1] + subroutine NVPWaterBalance_Column(bounds, dtime, waterfluxbulk_inst, waterstate_inst, & + waterdiagnosticbulk_inst, soilstate_inst) + ! ------------------------------------------------------------------------- + ! Update h2osoi_liq(c,0) for the NVP layer and compute the net water + ! exchange with soil layer 1 via a bidirectional Darcy flux. + ! + ! Physics: + ! q01 = K_interface * ( (ψ_nvp - ψ_soil1) / Δz_mm + 1 ) + ! + ! where q01 > 0 means downward (NVP drains to soil) and + ! q01 < 0 means upward (NVP absorbs from soil). + ! + ! ψ_nvp — van Genuchten matric potential of NVP layer [mm] + ! ψ_soil1 — CLM Clapp-Hornberger matric potential of soil layer 1 [mm], + ! from previous timestep (soilstate_inst%smp_l_col) + ! Δz_mm — distance between layer centres [mm] + ! = (0.5*dz(c,0) + 0.5*dz(c,1)) * 1000 + ! K_interface — harmonic mean of K_nvp and K_soil1 [mm/s] + ! + ! Capping: + ! Downward (q01 > 0): limited by available NVP liquid water + ! Upward (q01 < 0): limited by current soil layer-1 liquid water + ! + ! qflx_nvp_drain_col is signed (+: NVP→soil, -: soil→NVP) and is added + ! to qflx_infl in Infiltration so SoilWater sees the net exchange. + ! + ! Must be called after: + ! - SnowWater (so qflx_rain_plus_snomelt is finalised) + ! - clm_drv_patch2col p2c (so qflx_ev_nvp_col is valid) + ! and before: + ! - SetQflxInputs / Infiltration (so qflx_nvp_drain_col is available) + ! + ! Outputs written: + ! waterfluxbulk_inst%qflx_nvp_infl_col(c) [mm/s] water into NVP top + ! waterfluxbulk_inst%qflx_nvp_drain_col(c) [mm/s] net NVP-soil exchange (+down) + ! waterstate_inst%h2osoi_liq_col(c,0) [kg m-2] updated NVP water store + ! waterdiagnosticbulk_inst%fwet_nvp_col(c) [-] updated NVP wet fraction + ! ------------------------------------------------------------------------- + type(bounds_type), intent(in) :: bounds + real(r8), intent(in) :: dtime ! timestep [s] + type(waterfluxbulk_type), intent(inout) :: waterfluxbulk_inst + class(waterstate_type), intent(inout) :: waterstate_inst + type(waterdiagnosticbulk_type), intent(inout) :: waterdiagnosticbulk_inst + type(soilstate_type), intent(in) :: soilstate_inst + + integer :: c + real(r8) :: frac_h2osfc ! fractional area with surface water [-] + real(r8) :: frac_nvp_eff ! effective NVP area fraction (not covered by h2osfc) [-] + real(r8) :: vol_liq ! NVP volumetric liquid water content [m3 m-3] + real(r8) :: khydr_nvp ! NVP unsaturated hydraulic conductivity [m s-1] + real(r8) :: K_nvp_mms ! khydr_nvp converted to mm/s + real(r8) :: K_soil1 ! soil layer 1 hydraulic conductivity [mm/s] + real(r8) :: K_interface ! harmonic-mean interface conductivity [mm/s] + real(r8) :: psi_nvp ! NVP van Genuchten matric potential [mm] + real(r8) :: smp_soil1 ! soil layer 1 matric potential, prev. timestep [mm] + real(r8) :: dz_iface_mm ! distance between NVP and soil layer 1 centres [mm] + real(r8) :: q01 ! Darcy flux NVP→soil (+down, -up) [mm s-1] + real(r8) :: h2osoi_net ! h2osoi_liq(c,0) after infl and evap [kg m-2] + real(r8) :: satfrac ! NVP effective saturation fraction [-] + + associate( & + qflx_rain_plus_snomelt => waterfluxbulk_inst%qflx_rain_plus_snomelt_col, & + qflx_ev_nvp_col => waterfluxbulk_inst%qflx_ev_nvp_col, & + qflx_nvp_infl_col => waterfluxbulk_inst%qflx_nvp_infl_col, & + qflx_nvp_drain_col => waterfluxbulk_inst%qflx_nvp_drain_col, & + h2osoi_liq => waterstate_inst%h2osoi_liq_col, & + h2onvp_col => waterstate_inst%h2onvp_col, & ! [PORTED by Hui Tang: sync diagnostic copy] + smp_l => soilstate_inst%smp_l_col, & + hk_l => soilstate_inst%hk_l_col, & + frac_h2osfc_col => waterdiagnosticbulk_inst%frac_h2osfc_col, & + fwet_nvp_col => waterdiagnosticbulk_inst%fwet_nvp_col, & + vwc_nvp_col => waterdiagnosticbulk_inst%vwc_nvp_col & ! [PORTED by Hui Tang: volumetric water content] + ) + + do c = bounds%begc, bounds%endc + + if (.not. col%nvp_layer_active(c)) then + qflx_nvp_infl_col(c) = 0._r8 + qflx_nvp_drain_col(c) = 0._r8 + cycle + end if + + ! --- Effective NVP area: not covered by surface water --- + frac_h2osfc = frac_h2osfc_col(c) + frac_nvp_eff = min(col%frac_nvp(c), max(0._r8, 1._r8 - frac_h2osfc)) + + ! --- Water input to NVP from precipitation / snowmelt --- + qflx_nvp_infl_col(c) = frac_nvp_eff * qflx_rain_plus_snomelt(c) ! [mm/s] + + ! --- NVP volumetric water content (clamped to valid range) --- + if (col%dz(c,0) > 0._r8) then + vol_liq = h2osoi_liq(c,0) / (denh2o * col%dz(c,0)) ! [m3 m-3] + vol_liq = max(0._r8, min(watsat_nvp, vol_liq)) + else + vol_liq = 0._r8 + end if + + ! --- NVP van Genuchten matric potential and hydraulic conductivity --- + call NVPWaterRetentionCurve(vol_liq, n_van_nvp, alpha_van_nvp, & + watsat_nvp, watres_nvp, psi_nvp) + call NVPHydraulicConductivity(vol_liq, n_van_nvp, watsat_nvp, watres_nvp, & + ksat_nvp, khydr_nvp) + K_nvp_mms = khydr_nvp * 1000._r8 ! m/s → mm/s + + ! --- Bidirectional Darcy flux with soil layer 1 --- + ! Uses previous-timestep smp_l and hk_l (explicit time stepping) + smp_soil1 = smp_l(c,1) ! [mm] + K_soil1 = hk_l(c,1) ! [mm/s] + + ! Harmonic-mean interface conductivity + if (K_nvp_mms + K_soil1 > 0._r8) then + K_interface = 2._r8 * K_nvp_mms * K_soil1 / (K_nvp_mms + K_soil1) + else + K_interface = 0._r8 + end if + + ! Distance between layer centres [mm] + dz_iface_mm = (0.5_r8 * col%dz(c,0) + 0.5_r8 * col%dz(c,1)) * 1000._r8 + + ! Darcy flux: q = K * (grad_psi + gravity), positive = downward + q01 = K_interface * ((psi_nvp - smp_soil1) / dz_iface_mm + 1.0_r8) + + ! --- Update h2osoi_liq(c,0): add infl, subtract evap; cannot go negative --- + h2osoi_net = h2osoi_liq(c,0) & + + (qflx_nvp_infl_col(c) - qflx_ev_nvp_col(c)) * dtime ! [kg m-2] + h2osoi_net = max(0._r8, h2osoi_net) + + if (q01 >= 0._r8) then + ! Downward drainage: cap by available NVP liquid water + qflx_nvp_drain_col(c) = min(q01, h2osoi_net / dtime) + else + ! Upward absorption from soil: cap by current soil layer-1 liquid water + qflx_nvp_drain_col(c) = max(q01, -h2osoi_liq(c,1) / dtime) + end if + + h2osoi_liq(c,0) = h2osoi_net - qflx_nvp_drain_col(c) * dtime ! [kg m-2] + + ! --- Step 6: Saturation excess — flush water above watsat to drain --- + if (col%dz(c,0) > 0._r8) then + vol_liq = h2osoi_liq(c,0) / (denh2o * col%dz(c,0)) + if (vol_liq > watsat_nvp) then + ! Excess water above saturation drains immediately to soil layer 1 + satfrac = (vol_liq - watsat_nvp) * denh2o * col%dz(c,0) ! excess [kg m-2] + qflx_nvp_drain_col(c) = qflx_nvp_drain_col(c) + satfrac / dtime + h2osoi_liq(c,0) = h2osoi_liq(c,0) - satfrac + end if + end if + + ! --- Update fwet_nvp (saturation fraction passed to FATES) --- + if (col%dz(c,0) > 0._r8) then + vol_liq = h2osoi_liq(c,0) / (denh2o * col%dz(c,0)) + satfrac = (vol_liq - watres_nvp) / max(watsat_nvp - watres_nvp, 1.e-10_r8) + fwet_nvp_col(c) = max(0._r8, min(1._r8, satfrac)) + else + fwet_nvp_col(c) = 0._r8 + end if + + ! [PORTED by Hui Tang: sync diagnostic copies for history output] + ! h2onvp_col mirrors h2osoi_liq(c,0) [kg m-2 = mm H2O]; vwc_nvp_col is volumetric [m3 m-3] + h2onvp_col(c) = h2osoi_liq(c,0) + if (col%dz(c,0) > 0._r8) then + vwc_nvp_col(c) = h2osoi_liq(c,0) / (denh2o * col%dz(c,0)) + else + vwc_nvp_col(c) = 0._r8 + end if + + end do + + end associate + + end subroutine NVPWaterBalance_Column + + ! =========================================================================== + + subroutine NVPLayerRestart(bounds, ncid, flag) + ! ------------------------------------------------------------------------- + ! [PORTED by Hui Tang: restart NVP column geometry and layer-state variables] + ! + ! Saves/restores the four column-level variables that define NVP layer + ! presence and geometry. Without these, jbot_sno and col%dz(c,0) would + ! revert to zero at restart, causing CLM physics to miss the NVP layer + ! until the first FATES dynamics call. + ! + ! Note: t_soisno(:,0), h2osoi_liq(:,0), h2osoi_ice(:,0) are already + ! covered by the standard T_SOISNO / H2OSOI_LIQ / H2OSOI_ICE restart + ! variables (dim2name='levtot', which spans -nlevsno+1:nlevgrnd). + ! h2onvp_col and t_nvp_col are restarted in WaterStateType and + ! TemperatureType respectively. + ! ------------------------------------------------------------------------- + use ncdio_pio , only : file_desc_t, ncd_double, ncd_int, ncd_log + use restFileMod , only : restartvar + ! + ! !ARGUMENTS: + type(bounds_type) , intent(in) :: bounds + type(file_desc_t) , intent(inout) :: ncid + character(len=*) , intent(in) :: flag ! 'define', 'write', or 'read' + ! + ! !LOCAL VARIABLES: + logical :: readvar + !----------------------------------------------------------------------- + + ! Column NVP layer thickness [m] + call restartvar(ncid=ncid, flag=flag, varname='DZ_NVP', xtype=ncd_double, & + dim1name='column', & + long_name='NVP (moss/lichen) layer thickness', units='m', & + interpinic_flag='interp', readvar=readvar, data=col%dz_nvp) + if (flag == 'read' .and. .not. readvar) then + col%dz_nvp(bounds%begc:bounds%endc) = 0._r8 + end if + + ! Column NVP fractional coverage [-] + call restartvar(ncid=ncid, flag=flag, varname='FRAC_NVP', xtype=ncd_double, & + dim1name='column', & + long_name='NVP (moss/lichen) fractional coverage', units='1', & + interpinic_flag='interp', readvar=readvar, data=col%frac_nvp) + if (flag == 'read' .and. .not. readvar) then + col%frac_nvp(bounds%begc:bounds%endc) = 0._r8 + end if + + ! Logical flag: .true. when NVP occupies layer index 0 + call restartvar(ncid=ncid, flag=flag, varname='NVP_LAYER_ACTIVE', xtype=ncd_log, & + dim1name='column', & + long_name='flag: NVP layer occupies vertical index 0', units='', & + interpinic_flag='interp', readvar=readvar, data=col%nvp_layer_active) + if (flag == 'read' .and. .not. readvar) then + col%nvp_layer_active(bounds%begc:bounds%endc) = .false. + end if + + ! Bottom index of active snow: 0 = no NVP, -1 = NVP present at layer 0 + call restartvar(ncid=ncid, flag=flag, varname='JBOT_SNO', xtype=ncd_int, & + dim1name='column', & + long_name='bottom index of active snow layers (0 or -1 with NVP)', units='', & + interpinic_flag='interp', readvar=readvar, data=col%jbot_sno) + if (flag == 'read' .and. .not. readvar) then + col%jbot_sno(bounds%begc:bounds%endc) = 0 + end if + + end subroutine NVPLayerRestart + +end module NVPLayerDynamicsMod From 8f5d482828e6b36791adc3f1c1efdbbab431b3c1 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Fri, 10 Apr 2026 11:08:55 +0300 Subject: [PATCH 004/113] Define NVP layer controllers and variables. --- src/main/ColumnType.F90 | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/main/ColumnType.F90 b/src/main/ColumnType.F90 index ab7ee8e261..c73f521daf 100644 --- a/src/main/ColumnType.F90 +++ b/src/main/ColumnType.F90 @@ -60,9 +60,20 @@ module ColumnType ! vertical levels integer , pointer :: snl (:) ! number of snow layers - real(r8), pointer :: dz (:,:) ! layer thickness (m) (-nlevsno+1:nlevgrnd) - real(r8), pointer :: z (:,:) ! layer depth (m) (-nlevsno+1:nlevgrnd) - real(r8), pointer :: zi (:,:) ! interface level below a "z" level (m) (-nlevsno+0:nlevgrnd) + ! [PORTED by Hui Tang: bottom index of active snow layers for NVP layer-0 design] + ! jbot_sno = 0 when no NVP layer (standard CLM: snow loops run snl(c)+1 .. 0) + ! jbot_sno = -1 when NVP layer is present at index 0 (snow loops stop at -1) + integer , pointer :: jbot_sno (:) ! bottom index of active snow layers (0 or -1) + ! [PORTED by Hui Tang: NVP (moss/lichen) layer presence flag at vertical index 0] + logical , pointer :: nvp_layer_active (:) ! .true. when NVP layer occupies index 0 + ! [PORTED by Hui Tang: column-effective NVP layer geometry, aggregated from FATES bc_out] + ! Updated each FATES dynamics timestep in clmfates_interfaceMod%wrap_update_hlmfates_dyn. + ! Consumed by NVPLayerDynamicsMod%UpdateNVPLayer to drive col%dz(c,0) and jbot_sno. + real(r8), pointer :: dz_nvp (:) ! column-effective NVP layer thickness [m] + real(r8), pointer :: frac_nvp (:) ! column-effective NVP fractional coverage [0-1] + real(r8), pointer :: dz (:,:) ! layer thickness (m) (-nlevsno+1:nlevgrnd) + real(r8), pointer :: z (:,:) ! layer depth (m) (-nlevsno+1:nlevgrnd) + real(r8), pointer :: zi (:,:) ! interface level below a "z" level (m) (-nlevsno+0:nlevgrnd) real(r8), pointer :: zii (:) ! convective boundary height [m] real(r8), pointer :: dz_lake (:,:) ! lake layer thickness (m) (1:nlevlak) real(r8), pointer :: z_lake (:,:) ! layer depth for lake (m) @@ -135,6 +146,12 @@ subroutine Init(this, begc, endc) ! The following is set in initVerticalMod allocate(this%snl (begc:endc)) ; this%snl (:) = ispval !* cannot be averaged up + ! [PORTED by Hui Tang: allocate NVP layer-0 control arrays] + allocate(this%jbot_sno (begc:endc)) ; this%jbot_sno (:) = 0 ! default: no NVP, snow to index 0 + allocate(this%nvp_layer_active(begc:endc)) ; this%nvp_layer_active(:) = .false. + ! [PORTED by Hui Tang: column-effective NVP geometry; zero until FATES dynamics provides values] + allocate(this%dz_nvp (begc:endc)) ; this%dz_nvp (:) = 0._r8 + allocate(this%frac_nvp(begc:endc)) ; this%frac_nvp(:) = 0._r8 allocate(this%dz (begc:endc,-nlevsno+1:nlevmaxurbgrnd)) ; this%dz (:,:) = nan allocate(this%z (begc:endc,-nlevsno+1:nlevmaxurbgrnd)) ; this%z (:,:) = nan allocate(this%zi (begc:endc,-nlevsno+0:nlevmaxurbgrnd)) ; this%zi (:,:) = nan @@ -183,6 +200,10 @@ subroutine Clean(this) deallocate(this%is_fates ) deallocate(this%type_is_dynamic) deallocate(this%snl ) + deallocate(this%jbot_sno ) + deallocate(this%nvp_layer_active) + deallocate(this%dz_nvp ) + deallocate(this%frac_nvp) deallocate(this%dz ) deallocate(this%z ) deallocate(this%zi ) From 6f677b756a390066dc6ef93f472edaa481025c5d Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Fri, 10 Apr 2026 11:32:06 +0300 Subject: [PATCH 005/113] Pass NVP namelist control to FATES. --- src/utils/clmfates_interfaceMod.F90 | 33 +++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/utils/clmfates_interfaceMod.F90 b/src/utils/clmfates_interfaceMod.F90 index 19b247218e..c9e2e16a0f 100644 --- a/src/utils/clmfates_interfaceMod.F90 +++ b/src/utils/clmfates_interfaceMod.F90 @@ -81,6 +81,12 @@ module CLMFatesInterfaceMod use clm_varctl , only : fates_history_dimlevel use clm_varctl , only : nsrest, nsrBranch use clm_varctl , only : Allocate_Carbon_only + ! [PORTED by Hui Tang: nvp (moss/lichen) control switches] + use clm_varctl , only : use_nvp + use clm_varctl , only : use_nvp_undersnow + use clm_varctl , only : nvp_rad_model_ground + ! [PORTED by Hui Tang: NVP layer geometry updater] + use NVPLayerDynamicsMod, only : UpdateNVPLayer use clm_varcon , only : tfrz use clm_varcon , only : spval use clm_varcon , only : denice @@ -314,6 +320,10 @@ subroutine CLMFatesGlobals1(surf_numpft,surf_numcft,maxsoil_patches) integer :: pass_use_sp integer :: pass_masterproc integer :: pass_use_luh2 + ! [PORTED by Hui Tang: nvp (moss/lichen) control integer flags] + integer :: pass_nvp + integer :: pass_nvp_undersnow + integer :: pass_nvp_rad_model_ground logical :: verbose_output call t_startf('fates_globals1') @@ -367,6 +377,29 @@ subroutine CLMFatesGlobals1(surf_numpft,surf_numcft,maxsoil_patches) call set_fates_ctrlparms('parteh_mode',ival=fates_parteh_mode) + ! [PORTED by Hui Tang: pass nvp (moss/lichen) switches to FATES] + if (use_nvp) then + pass_nvp = 1 + else + pass_nvp = 0 + end if + call set_fates_ctrlparms('use_nvp', ival=pass_nvp) + + if (use_nvp_undersnow) then + pass_nvp_undersnow = 1 + else + pass_nvp_undersnow = 0 + end if + call set_fates_ctrlparms('use_nvp_undersnow', ival=pass_nvp_undersnow) + + ! [PORTED by Hui Tang: pass NVP radiation model choice to FATES] + if (nvp_rad_model_ground) then + pass_nvp_rad_model_ground = 1 + else + pass_nvp_rad_model_ground = 0 + end if + call set_fates_ctrlparms('nvp_rad_model_ground', ival=pass_nvp_rad_model_ground) + end if ! The following call reads in the parameter file From 2043eaa268dad417f49bc733712f1f189ca0d44a Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Fri, 10 Apr 2026 12:25:51 +0300 Subject: [PATCH 006/113] Add centralized parameter module for NVP (temporal solution). --- src/biogeophys/NVPParamsMod.F90 | 51 +++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/biogeophys/NVPParamsMod.F90 diff --git a/src/biogeophys/NVPParamsMod.F90 b/src/biogeophys/NVPParamsMod.F90 new file mode 100644 index 0000000000..652c12eb6d --- /dev/null +++ b/src/biogeophys/NVPParamsMod.F90 @@ -0,0 +1,51 @@ +module NVPParamsMod + + ! [PORTED by Hui Tang: centralized CLM-side NVP (moss/lichen) physics parameters] + ! + ! All tunable parameters for the NVP layer in CLM are declared here with their + ! default values. They are readable at runtime via the nvp_inparm namelist group + ! in user_nl_clm (or the model's standard namelist input file). Default values + ! reproduce the original hardcoded constants and leave model behaviour unchanged + ! when nvp_inparm is absent from the namelist. + ! + ! Usage: + ! use NVPParamsMod, only : nvp_frac_min, watsat_nvp, ... + ! + ! Parameters: + ! nvp_frac_min — min fractional coverage to activate the NVP layer + ! rnvp_min — minimum surface evaporation resistance (fully saturated) [s m-1] + ! rnvp_amp — resistance amplitude as NVP dries [s m-1] + ! rnvp_exp — exponent of resistance–dryness curve [-] + ! ksat_nvp — saturated hydraulic conductivity [m s-1] + ! n_van_nvp — van Genuchten shape parameter n [-] + ! alpha_van_nvp — van Genuchten inverse air-entry pressure alpha [cm-1] + ! watsat_nvp — saturated volumetric water content (porosity) [m3 m-3] + ! watres_nvp — residual volumetric water content [m3 m-3] + + use shr_kind_mod, only : r8 => shr_kind_r8 + + implicit none + public + + ! Activation threshold + real(r8) :: nvp_frac_min = 1.0e-6_r8 ! min NVP coverage fraction to activate layer [-] + + ! Evaporation resistance (van de Griend & Owe, 1994 / Daamen & Simmonds) + ! rnvp = rnvp_min + rnvp_amp * (1 - satfrac)^rnvp_exp [s m-1] + real(r8) :: rnvp_min = 10.0_r8 ! minimum surface resistance when saturated [s m-1] + real(r8) :: rnvp_amp = 500.0_r8 ! amplitude of resistance increase when dry [s m-1] + real(r8) :: rnvp_exp = 3.0_r8 ! exponent of dryness function [-] + + ! Hydraulic properties (Mualem-van Genuchten) + real(r8) :: ksat_nvp = 1.0e-4_r8 ! saturated hydraulic conductivity [m s-1] + real(r8) :: n_van_nvp = 1.5_r8 ! van Genuchten shape parameter n [-] + real(r8) :: alpha_van_nvp = 0.01_r8 ! van Genuchten alpha [cm-1] + real(r8) :: watsat_nvp = 0.85_r8 ! saturated volumetric water content [m3 m-3] + real(r8) :: watres_nvp = 0.05_r8 ! residual volumetric water content [m3 m-3] + + ! Thermal properties of the dry NVP matrix (Farouki-style mixing with water/ice) + ! [PORTED by Hui Tang: NVP dry-matrix thermal parameters for SoilTemperatureMod] + real(r8) :: thk_dry_nvp = 0.05_r8 ! dry NVP thermal conductivity [W m-1 K-1] + real(r8) :: csol_nvp = 0.58e6_r8 ! dry NVP volumetric heat capacity [J m-3 K-1] + +end module NVPParamsMod From 3711441ef3e5e0a1a503062f01d1732ce470c9f9 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Fri, 10 Apr 2026 13:00:26 +0300 Subject: [PATCH 007/113] Add NVP radiation related variables and parameters. --- src/biogeophys/SurfaceAlbedoType.F90 | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/biogeophys/SurfaceAlbedoType.F90 b/src/biogeophys/SurfaceAlbedoType.F90 index 10819a47b6..e23cd23a32 100644 --- a/src/biogeophys/SurfaceAlbedoType.F90 +++ b/src/biogeophys/SurfaceAlbedoType.F90 @@ -8,6 +8,8 @@ module SurfaceAlbedoType use clm_varpar , only : numrad, nlevcan, nlevsno use abortutils , only : endrun use clm_varctl , only : use_SSRE, use_snicar_frc + ! [PORTED by Hui Tang: nvp (moss/lichen) control switch] + use clm_varctl , only : use_nvp ! ! !PUBLIC TYPES: implicit none @@ -33,6 +35,17 @@ module SurfaceAlbedoType real(r8), pointer :: albgri_dst_col (:,:) ! col ground diffuse albedo without dust (numrad) real(r8), pointer :: albgrd_col (:,:) ! col ground albedo (direct) (numrad) real(r8), pointer :: albgri_col (:,:) ! col ground albedo (diffuse) (numrad) + ! [PORTED by Hui Tang: nvp (moss/lichen) surface albedo fields] + real(r8), pointer :: fabd_nvp_col (:,:) ! col flux absorbed by nvp per unit direct flux (numrad) + real(r8), pointer :: fabi_nvp_col (:,:) ! col flux absorbed by nvp per unit diffuse flux (numrad) + ! [PORTED by Hui Tang: NVP optical properties for SNICAR layer-0 (Approach B)] + ! nvp_tau_col: column-mean optical depth = k_nvp * lai_nvp * nvp_frac [-] + ! nvp_omega_vis_col: single-scatter albedo in VIS band = rhol(nvp_ft,1) + taul(nvp_ft,1) + ! nvp_omega_nir_col: single-scatter albedo in NIR band = rhol(nvp_ft,2) + taul(nvp_ft,2) + ! All set in wrap_canopy_radiation; read by SurfaceAlbedoMod before SNICAR_RT calls. + real(r8), pointer :: nvp_tau_col (:) ! col NVP optical depth (k*LAI*frac) [-] + real(r8), pointer :: nvp_omega_vis_col (:) ! col NVP single-scatter albedo VIS [-] + real(r8), pointer :: nvp_omega_nir_col (:) ! col NVP single-scatter albedo NIR [-] real(r8), pointer :: albsod_col (:,:) ! col soil albedo: direct (col,bnd) [frc] real(r8), pointer :: albsoi_col (:,:) ! col soil albedo: diffuse (col,bnd) [frc] real(r8), pointer :: albsnd_hst_col (:,:) ! col snow albedo, direct , for history files (col,bnd) [frc] @@ -137,6 +150,15 @@ subroutine InitAllocate(this, bounds) allocate(this%coszen_col (begc:endc)) ; this%coszen_col (:) = nan allocate(this%albgrd_col (begc:endc,numrad)) ; this%albgrd_col (:,:) = nan allocate(this%albgri_col (begc:endc,numrad)) ; this%albgri_col (:,:) = nan + ! [PORTED by Hui Tang: allocate nvp (moss/lichen) surface albedo fields] + if (use_nvp) then + allocate(this%fabd_nvp_col (begc:endc,numrad)) ; this%fabd_nvp_col (:,:) = 0._r8 + allocate(this%fabi_nvp_col (begc:endc,numrad)) ; this%fabi_nvp_col (:,:) = 0._r8 + ! [PORTED by Hui Tang: allocate NVP optical properties for SNICAR layer-0] + allocate(this%nvp_tau_col (begc:endc)) ; this%nvp_tau_col (:) = 0._r8 + allocate(this%nvp_omega_vis_col (begc:endc)) ; this%nvp_omega_vis_col (:) = 0._r8 + allocate(this%nvp_omega_nir_col (begc:endc)) ; this%nvp_omega_nir_col (:) = 0._r8 + end if allocate(this%albsnd_hst_col (begc:endc,numrad)) ; this%albsnd_hst_col (:,:) = spval allocate(this%albsni_hst_col (begc:endc,numrad)) ; this%albsni_hst_col (:,:) = spval allocate(this%albsod_col (begc:endc,numrad)) ; this%albsod_col (:,:) = spval From a1955f93fc392700d454efe6aaf900394b217bed Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Fri, 10 Apr 2026 13:01:31 +0300 Subject: [PATCH 008/113] Add NVP radiation related variables into history output field. --- src/biogeophys/SurfaceAlbedoType.F90 | 43 ++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/src/biogeophys/SurfaceAlbedoType.F90 b/src/biogeophys/SurfaceAlbedoType.F90 index e23cd23a32..7b4a97b987 100644 --- a/src/biogeophys/SurfaceAlbedoType.F90 +++ b/src/biogeophys/SurfaceAlbedoType.F90 @@ -33,8 +33,8 @@ module SurfaceAlbedoType real(r8), pointer :: albgri_oc_col (:,:) ! col ground diffuse albedo without OC (numrad) real(r8), pointer :: albgrd_dst_col (:,:) ! col ground direct albedo without dust (numrad) real(r8), pointer :: albgri_dst_col (:,:) ! col ground diffuse albedo without dust (numrad) - real(r8), pointer :: albgrd_col (:,:) ! col ground albedo (direct) (numrad) - real(r8), pointer :: albgri_col (:,:) ! col ground albedo (diffuse) (numrad) + real(r8), pointer :: albgrd_col (:,:) ! col ground albedo (direct) (numrad) + real(r8), pointer :: albgri_col (:,:) ! col ground albedo (diffuse) (numrad) ! [PORTED by Hui Tang: nvp (moss/lichen) surface albedo fields] real(r8), pointer :: fabd_nvp_col (:,:) ! col flux absorbed by nvp per unit direct flux (numrad) real(r8), pointer :: fabi_nvp_col (:,:) ! col flux absorbed by nvp per unit diffuse flux (numrad) @@ -230,6 +230,7 @@ subroutine InitHistory(this, bounds) use shr_infnan_mod, only: nan => shr_infnan_nan, assignment(=) use clm_varcon , only: spval use histFileMod , only: hist_addfld1d, hist_addfld2d + use ColumnType , only: col ! [PORTED by Hui Tang: NVP structural history fields] ! ! !ARGUMENTS: class(surfalb_type) :: this @@ -271,6 +272,44 @@ subroutine InitHistory(this, bounds) avgflag='A', long_name='ground albedo (indirect)', & ptr_col=this%albgri_col, default='active') + ! [PORTED by Hui Tang: history fields for nvp (moss/lichen) surface absorbed flux] + if (use_nvp) then + this%fabd_nvp_col(begc:endc,:) = spval + call hist_addfld2d (fname='FABD_NVP', units='proportion', type2d='numrad', & + avgflag='A', long_name='flux absorbed by nvp per unit direct flux', & + ptr_col=this%fabd_nvp_col, default='active') + + this%fabi_nvp_col(begc:endc,:) = spval + call hist_addfld2d (fname='FABI_NVP', units='proportion', type2d='numrad', & + avgflag='A', long_name='flux absorbed by nvp per unit diffuse flux', & + ptr_col=this%fabi_nvp_col, default='active') + + ! [PORTED by Hui Tang: history fields for NVP optical properties and geometry] + this%nvp_tau_col(begc:endc) = spval + call hist_addfld1d (fname='NVP_TAU', units='none', & + avgflag='A', long_name='nvp (moss/lichen) optical depth (k*LAI*frac)', & + ptr_col=this%nvp_tau_col, default='inactive') + + this%nvp_omega_vis_col(begc:endc) = spval + call hist_addfld1d (fname='NVP_OMEGA_VIS', units='none', & + avgflag='A', long_name='nvp (moss/lichen) single-scatter albedo VIS band', & + ptr_col=this%nvp_omega_vis_col, default='inactive') + + this%nvp_omega_nir_col(begc:endc) = spval + call hist_addfld1d (fname='NVP_OMEGA_NIR', units='none', & + avgflag='A', long_name='nvp (moss/lichen) single-scatter albedo NIR band', & + ptr_col=this%nvp_omega_nir_col, default='inactive') + + ! Note: col%dz_nvp and col%frac_nvp initialized to 0._r8 in ColumnType; no spval pre-set needed + call hist_addfld1d (fname='DZ_NVP', units='m', & + avgflag='A', long_name='nvp (moss/lichen) column-effective layer thickness', & + ptr_col=col%dz_nvp, default='inactive') + + call hist_addfld1d (fname='FRAC_NVP', units='1', & + avgflag='A', long_name='nvp (moss/lichen) column fractional coverage', & + ptr_col=col%frac_nvp, default='inactive') + end if + if (use_SSRE) then this%albdSF_patch(begp:endp,:) = spval call hist_addfld2d (fname='ALBDSF', units='proportion', type2d='numrad', & From e259d4bcee241fb943deb704df5865475bccc2a7 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Fri, 10 Apr 2026 14:05:44 +0300 Subject: [PATCH 009/113] Add NVP-enabled SNICAR_RT call. --- src/biogeophys/SurfaceAlbedoMod.F90 | 91 +++++++++++++++++++++-------- 1 file changed, 66 insertions(+), 25 deletions(-) diff --git a/src/biogeophys/SurfaceAlbedoMod.F90 b/src/biogeophys/SurfaceAlbedoMod.F90 index d8d71ae41d..80d3d01319 100644 --- a/src/biogeophys/SurfaceAlbedoMod.F90 +++ b/src/biogeophys/SurfaceAlbedoMod.F90 @@ -15,6 +15,8 @@ module SurfaceAlbedoMod use clm_varcon , only : grlnd, spval use clm_varpar , only : numrad, nlevcan, nlevsno, nlevcan use clm_varctl , only : fsurdat, iulog, use_SSRE, do_sno_oc + ! [PORTED by Hui Tang: nvp (moss/lichen) control switch] + use clm_varctl , only : use_nvp use pftconMod , only : pftcon use SnowSnicarMod , only : sno_nbr_aer, SNICAR_RT, DO_SNO_AER use AerosolMod , only : aerosol_type @@ -778,32 +780,71 @@ subroutine SurfaceAlbedo(bounds,nc, & ! CLIMATE FEEDBACK CALCULATIONS, ALL AEROSOLS: flg_slr = 1; ! direct-beam - call SNICAR_RT(bounds, num_nourbanc, filter_nourbanc, & - coszen_col(bounds%begc:bounds%endc), & - flg_slr, & - h2osno_liq(bounds%begc:bounds%endc, :), & - h2osno_ice(bounds%begc:bounds%endc, :), & - h2osno_total(bounds%begc:bounds%endc), & - snw_rds_in(bounds%begc:bounds%endc, :), & - mss_cnc_aer_in_fdb(bounds%begc:bounds%endc, :, :), & - albsfc(bounds%begc:bounds%endc, :), & - albsnd(bounds%begc:bounds%endc, :), & - flx_absd_snw(bounds%begc:bounds%endc, :, :), & - waterdiagnosticbulk_inst) + ! [PORTED by Hui Tang: SNICAR Approach B - pass NVP layer-0 optical properties to feedback calls] + ! nvp_tau_col/omega_*_col are from the previous timestep (one-timestep lag, consistent with + ! other doalb quantities). Non-feedback forcing calls omit these args (NVP not in diagnostics). + if (use_nvp) then + call SNICAR_RT(bounds, num_nourbanc, filter_nourbanc, & + coszen_col(bounds%begc:bounds%endc), & + flg_slr, & + h2osno_liq(bounds%begc:bounds%endc, :), & + h2osno_ice(bounds%begc:bounds%endc, :), & + h2osno_total(bounds%begc:bounds%endc), & + snw_rds_in(bounds%begc:bounds%endc, :), & + mss_cnc_aer_in_fdb(bounds%begc:bounds%endc, :, :), & + albsfc(bounds%begc:bounds%endc, :), & + albsnd(bounds%begc:bounds%endc, :), & + flx_absd_snw(bounds%begc:bounds%endc, :, :), & + waterdiagnosticbulk_inst, & + nvp_tau_col = surfalb_inst%nvp_tau_col(bounds%begc:bounds%endc), & + nvp_omega_vis_col = surfalb_inst%nvp_omega_vis_col(bounds%begc:bounds%endc), & + nvp_omega_nir_col = surfalb_inst%nvp_omega_nir_col(bounds%begc:bounds%endc)) + else + call SNICAR_RT(bounds, num_nourbanc, filter_nourbanc, & + coszen_col(bounds%begc:bounds%endc), & + flg_slr, & + h2osno_liq(bounds%begc:bounds%endc, :), & + h2osno_ice(bounds%begc:bounds%endc, :), & + h2osno_total(bounds%begc:bounds%endc), & + snw_rds_in(bounds%begc:bounds%endc, :), & + mss_cnc_aer_in_fdb(bounds%begc:bounds%endc, :, :), & + albsfc(bounds%begc:bounds%endc, :), & + albsnd(bounds%begc:bounds%endc, :), & + flx_absd_snw(bounds%begc:bounds%endc, :, :), & + waterdiagnosticbulk_inst) + end if flg_slr = 2; ! diffuse - call SNICAR_RT(bounds, num_nourbanc, filter_nourbanc, & - coszen_col(bounds%begc:bounds%endc), & - flg_slr, & - h2osno_liq(bounds%begc:bounds%endc, :), & - h2osno_ice(bounds%begc:bounds%endc, :), & - h2osno_total(bounds%begc:bounds%endc), & - snw_rds_in(bounds%begc:bounds%endc, :), & - mss_cnc_aer_in_fdb(bounds%begc:bounds%endc, :, :), & - albsfc(bounds%begc:bounds%endc, :), & - albsni(bounds%begc:bounds%endc, :), & - flx_absi_snw(bounds%begc:bounds%endc, :, :), & - waterdiagnosticbulk_inst) + if (use_nvp) then + call SNICAR_RT(bounds, num_nourbanc, filter_nourbanc, & + coszen_col(bounds%begc:bounds%endc), & + flg_slr, & + h2osno_liq(bounds%begc:bounds%endc, :), & + h2osno_ice(bounds%begc:bounds%endc, :), & + h2osno_total(bounds%begc:bounds%endc), & + snw_rds_in(bounds%begc:bounds%endc, :), & + mss_cnc_aer_in_fdb(bounds%begc:bounds%endc, :, :), & + albsfc(bounds%begc:bounds%endc, :), & + albsni(bounds%begc:bounds%endc, :), & + flx_absi_snw(bounds%begc:bounds%endc, :, :), & + waterdiagnosticbulk_inst, & + nvp_tau_col = surfalb_inst%nvp_tau_col(bounds%begc:bounds%endc), & + nvp_omega_vis_col = surfalb_inst%nvp_omega_vis_col(bounds%begc:bounds%endc), & + nvp_omega_nir_col = surfalb_inst%nvp_omega_nir_col(bounds%begc:bounds%endc)) + else + call SNICAR_RT(bounds, num_nourbanc, filter_nourbanc, & + coszen_col(bounds%begc:bounds%endc), & + flg_slr, & + h2osno_liq(bounds%begc:bounds%endc, :), & + h2osno_ice(bounds%begc:bounds%endc, :), & + h2osno_total(bounds%begc:bounds%endc), & + snw_rds_in(bounds%begc:bounds%endc, :), & + mss_cnc_aer_in_fdb(bounds%begc:bounds%endc, :, :), & + albsfc(bounds%begc:bounds%endc, :), & + albsni(bounds%begc:bounds%endc, :), & + flx_absi_snw(bounds%begc:bounds%endc, :, :), & + waterdiagnosticbulk_inst) + end if ! ground albedos and snow-fraction weighting of snow absorption factors do ib = 1, nband @@ -1065,7 +1106,7 @@ subroutine SurfaceAlbedo(bounds,nc, & ! Only perform on vegetated pfts where coszen > 0 if (use_fates) then - + call clm_fates%wrap_canopy_radiation(bounds, nc, fcansno(bounds%begp:bounds%endp), surfalb_inst) else From 80ca26e05159dfea79beac66c1bafde75a780bcd Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Fri, 10 Apr 2026 14:09:56 +0300 Subject: [PATCH 010/113] Add NVP colum filter which can be used for NVP column loops. --- src/main/filterMod.F90 | 57 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/main/filterMod.F90 b/src/main/filterMod.F90 index 6540021923..c4e506d790 100644 --- a/src/main/filterMod.F90 +++ b/src/main/filterMod.F90 @@ -112,6 +112,10 @@ module filterMod integer, pointer :: actfirep(:) ! soil patches with active fire filter (patches) integer :: num_actfirep ! number of patches in active fire filter + ! [PORTED by Hui Tang: NVP-active column filter] + integer, pointer :: nvpc(:) ! columns where NVP layer is active (nvp_layer_active=.true.) + integer :: num_nvpc ! number of NVP-active columns + end type clumpfilter public clumpfilter @@ -139,6 +143,7 @@ module filterMod public allocFilters ! allocate memory for filters public setFilters ! set filters public setExposedvegpFilter ! set the exposedvegp and noexposedvegp filters + public setNVPcFilter ! [PORTED by Hui Tang: set the nvpc NVP-active column filter] private allocFiltersOneGroup ! allocate memory for one group of filters private setFiltersOneGroup ! set one group of filters @@ -257,6 +262,10 @@ subroutine allocFiltersOneGroup(this_filter) this_filter(nc)%num_actfirep = 1 this_filter(nc)%num_actfirec = 1 + + ! [PORTED by Hui Tang: NVP column filter — starts empty, filled by setNVPcFilter] + allocate(this_filter(nc)%nvpc(bounds%endc-bounds%begc+1)) + this_filter(nc)%num_nvpc = 0 end do !$OMP END PARALLEL DO @@ -648,4 +657,52 @@ subroutine setExposedvegpFilter(bounds, frac_veg_nosno) end subroutine setExposedvegpFilter + !----------------------------------------------------------------------- + subroutine setNVPcFilter(bounds) + ! + ! !DESCRIPTION: + ! [PORTED by Hui Tang: build the NVP-active column filter] + ! + ! Populates filter%nvpc with columns where col%nvp_layer_active is .true. + ! The filter is a sub-set of nolakec. When use_nvp is .false. or no column + ! has NVP active, num_nvpc = 0 and no NVP-specific loops execute. + ! + ! Call from clm_driver after dynamics_driv (where UpdateNVPLayer runs). + ! + ! !USES: + use decompMod , only : bounds_level_clump + use clm_varctl, only : use_nvp + ! + ! !ARGUMENTS: + type(bounds_type), intent(in) :: bounds + ! + ! !LOCAL VARIABLES: + integer :: nc ! clump index + integer :: fc ! filter index into nolakec + integer :: c ! column index + integer :: f ! count for nvpc filter + + character(len=*), parameter :: subname = 'setNVPcFilter' + !----------------------------------------------------------------------- + + SHR_ASSERT_FL(bounds%level == bounds_level_clump, sourcefile, __LINE__) + + nc = bounds%clump_index + f = 0 + + if (use_nvp) then + do fc = 1, filter(nc)%num_nolakec + c = filter(nc)%nolakec(fc) + if (col%nvp_layer_active(c)) then + f = f + 1 + filter(nc)%nvpc(f) = c + end if + end do + end if + + filter(nc)%num_nvpc = f + + end subroutine setNVPcFilter + + end module filterMod From 78ef7a0ce71c0d8798a38935ecab46018ce0fdad Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Fri, 10 Apr 2026 14:11:42 +0300 Subject: [PATCH 011/113] Add NVP namelist settings (temporary) --- src/main/controlMod.F90 | 58 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/src/main/controlMod.F90 b/src/main/controlMod.F90 index 082e3bb710..68cd7aad22 100644 --- a/src/main/controlMod.F90 +++ b/src/main/controlMod.F90 @@ -51,6 +51,8 @@ module controlMod use CanopyFluxesMod , only: CanopyFluxesReadNML use shr_drydep_mod , only: n_drydep use clm_varctl + ! [PORTED by Hui Tang: NVP parameter namelist module] + use NVPParamsMod ! ! !PUBLIC TYPES: implicit none @@ -267,6 +269,15 @@ subroutine control_init(dtime) ! CLM 5.0 nitrogen flags namelist /clm_inparm/ use_flexibleCN, use_luna + ! [PORTED by Hui Tang: nvp (moss/lichen) namelist flags] + namelist /clm_inparm/ use_nvp, use_nvp_undersnow, nvp_rad_model_ground + + ! [PORTED by Hui Tang: nvp physics parameter namelist] + namelist /nvp_inparm/ & + nvp_frac_min, rnvp_min, rnvp_amp, rnvp_exp, & + ksat_nvp, n_van_nvp, alpha_van_nvp, watsat_nvp, watres_nvp, & + thk_dry_nvp, csol_nvp + namelist /clm_nitrogen/ MM_Nuptake_opt, & CNratio_floating, lnc_opt, reduce_dayl_factor, vcmax_opt, & CN_evergreen_phenology_opt, carbon_resp_opt @@ -390,6 +401,16 @@ subroutine control_init(dtime) call endrun(msg='ERROR finding clm_nitrogen namelist'//errMsg(sourcefile, __LINE__)) end if + ! [PORTED by Hui Tang: read nvp physics parameter namelist (optional)] + rewind(unitn) + call shr_nl_find_group_name(unitn, 'nvp_inparm', status=ierr) + if (ierr == 0) then + read(unitn, nvp_inparm, iostat=ierr) + if (ierr /= 0) then + call endrun(msg='ERROR reading nvp_inparm namelist'//errMsg(sourcefile, __LINE__)) + end if + end if + call relavu( unitn ) ! ---------------------------------------------------------------------- @@ -865,6 +886,24 @@ subroutine control_spmd() call mpi_bcast (use_luna, 1, MPI_LOGICAL, 0, mpicom, ier) + ! [PORTED by Hui Tang: broadcast nvp (moss/lichen) flags] + call mpi_bcast (use_nvp, 1, MPI_LOGICAL, 0, mpicom, ier) + call mpi_bcast (use_nvp_undersnow, 1, MPI_LOGICAL, 0, mpicom, ier) + call mpi_bcast (nvp_rad_model_ground, 1, MPI_LOGICAL, 0, mpicom, ier) + + ! [PORTED by Hui Tang: broadcast nvp physics parameters] + call mpi_bcast (nvp_frac_min, 1, MPI_REAL8, 0, mpicom, ier) + call mpi_bcast (rnvp_min, 1, MPI_REAL8, 0, mpicom, ier) + call mpi_bcast (rnvp_amp, 1, MPI_REAL8, 0, mpicom, ier) + call mpi_bcast (rnvp_exp, 1, MPI_REAL8, 0, mpicom, ier) + call mpi_bcast (ksat_nvp, 1, MPI_REAL8, 0, mpicom, ier) + call mpi_bcast (n_van_nvp, 1, MPI_REAL8, 0, mpicom, ier) + call mpi_bcast (alpha_van_nvp, 1, MPI_REAL8, 0, mpicom, ier) + call mpi_bcast (watsat_nvp, 1, MPI_REAL8, 0, mpicom, ier) + call mpi_bcast (watres_nvp, 1, MPI_REAL8, 0, mpicom, ier) + call mpi_bcast (thk_dry_nvp, 1, MPI_REAL8, 0, mpicom, ier) + call mpi_bcast (csol_nvp, 1, MPI_REAL8, 0, mpicom, ier) + call mpi_bcast (use_soil_moisture_streams, 1, MPI_LOGICAL, 0, mpicom, ier) call mpi_bcast (use_excess_ice, 1, MPI_LOGICAL, 0, mpicom,ier) @@ -1230,6 +1269,25 @@ subroutine control_print () end if write(iulog, *) ' use_luna = ', use_luna + ! [PORTED by Hui Tang: log nvp (moss/lichen) settings] + write(iulog, *) ' use_nvp = ', use_nvp + if (use_nvp) then + write(iulog, *) ' use_nvp_undersnow = ', use_nvp_undersnow + write(iulog, *) ' nvp_rad_model_ground = ', nvp_rad_model_ground + write(iulog, *) ' NVP physics parameters:' + write(iulog, *) ' nvp_frac_min = ', nvp_frac_min + write(iulog, *) ' rnvp_min = ', rnvp_min + write(iulog, *) ' rnvp_amp = ', rnvp_amp + write(iulog, *) ' rnvp_exp = ', rnvp_exp + write(iulog, *) ' ksat_nvp = ', ksat_nvp + write(iulog, *) ' n_van_nvp = ', n_van_nvp + write(iulog, *) ' alpha_van_nvp = ', alpha_van_nvp + write(iulog, *) ' watsat_nvp = ', watsat_nvp + write(iulog, *) ' watres_nvp = ', watres_nvp + write(iulog, *) ' thk_dry_nvp = ', thk_dry_nvp + write(iulog, *) ' csol_nvp = ', csol_nvp + end if + write(iulog, *) ' ED/FATES: ' write(iulog, *) ' use_fates = ', use_fates if (use_fates) then From 79cbdedd88afbc8412a9f7e0a98ff93592070088 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Fri, 10 Apr 2026 15:09:09 +0300 Subject: [PATCH 012/113] Allow NVP optical properties to be ported into bottom snow layer (layer 0). --- src/biogeophys/SnowSnicarMod.F90 | 70 +++++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 10 deletions(-) diff --git a/src/biogeophys/SnowSnicarMod.F90 b/src/biogeophys/SnowSnicarMod.F90 index f6d41bd6a0..8cb6c2bdfa 100644 --- a/src/biogeophys/SnowSnicarMod.F90 +++ b/src/biogeophys/SnowSnicarMod.F90 @@ -242,6 +242,13 @@ subroutine SNICAR_RT (bounds, num_nourbanc, filter_nourbanc, & real(r8) , intent(out) :: albout ( bounds%begc: , 1: ) ! snow albedo, averaged into 2 bands (=0 if no sun or no snow) (col,bnd) [frc] real(r8) , intent(out) :: flx_abs ( bounds%begc: , -nlevsno+1: , 1: ) ! absorbed flux in each layer per unit flux incident (col, lyr, bnd) type(waterdiagnosticbulk_type) , intent(in) :: waterdiagnosticbulk_inst + ! [PORTED by Hui Tang: optional NVP layer-0 inputs for SNICAR Approach B] + ! When present and nvp_tau_col(c)>0 with explicit snow layers, NVP is inserted as layer 0 + ! below all snow layers. SNICAR then computes SNOW→NVP→SOIL radiative transfer in one pass. + ! snl_btm is shifted from 0 to -1 so that real snow occupies -1..snl_top and NVP sits at 0. + real(r8), optional, intent(in) :: nvp_tau_col ( bounds%begc: ) ! col-mean NVP optical depth (k*LAI*frac) [-] + real(r8), optional, intent(in) :: nvp_omega_vis_col ( bounds%begc: ) ! col NVP single-scatter albedo, VIS [-] + real(r8), optional, intent(in) :: nvp_omega_nir_col ( bounds%begc: ) ! col NVP single-scatter albedo, NIR [-] ! ! !LOCAL VARIABLES: ! @@ -302,8 +309,12 @@ subroutine SNICAR_RT (bounds, num_nourbanc, filter_nourbanc, & integer :: bnd_idx ! spectral band index (1 <= bnd_idx <= snicar_numrad_snw) [idx] integer :: rds_idx ! snow effective radius index for retrieving ! Mie parameters from lookup table [idx] - integer :: snl_btm ! index of bottom snow layer (0) [idx] + integer :: snl_btm ! index of bottom snow layer (0, or -1 when NVP present) [idx] integer :: snl_top ! index of top snow layer (-4 to 0) [idx] + ! [PORTED by Hui Tang: NVP layer-0 SNICAR locals] + logical :: nvp_active ! .true. if NVP optional args present and tau>0 + real(r8) :: nvp_tau_lcl ! local NVP optical depth for current column + real(r8) :: nvp_omega_lcl ! local NVP single-scatter albedo for current band integer :: fc ! column filter index integer :: i ! layer index [idx] integer :: j ! aerosol number index [idx] @@ -690,6 +701,26 @@ subroutine SNICAR_RT (bounds, num_nourbanc, filter_nourbanc, & snl_btm = 0 snl_top = snl_lcl+1 + ! [PORTED by Hui Tang: NVP layer-0 SNICAR Approach B] + ! When NVP optional args are supplied, NVP tau > 0, and real snow layers exist + ! (flg_nosnl==0), insert NVP as layer 0 by shifting snl_btm to -1. + ! Snow occupies snl_top..-1; NVP occupies layer 0; soil remains at interface 1. + nvp_active = .false. + nvp_tau_lcl = 0._r8 + if (present(nvp_tau_col) .and. flg_nosnl == 0) then + if (nvp_tau_col(c_idx) > 0._r8) then + nvp_active = .true. + nvp_tau_lcl = nvp_tau_col(c_idx) + snl_btm = -1 + ! Populate layer-0 local arrays for NVP (bypass snow grain-radius path): + ! Unit effective mass so tau_snw(0) = ext_cff_mss_snw_lcl(0) = nvp_tau_lcl + h2osno_ice_lcl(0) = 1._r8 + h2osno_liq_lcl(0) = 0._r8 + ! Zero aerosols in NVP layer (no snow impurities) + mss_cnc_aer_lcl(0,:) = 0._r8 + end if + end if + ! for debugging only l_idx = col%landunit(c_idx) g_idx = col%gridcell(c_idx) @@ -698,19 +729,22 @@ subroutine SNICAR_RT (bounds, num_nourbanc, filter_nourbanc, & lon_coord = grc%londeg(g_idx) - ! Set local aerosol array + ! Set local aerosol array (snow layers only; NVP layer 0 already zeroed above) do j=1,sno_nbr_aer - mss_cnc_aer_lcl(:,j) = mss_cnc_aer_in(c_idx,:,j) + mss_cnc_aer_lcl(snl_top:min(snl_btm,0)-1,j) = mss_cnc_aer_in(c_idx,snl_top:min(snl_btm,0)-1,j) + if (.not. nvp_active) then + mss_cnc_aer_lcl(0,j) = mss_cnc_aer_in(c_idx,0,j) + end if enddo ! Set spectral underlying surface albedos to their corresponding VIS or NIR albedos albsfc_lcl(1:(nir_bnd_bgn-1)) = albsfc(c_idx,ivis) albsfc_lcl(nir_bnd_bgn:nir_bnd_end) = albsfc(c_idx,inir) - - ! Error check for snow grain size: - do i=snl_top,snl_btm,1 + + ! Error check for snow grain size (skip layer 0 when NVP is active there): + do i=snl_top, merge(-1, snl_btm, nvp_active), 1 if ((snw_rds_lcl(i) < snw_rds_min_tbl) .or. (snw_rds_lcl(i) > snw_rds_max_tbl)) then write (iulog,*) "SNICAR ERROR: snow grain radius of ", snw_rds_lcl(i), " out of bounds." write (iulog,*) "NSTEP= ", nstep @@ -788,9 +822,25 @@ subroutine SNICAR_RT (bounds, num_nourbanc, filter_nourbanc, & !--------------------------- Start snow & aerosol optics -------------------------------- ! Define local Mie parameters based on snow grain size and aerosol species retrieved from a lookup table. + ! [PORTED by Hui Tang: NVP layer-0 SNICAR Approach B - set omega for current band] + ! NVP properties bypass the grain-radius Mie tables entirely. + if (nvp_active) then + if (bnd_idx < nir_bnd_bgn) then + nvp_omega_lcl = nvp_omega_vis_col(c_idx) + else + nvp_omega_lcl = nvp_omega_nir_col(c_idx) + end if + ! Layer 0: NVP pseudo-layer optical properties + ! ext_cff_mss = tau_nvp (since h2osno_ice_lcl(0)=1, so L_snw*ext=tau_nvp) + ss_alb_snw_lcl(0) = nvp_omega_lcl + ext_cff_mss_snw_lcl(0) = nvp_tau_lcl + asm_prm_snw_lcl(0) = 0._r8 ! isotropic scattering (flat mat, no preferred direction) + end if + ! Spherical snow: single-scatter albedo, mass extinction coefficient, asymmetry factor + ! (snow layers only; layer 0 handled above when NVP active) if (flg_slr_in == 1) then - do i=snl_top,snl_btm,1 + do i=snl_top, merge(-1, snl_btm, nvp_active), 1 rds_idx = snw_rds_lcl(i) - snw_rds_min_tbl + 1 ! snow optical properties (direct radiation) ss_alb_snw_lcl(i) = ss_alb_snw_drc(rds_idx,bnd_idx) @@ -798,7 +848,7 @@ subroutine SNICAR_RT (bounds, num_nourbanc, filter_nourbanc, & if (sno_shp(i) == 'sphere') asm_prm_snw_lcl(i) = asm_prm_snw_drc(rds_idx,bnd_idx) enddo elseif (flg_slr_in == 2) then - do i=snl_top,snl_btm,1 + do i=snl_top, merge(-1, snl_btm, nvp_active), 1 rds_idx = snw_rds_lcl(i) - snw_rds_min_tbl + 1 ! snow optical properties (diffuse radiation) ss_alb_snw_lcl(i) = ss_alb_snw_dfs(rds_idx,bnd_idx) @@ -807,8 +857,8 @@ subroutine SNICAR_RT (bounds, num_nourbanc, filter_nourbanc, & enddo endif - ! Nonspherical snow: shape-dependent asymmetry factors - do i=snl_top,snl_btm,1 + ! Nonspherical snow: shape-dependent asymmetry factors (snow layers only) + do i=snl_top, merge(-1, snl_btm, nvp_active), 1 select case (sno_shp(i)) case ('spheroid') From 64bf7b161ac8c2c9754f611fe4bb526353d67a65 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Fri, 10 Apr 2026 19:48:47 +0300 Subject: [PATCH 013/113] Add NVP light absorption for both snow and no-snow case --- src/biogeophys/SurfaceRadiationMod.F90 | 33 ++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/biogeophys/SurfaceRadiationMod.F90 b/src/biogeophys/SurfaceRadiationMod.F90 index 5de3ba6e09..91b8ee07d2 100644 --- a/src/biogeophys/SurfaceRadiationMod.F90 +++ b/src/biogeophys/SurfaceRadiationMod.F90 @@ -8,6 +8,8 @@ module SurfaceRadiationMod use shr_kind_mod , only : r8 => shr_kind_r8 use shr_log_mod , only : errMsg => shr_log_errMsg use clm_varctl , only : use_snicar_frc, use_fates + ! [PORTED by Hui Tang: nvp (moss/lichen) control switches for radiation] + use clm_varctl , only : use_nvp use decompMod , only : bounds_type, subgrid_level_column use atm2lndType , only : atm2lnd_type use WaterDiagnosticBulkType , only : waterdiagnosticbulk_type @@ -758,6 +760,29 @@ subroutine SurfaceRadiation(bounds, num_nourbanp, filter_nourbanp, & sabg_lyr(p,:) = 0._r8 sabg_lyr(p,1) = sabg(p) sabg_snl_sum = sabg_lyr(p,1) + ! [PORTED by Hui Tang: no-snow - NVP layer (index 0) absorbs before soil] + ! fabd_nvp_col/fabi_nvp_col are Beer's law absorptance fractions (dimensionless, + ! per unit flux incident on NVP). trd/tri are below-canopy direct/diffuse fluxes. + ! sabg(p) is unchanged (ground total = NVP + soil via modified albedo). + ! sabg_soil is corrected because it was computed using soil-only albedo (albsod). + if (use_nvp) then + sabg_lyr(p,0) = 0._r8 + do ib = 1, nband + sabg_lyr(p,0) = sabg_lyr(p,0) + & + surfalb_inst%fabd_nvp_col(c,ib) * trd(p,ib) + & + surfalb_inst%fabi_nvp_col(c,ib) * tri(p,ib) + end do + sabg_lyr(p,0) = max(0._r8, min(sabg_lyr(p,0), sabg_lyr(p,1))) + sabg_lyr(p,1) = sabg_lyr(p,1) - sabg_lyr(p,0) + sabg_soil(p) = sabg_soil(p) - sabg_lyr(p,0) + ! [PORTED by Hui Tang: no-snow - store VIS canopy transmittances for NVP photosynthesis PAR] + ! ftdd(p,1) and ftii(p,1) are dimensionless fractions of direct/diffuse VIS + ! reaching the ground (below vascular canopy). Stored in layer 0 of the SNICAR + ! flx_abs arrays (unused by SNICAR when snl==0). Retrieved in wrap_sunfrac + ! at the next timestep for bc_in%flx_absdv/flx_absiv (one-timestep lag). + flx_absdv(c,:) = surfalb_inst%fabd_nvp_col(c,:) + flx_absiv(c,:) = surfalb_inst%fabi_nvp_col(c,:) + end if ! CASE 2: Snow layers present: absorbed radiation is scaled according to ! flux factors computed by SNICAR @@ -774,6 +799,14 @@ subroutine SurfaceRadiation(bounds, num_nourbanp, filter_nourbanp, & sub_surf_abs_SW(p) = sub_surf_abs_SW(p) + sabg_lyr(p,i) endif enddo + ! [PORTED by Hui Tang: snow - NVP layer-0 SNICAR] + ! When use_nvp and SNICAR NVP layer-0 is active, flx_absdv(c,0)/flx_absiv(c,0) + ! already hold NVP absorption (set by SNICAR_RT above), so sabg_lyr(p,0) is + ! correct from the SNICAR loop above. Correct sabg_soil to use SNICAR soil layer. + if (use_nvp .and. surfalb_inst%nvp_tau_col(c) > 0._r8) then + ! sabg_lyr(p,1) = SNICAR soil-layer absorption (excludes NVP); use it directly. + sabg_soil(p) = sabg_lyr(p,1) + end if ! Divide absorbed by total, to get fraction absorbed in subsurface if (sabg_snl_sum /= 0._r8) then From 299017241030f3899368e40dc38b15e6b0798069 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Fri, 10 Apr 2026 20:04:36 +0300 Subject: [PATCH 014/113] Add NVP radiation wapper for FATES --- src/utils/clmfates_interfaceMod.F90 | 63 ++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/src/utils/clmfates_interfaceMod.F90 b/src/utils/clmfates_interfaceMod.F90 index c9e2e16a0f..bd3b2df436 100644 --- a/src/utils/clmfates_interfaceMod.F90 +++ b/src/utils/clmfates_interfaceMod.F90 @@ -137,6 +137,7 @@ module CLMFatesInterfaceMod ! Used FATES Modules use FatesInterfaceMod , only : fates_interface_type + use EDParamsMod , only : nvp_extinction_coeff ! [PORTED by Hui Tang: NVP Beer's law k from parameter file] use FatesInterfaceMod, only : FatesInterfaceInit use FatesInterfaceMod, only : SetFatesGlobalElements1 use FatesInterfaceMod, only : SetFatesGlobalElements2 @@ -2340,7 +2341,8 @@ end subroutine init_coldstart ! ====================================================================================== - subroutine wrap_sunfrac(this,nc,atm2lnd_inst,canopystate_inst) + ! [PORTED by Hui Tang: add surfalb_inst to wrap_sunfrac for nvp radiation] + subroutine wrap_sunfrac(this,nc,atm2lnd_inst,canopystate_inst,surfalb_inst) ! --------------------------------------------------------------------------------- ! This interface function is a wrapper call on ED_SunShadeFracs. The only @@ -2359,6 +2361,9 @@ subroutine wrap_sunfrac(this,nc,atm2lnd_inst,canopystate_inst) ! Input/Output Arguments to CLM type(canopystate_type),intent(inout) :: canopystate_inst + ! [PORTED by Hui Tang: surface albedo for absorbed flux bc_in - needed by nvp radiation] + type(surfalb_type),intent(in) :: surfalb_inst + ! Local Variables integer :: p ! global index of the host patch integer :: g ! global index of the host gridcell @@ -2392,6 +2397,13 @@ subroutine wrap_sunfrac(this,nc,atm2lnd_inst,canopystate_inst) do ifp = 1, this%fates(nc)%sites(s)%youngest_patch%patchno this%fates(nc)%bc_in(s)%solad_parb(ifp,:) = forc_solad_g(g,:) this%fates(nc)%bc_in(s)%solai_parb(ifp,:) = forc_solai_g(g,:) + ! [PORTED by Hui Tang: pass VIS canopy transmittances for NVP photosynthesis PAR] + ! flx_absdv_col(c,0) holds ftdd(p,1) (?) and flx_absiv_col(c,0) holds ftii(p,1) (?), + ! stored in SurfaceRadiationMod at the previous timestep (one-timestep lag). + if (use_nvp) then + this%fates(nc)%bc_in(s)%flx_absdv(ifp) = surfalb_inst%flx_absdv_col(c,0) + this%fates(nc)%bc_in(s)%flx_absiv(ifp) = surfalb_inst%flx_absiv_col(c,0) + end if end do end do @@ -2884,6 +2896,10 @@ subroutine wrap_canopy_radiation(this, bounds_clump, nc, fcansno, surfalb_inst) ! locals integer :: s,c,p,ifp,g + ! [PORTED by Hui Tang: NVP absorptance patch→col aggregation] + integer :: ib ! band index + integer :: npatches_site ! patch count in site + ! [PORTED by Hui Tang: NVP Beer's law k now read from fates_params_default.json via nvp_extinction_coeff] call t_startf('fates_wrapcanopyradiation') @@ -2956,6 +2972,51 @@ subroutine wrap_canopy_radiation(this, bounds_clump, nc, fcansno, surfalb_inst) ftii(p,:) = this%fates(nc)%bc_out(s)%ftii_parb(ifp,:) end do + + ! [PORTED by Hui Tang: transfer NVP Beer's law absorptance from bc_out to surfalb_inst] + ! fabd_nvp_col/fabi_nvp_col are col-level; average equally over all patches in the site. + ! These are used by SurfaceRadiationMod to compute sabg_lyr(p,0) for the NVP layer. + if (use_nvp) then + npatches_site = this%fates(nc)%sites(s)%youngest_patch%patchno + surfalb_inst%fabd_nvp_col(c,:) = 0._r8 + surfalb_inst%fabi_nvp_col(c,:) = 0._r8 + do ifp = 1, npatches_site + do ib = 1, numrad + surfalb_inst%fabd_nvp_col(c,ib) = surfalb_inst%fabd_nvp_col(c,ib) + & + this%fates(nc)%bc_out(s)%fabd_nvp_pa(ifp,ib) + surfalb_inst%fabi_nvp_col(c,ib) = surfalb_inst%fabi_nvp_col(c,ib) + & + this%fates(nc)%bc_out(s)%fabi_nvp_pa(ifp,ib) + end do + end do + if (npatches_site > 0) then + surfalb_inst%fabd_nvp_col(c,:) = & + surfalb_inst%fabd_nvp_col(c,:) / real(npatches_site, r8) + surfalb_inst%fabi_nvp_col(c,:) = & + surfalb_inst%fabi_nvp_col(c,:) / real(npatches_site, r8) + end if + ! [PORTED by Hui Tang: compute NVP optical properties for SNICAR layer-0 ] + ! nvp_tau_col = column-mean optical depth = k_nvp * lai_nvp_pa * nvp_frac_pa averaged over patches. + ! nvp_omega_*_col = NVP single-scatter albedo (rhol+taul); average over patches (intensive property). + ! Stored here for use by SurfaceAlbedoMod before SNICAR_RT calls (one-timestep lag, consistent). + surfalb_inst%nvp_tau_col(c) = 0._r8 + surfalb_inst%nvp_omega_vis_col(c) = 0._r8 + surfalb_inst%nvp_omega_nir_col(c) = 0._r8 + if (npatches_site > 0) then + do ifp = 1, npatches_site + surfalb_inst%nvp_tau_col(c) = surfalb_inst%nvp_tau_col(c) + & + nvp_extinction_coeff * this%fates(nc)%bc_out(s)%lai_nvp_pa(ifp) * & + this%fates(nc)%bc_out(s)%nvp_frac_pa(ifp) + surfalb_inst%nvp_omega_vis_col(c) = surfalb_inst%nvp_omega_vis_col(c) + & + this%fates(nc)%bc_out(s)%nvp_omega_pa(ifp, ivis) + surfalb_inst%nvp_omega_nir_col(c) = surfalb_inst%nvp_omega_nir_col(c) + & + this%fates(nc)%bc_out(s)%nvp_omega_pa(ifp, inir) + end do + surfalb_inst%nvp_tau_col(c) = surfalb_inst%nvp_tau_col(c) / real(npatches_site, r8) + surfalb_inst%nvp_omega_vis_col(c) = surfalb_inst%nvp_omega_vis_col(c) / real(npatches_site, r8) + surfalb_inst%nvp_omega_nir_col(c) = surfalb_inst%nvp_omega_nir_col(c) / real(npatches_site, r8) + end if + end if + end do end associate From a2cf476dfa7296087af5782a7625fe677bc8a869 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Fri, 10 Apr 2026 20:08:59 +0300 Subject: [PATCH 015/113] Add FATES wrapper for dynamic NVP layer --- src/utils/clmfates_interfaceMod.F90 | 41 ++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/src/utils/clmfates_interfaceMod.F90 b/src/utils/clmfates_interfaceMod.F90 index bd3b2df436..5098caf283 100644 --- a/src/utils/clmfates_interfaceMod.F90 +++ b/src/utils/clmfates_interfaceMod.F90 @@ -1398,7 +1398,9 @@ subroutine dynamics_driv(this, nc, bounds_clump, & waterdiagnosticbulk_inst, & canopystate_inst, & soilbiogeochem_carbonflux_inst, & - .false.) + .false., & + temperature_inst, & + waterstatebulk_inst) ! --------------------------------------------------------------------------------- ! Part IV: @@ -1569,7 +1571,8 @@ end subroutine UpdateCLitterFluxes subroutine wrap_update_hlmfates_dyn(this, nc, bounds_clump, & waterdiagnosticbulk_inst, canopystate_inst, & - soilbiogeochem_carbonflux_inst, is_initing_from_restart) + soilbiogeochem_carbonflux_inst, is_initing_from_restart, & + temperature_inst, waterstatebulk_inst) ! --------------------------------------------------------------------------------- ! This routine handles the updating of vegetation canopy diagnostics, (such as lai) @@ -1588,6 +1591,11 @@ subroutine wrap_update_hlmfates_dyn(this, nc, bounds_clump, & ! is this being called during a read from restart sequence (if so then use the restarted fates ! snow depth variable rather than the CLM variable). logical , intent(in) :: is_initing_from_restart + ! [PORTED by Hui Tang: optional args for NVP energy/mass conservation on activation/deactivation. + ! Pass during normal timestep calls; omit during restart/cold-start where thermo state is + ! initialised independently.] + type(temperature_type) , optional, intent(inout) :: temperature_inst + type(waterstatebulk_type), optional, intent(inout) :: waterstatebulk_inst integer :: npatch ! number of patches in each site integer :: ifp ! index FATES patch @@ -1783,7 +1791,34 @@ subroutine wrap_update_hlmfates_dyn(this, nc, bounds_clump, & z0m(p) = this%fates(nc)%bc_out(s)%z0m_pa(ifp) displa(p) = this%fates(nc)%bc_out(s)%displa_pa(ifp) dleaf_patch(p) = this%fates(nc)%bc_out(s)%dleaf_pa(ifp) - end do ! veg pach + end do ! veg patch + + ! [PORTED by Hui Tang: aggregate NVP patch geometry to column, then update layer state] + ! nvp_dz_pa(ifp) = mean NVP thickness where NVP is present within patch [m] + ! nvp_frac_pa(ifp) = fraction of patch covered by NVP [0-1] + ! Weight by canopy_fraction_pa (patch area fraction of column) to get column means. + if (use_nvp) then + col%dz_nvp(c) = 0._r8 + col%frac_nvp(c) = 0._r8 + do ifp = 1, npatch + ! [PORTED by Hui Tang: weight by both nvp_frac_pa (NVP coverage within patch) + ! and canopy_fraction_pa (patch area fraction of column) so col%dz_nvp is the + ! column-effective NVP depth (dz where present × frac), not the raw mean thickness] + col%dz_nvp(c) = col%dz_nvp(c) + & + this%fates(nc)%bc_out(s)%nvp_dz_pa(ifp) * & + this%fates(nc)%bc_out(s)%nvp_frac_pa(ifp) * & + this%fates(nc)%bc_out(s)%canopy_fraction_pa(ifp) + col%frac_nvp(c) = col%frac_nvp(c) + & + this%fates(nc)%bc_out(s)%nvp_frac_pa(ifp) * & + this%fates(nc)%bc_out(s)%canopy_fraction_pa(ifp) + end do + ! [PORTED by Hui Tang: pass thermo instances only when present (normal timestep)] + if (present(temperature_inst) .and. present(waterstatebulk_inst)) then + call UpdateNVPLayer(c, temperature_inst, waterstatebulk_inst) + else + call UpdateNVPLayer(c) + end if + end if if(abs(areacheck - 1.0_r8).gt.1.e-9_r8)then write(iulog,*) 'area wrong in interface',areacheck - 1.0_r8 From 84e0ed8ea165f6131defc8dcd44ec23e5864683c Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Fri, 10 Apr 2026 20:18:29 +0300 Subject: [PATCH 016/113] Add NVP temperature state variable t_nvp_col. --- src/biogeophys/TemperatureType.F90 | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/biogeophys/TemperatureType.F90 b/src/biogeophys/TemperatureType.F90 index 6fd8faf037..802070d5ec 100644 --- a/src/biogeophys/TemperatureType.F90 +++ b/src/biogeophys/TemperatureType.F90 @@ -7,7 +7,8 @@ module TemperatureType use shr_log_mod , only : errMsg => shr_log_errMsg use decompMod , only : bounds_type use abortutils , only : endrun - use clm_varctl , only : use_cndv, iulog, use_luna, use_crop, use_biomass_heat_storage + ! [PORTED by Hui Tang: add use_nvp for nvp (moss/lichen) temperature field] + use clm_varctl , only : use_cndv, iulog, use_luna, use_crop, use_biomass_heat_storage, use_nvp use clm_varctl , only : flush_gdd20 use clm_varpar , only : nlevsno, nlevgrnd, nlevlak, nlevurb, nlevmaxurbgrnd, nlevsoi use clm_varcon , only : spval, ispval @@ -34,6 +35,8 @@ module TemperatureType integer, pointer :: nnightsteps_patch (:) ! number of nighttime steps accumulated from mid-night, LUNA specific real(r8), pointer :: t_h2osfc_col (:) ! col surface water temperature real(r8), pointer :: t_h2osfc_bef_col (:) ! col surface water temperature from time-step before + ! [PORTED by Hui Tang: nvp (moss/lichen) column temperature] + real(r8), pointer :: t_nvp_col (:) ! col nvp (moss/lichen) temperature (Kelvin) real(r8), pointer :: t_ssbef_col (:,:) ! col soil/snow temperature before update (-nlevsno+1:nlevgrnd) real(r8), pointer :: t_soisno_col (:,:) ! col soil temperature (Kelvin) (-nlevsno+1:nlevgrnd) real(r8), pointer :: tsl_col (:) ! col temperature of near-surface soil layer (Kelvin) @@ -225,6 +228,8 @@ subroutine InitAllocate(this, bounds) endif allocate(this%t_h2osfc_col (begc:endc)) ; this%t_h2osfc_col (:) = nan allocate(this%t_h2osfc_bef_col (begc:endc)) ; this%t_h2osfc_bef_col (:) = nan + ! [PORTED by Hui Tang: allocate nvp (moss/lichen) column temperature] + allocate(this%t_nvp_col (begc:endc)) ; this%t_nvp_col (:) = nan allocate(this%t_ssbef_col (begc:endc,-nlevsno+1:nlevmaxurbgrnd)) ; this%t_ssbef_col (:,:) = nan allocate(this%t_soisno_col (begc:endc,-nlevsno+1:nlevmaxurbgrnd)) ; this%t_soisno_col (:,:) = nan allocate(this%t_lake_col (begc:endc,1:nlevlak)) ; this%t_lake_col (:,:) = nan @@ -665,6 +670,14 @@ subroutine InitHistory(this, bounds, is_simple_buildtemp, is_prog_buildtemp ) ptr_patch=this%t_veg10_night_patch, default='inactive') endif + ! [PORTED by Hui Tang: register nvp (moss/lichen) temperature history field] + if (use_nvp) then + this%t_nvp_col(begc:endc) = spval + call hist_addfld1d (fname='T_NVP', units='K', & + avgflag='A', long_name='nvp (moss/lichen) temperature', & + ptr_col=this%t_nvp_col, default='active') + end if + end subroutine InitHistory !----------------------------------------------------------------------- @@ -842,6 +855,9 @@ subroutine InitCold(this, bounds, & this%t_h2osfc_col(bounds%begc:bounds%endc) = 274._r8 + ! [PORTED by Hui Tang: initialize nvp (moss/lichen) column temperature to 274 K] + this%t_nvp_col(bounds%begc:bounds%endc) = 274._r8 + ! Set t_veg, t_ref2m, t_ref2m_u and tref2m_r do p = bounds%begp, bounds%endp @@ -1113,6 +1129,17 @@ subroutine Restart(this, bounds, ncid, flag, is_simple_buildtemp, is_prog_buildt interpinic_flag='interp', readvar=readvar, data=this%nnightsteps_patch ) endif + ! [PORTED by Hui Tang: restart I/O for nvp (moss/lichen) column temperature] + if (use_nvp) then + call restartvar(ncid=ncid, flag=flag, varname='T_NVP', xtype=ncd_double, & + dim1name='column', & + long_name='nvp (moss/lichen) temperature', units='K', & + interpinic_flag='interp', readvar=readvar, data=this%t_nvp_col) + if (flag=='read' .and. .not. readvar) then + this%t_nvp_col(bounds%begc:bounds%endc) = 274.0_r8 + end if + end if + if ( is_prog_buildtemp )then ! landunit type physical state variable - t_building call restartvar(ncid=ncid, flag=flag, varname='t_building', xtype=ncd_double, & From 0481b005123cac3e6a49d47049b9b5106486e820 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Fri, 10 Apr 2026 21:03:58 +0300 Subject: [PATCH 017/113] Accomdate NVP as layer 0 (snow layer) in soil-snow temperature integration --- src/biogeophys/SoilTemperatureMod.F90 | 229 ++++++++++++++++++++++---- 1 file changed, 198 insertions(+), 31 deletions(-) diff --git a/src/biogeophys/SoilTemperatureMod.F90 b/src/biogeophys/SoilTemperatureMod.F90 index 367da626e6..c66a283674 100644 --- a/src/biogeophys/SoilTemperatureMod.F90 +++ b/src/biogeophys/SoilTemperatureMod.F90 @@ -13,7 +13,8 @@ module SoilTemperatureMod use abortutils , only : endrun use shr_log_mod , only : errMsg => shr_log_errMsg use perf_mod , only : t_startf, t_stopf - use clm_varctl , only : iulog + ! [PORTED by Hui Tang: add use_nvp for nvp (moss/lichen) surface temperature update] + use clm_varctl , only : iulog, use_nvp use UrbanParamsType , only : urbanparams_type use UrbanTimeVarType , only : urbantv_type use atm2lndType , only : atm2lnd_type @@ -116,7 +117,8 @@ subroutine SoilTemperature(bounds, num_urbanl, filter_urbanl, num_urbanc, filter ! !USES: use clm_time_manager , only : get_step_size_real use clm_varpar , only : nlevsno, nlevgrnd, nlevurb, nlevmaxurbgrnd - use clm_varctl , only : iulog, use_excess_ice + ! [PORTED by Hui Tang: add use_nvp for nvp (moss/lichen) surface temperature update] + use clm_varctl , only : iulog, use_excess_ice, use_nvp use clm_varcon , only : cnfac, cpice, cpliq, denh2o, denice use landunit_varcon , only : istsoil, istcrop use column_varcon , only : icol_roof, icol_sunwall, icol_shadewall, icol_road_perv, icol_road_imperv @@ -130,6 +132,8 @@ subroutine SoilTemperature(bounds, num_urbanl, filter_urbanl, num_urbanc, filter integer , intent(in) :: filter_nolakep(:) ! patch filter for non-lake points integer , intent(in) :: num_nolakec ! number of column non-lake points in column filter integer , intent(in) :: filter_nolakec(:) ! column filter for non-lake points + integer , intent(in) :: num_nvpc ! [PORTED by Hui Tang: number of NVP-active columns] + integer , intent(in) :: filter_nvpc(:) ! [PORTED by Hui Tang: NVP-active column filter] integer , intent(in) :: num_urbanl ! number of urban landunits in clump integer , intent(in) :: filter_urbanl(:) ! urban landunit filter integer , intent(in) :: num_urbanc ! number of urban columns in clump @@ -173,8 +177,11 @@ subroutine SoilTemperature(bounds, num_urbanl, filter_urbanl, num_urbanc, filter real(r8) :: dhsdT(bounds%begc:bounds%endc) ! temperature derivative of "hs" [col] real(r8) :: hs_soil(bounds%begc:bounds%endc) ! heat flux on soil [W/m2] real(r8) :: hs_top_snow(bounds%begc:bounds%endc) ! heat flux on top snow layer [W/m2] + real(r8) :: hs_nvp(bounds%begc:bounds%endc) ! [PORTED by Hui Tang: surface heat flux at NVP layer 0] [W/m2] real(r8) :: hs_h2osfc(bounds%begc:bounds%endc) ! heat flux on standing water [W/m2] integer :: jbot(bounds%begc:bounds%endc) ! bottom level at each column + ! [PORTED by Hui Tang: NVP effective fraction for t_grnd blend] + real(r8) :: frac_nvp_eff ! NVP fraction not covered by snow or h2osfc [-] real(r8) :: dz_0(bounds%begc:bounds%endc,-nlevsno+1:nlevmaxurbgrnd) ! original layer thickness [m] real(r8) :: z_0(bounds%begc:bounds%endc,-nlevsno+1:nlevmaxurbgrnd) ! original layer depth [m] real(r8) :: zi_0(bounds%begc:bounds%endc,-nlevsno+0:nlevmaxurbgrnd) ! original layer interface level bellow layer "z" [m] @@ -237,11 +244,15 @@ subroutine SoilTemperature(bounds, num_urbanl, filter_urbanl, num_urbanc, filter eflx_urban_ac_col => energyflux_inst%eflx_urban_ac_col , & ! Output: [real(r8) (:) ] urban air conditioning flux (W/m**2) eflx_urban_heat_col => energyflux_inst%eflx_urban_heat_col , & ! Output: [real(r8) (:) ] urban heating flux (W/m**2) - emg => temperature_inst%emg_col , & ! Input: [real(r8) (:) ] ground emissivity - tssbef => temperature_inst%t_ssbef_col , & ! Input: [real(r8) (:,:) ] temperature at previous time step [K] - t_h2osfc => temperature_inst%t_h2osfc_col , & ! Output: [real(r8) (:) ] surface water temperature - t_soisno => temperature_inst%t_soisno_col , & ! Output: [real(r8) (:,:) ] soil temperature [K] - t_grnd => temperature_inst%t_grnd_col , & ! Output: [real(r8) (:) ] ground surface temperature [K] + emg => temperature_inst%emg_col , & ! Input: [real(r8) (:) ] ground emissivity + tssbef => temperature_inst%t_ssbef_col , & ! Input: [real(r8) (:,:) ] temperature at previous time step [K] + t_h2osfc => temperature_inst%t_h2osfc_col , & ! Output: [real(r8) (:) ] surface water temperature + t_soisno => temperature_inst%t_soisno_col , & ! Output: [real(r8) (:,:) ] soil temperature [K] + t_grnd => temperature_inst%t_grnd_col , & ! Output: [real(r8) (:) ] ground surface temperature [K] + ! [PORTED by Hui Tang: nvp (moss/lichen) surface temperature at layer 0] + t_nvp_col => temperature_inst%t_nvp_col , & ! Output: [real(r8) (:) ] nvp (moss/lichen) temperature [K] + ! [PORTED by Hui Tang: jbot_sno defines real bottom of snow (0=no NVP, -1=NVP at layer 0)] + jbot_sno => col%jbot_sno , & ! Input: [integer (:) ] bottom snow layer index (0 or -1) t_building => temperature_inst%t_building_lun , & ! Output: [real(r8) (:) ] internal building air temperature [K] t_roof_inner => temperature_inst%t_roof_inner_lun , & ! Input: [real(r8) (:) ] roof inside surface temperature [K] t_sunw_inner => temperature_inst%t_sunw_inner_lun , & ! Input: [real(r8) (:) ] sunwall inside surface temperature [K] @@ -271,6 +282,11 @@ subroutine SoilTemperature(bounds, num_urbanl, filter_urbanl, num_urbanc, filter do fc = 1,num_nolakec c = filter_nolakec(fc) jtop(c) = snl(c) + ! [PORTED by Hui Tang: when NVP occupies layer 0 and there is no snow, extend + ! jtop to -1 so the tridiagonal solver includes the NVP layer at tvector(c,-1)] + if (col%nvp_layer_active(c) .and. snl(c) == 0) then + jtop(c) = -1 + end if ! compute jbot if ((col%itype(c) == icol_sunwall .or. col%itype(c) == icol_shadewall & .or. col%itype(c) == icol_roof) ) then @@ -322,10 +338,12 @@ subroutine SoilTemperature(bounds, num_urbanl, filter_urbanl, num_urbanc, filter call ComputeGroundHeatFluxAndDeriv(bounds, & num_nolakep, filter_nolakep, num_nolakec, filter_nolakec, & + num_nvpc, filter_nvpc, & ! [PORTED by Hui Tang: NVP column filter] hs_h2osfc( begc:endc ), & hs_top_snow( begc:endc ), & hs_soil( begc:endc ), & hs_top( begc:endc ), & + hs_nvp( begc:endc ), & dhsdT( begc:endc ), & sabg_lyr_col( begc:endc, -nlevsno+1: ), & atm2lnd_inst, urbanparams_inst, canopystate_inst, waterdiagnosticbulk_inst, & @@ -365,6 +383,7 @@ subroutine SoilTemperature(bounds, num_urbanl, filter_urbanl, num_urbanc, filter hs_top_snow( begc:endc ), & hs_soil( begc:endc ), & hs_top( begc:endc ), & + hs_nvp( begc:endc ), & dhsdT( begc:endc ), & sabg_lyr_col (begc:endc, -nlevsno+1: ), & tk( begc:endc, -nlevsno+1: ), & @@ -400,6 +419,12 @@ subroutine SoilTemperature(bounds, num_urbanl, filter_urbanl, num_urbanc, filter tvector(c,j-1) = t_soisno(c,j) end do + ! [PORTED by Hui Tang: when NVP is at layer 0 and there is no snow, the snow + ! loop above is empty (snl=0); explicitly load NVP temperature into tvector] + if (col%nvp_layer_active(c) .and. snl(c) == 0) then + tvector(c,-1) = t_soisno(c,0) ! NVP layer: t_soisno(c,0) -> tvector(c,-1) + end if + ! surface water layer has two coefficients tvector(c,0) = t_h2osfc(c) @@ -424,6 +449,13 @@ subroutine SoilTemperature(bounds, num_urbanl, filter_urbanl, num_urbanc, filter do j = snl(c)+1, 0 t_soisno(c,j) = tvector(c,j-1) !snow layers end do + + ! [PORTED by Hui Tang: when NVP at layer 0 and no snow, the snow loop above is + ! empty; explicitly extract the solved NVP temperature back to t_soisno(c,0)] + if (col%nvp_layer_active(c) .and. snl(c) == 0) then + t_soisno(c,0) = tvector(c,-1) ! NVP layer: tvector(c,-1) -> t_soisno(c,0) + end if + t_soisno(c,1:nlevmaxurbgrnd) = tvector(c,1:nlevmaxurbgrnd) !soil layers if (frac_h2osfc(c) == 0._r8) then @@ -545,24 +577,49 @@ subroutine SoilTemperature(bounds, num_urbanl, filter_urbanl, num_urbanc, filter temperature_inst, energyflux_inst, urbantv_inst, atm2lnd_inst) end if + ! [PORTED by Hui Tang: update t_nvp_col BEFORE t_grnd so t_grnd can use it] + ! Default: inactive-NVP columns track soil layer 1. Active-NVP columns track + ! layer 0. Use the nvpc filter so the active override is O(NVP columns) only. + if (use_nvp) then + do fc = 1, num_nolakec + c = filter_nolakec(fc) + t_nvp_col(c) = t_soisno(c,1) ! default: no NVP → layer 1 + end do + do fc = 1, num_nvpc ! [PORTED: override for NVP-active columns] + c = filter_nvpc(fc) + t_nvp_col(c) = t_soisno(c,0) + end do + end if + do fc = 1,num_nolakec c = filter_nolakec(fc) + ! [PORTED by Hui Tang: NVP fractional area for t_grnd blend (excludes snow and h2osfc)] + if (use_nvp) then + frac_nvp_eff = min(col%frac_nvp(c), max(0._r8, 1._r8 - frac_sno_eff(c) - frac_h2osfc(c))) + else + frac_nvp_eff = 0._r8 + end if ! this expression will (should) work whether there is snow or not if (snl(c) < 0) then if(frac_h2osfc(c) /= 0._r8) then t_grnd(c) = frac_sno_eff(c) * t_soisno(c,snl(c)+1) & - + (1.0_r8 - frac_sno_eff(c) - frac_h2osfc(c)) * t_soisno(c,1) & - + frac_h2osfc(c) * t_h2osfc(c) + + (1.0_r8 - frac_sno_eff(c) - frac_h2osfc(c) - frac_nvp_eff) * t_soisno(c,1) & + + frac_h2osfc(c) * t_h2osfc(c) & + + frac_nvp_eff * t_nvp_col(c) else t_grnd(c) = frac_sno_eff(c) * t_soisno(c,snl(c)+1) & - + (1.0_r8 - frac_sno_eff(c)) * t_soisno(c,1) + + (1.0_r8 - frac_sno_eff(c) - frac_nvp_eff) * t_soisno(c,1) & + + frac_nvp_eff * t_nvp_col(c) end if else if(frac_h2osfc(c) /= 0._r8) then - t_grnd(c) = (1._r8 - frac_h2osfc(c)) * t_soisno(c,1) + frac_h2osfc(c) * t_h2osfc(c) + t_grnd(c) = (1._r8 - frac_h2osfc(c) - frac_nvp_eff) * t_soisno(c,1) & + + frac_h2osfc(c) * t_h2osfc(c) & + + frac_nvp_eff * t_nvp_col(c) else - t_grnd(c) = t_soisno(c,1) + t_grnd(c) = (1._r8 - frac_nvp_eff) * t_soisno(c,1) & + + frac_nvp_eff * t_nvp_col(c) end if endif end do @@ -624,7 +681,9 @@ subroutine SoilThermProp (bounds, num_urbanc, filter_urbanc, num_nolakec, filter use clm_varcon , only : denh2o, denice, tfrz, tkwat, tkice, tkair, cpice, cpliq, thk_bedrock, csol_bedrock use landunit_varcon , only : istice, istwet use column_varcon , only : icol_roof, icol_sunwall, icol_shadewall, icol_road_perv, icol_road_imperv - use clm_varctl , only : iulog, snow_thermal_cond_method, snow_thermal_cond_glc_method + use clm_varctl , only : iulog, snow_thermal_cond_method, snow_thermal_cond_glc_method, use_nvp + ! [PORTED by Hui Tang: NVP dry-matrix thermal parameters] + use NVPParamsMod , only : thk_dry_nvp, csol_nvp, watsat_nvp ! ! !ARGUMENTS: type(bounds_type) , intent(in) :: bounds @@ -648,6 +707,10 @@ subroutine SoilThermProp (bounds, num_urbanc, filter_urbanc, num_nolakec, filter real(r8) :: dke ! kersten number real(r8) :: fl ! volume fraction of liquid or unfrozen water to total water real(r8) :: satw ! relative total water content of soil. + ! [PORTED by Hui Tang: NVP Farouki-style thermal mixing local vars] + real(r8) :: satw_nvp ! NVP saturation fraction [-] + real(r8) :: dke_nvp ! NVP Kersten number [-] + real(r8) :: thk_sat_nvp ! NVP saturated thermal conductivity [W m-1 K-1] real(r8) :: zh2osfc !----------------------------------------------------------------------- @@ -658,12 +721,14 @@ subroutine SoilThermProp (bounds, num_urbanc, filter_urbanc, num_nolakec, filter SHR_ASSERT_ALL_FL((ubound(tk) == (/bounds%endc, nlevmaxurbgrnd/)), sourcefile, __LINE__) SHR_ASSERT_ALL_FL((ubound(tk_h2osfc) == (/bounds%endc/)), sourcefile, __LINE__) - associate( & - nbedrock => col%nbedrock , & ! Input: [real(r8) (:,:) ] depth to bedrock (m) - snl => col%snl , & ! Input: [integer (:) ] number of snow layers - dz => col%dz , & ! Input: [real(r8) (:,:) ] layer depth (m) - zi => col%zi , & ! Input: [real(r8) (:,:) ] interface level below a "z" level (m) - z => col%z , & ! Input: [real(r8) (:,:) ] layer thickness (m) + associate( & + nbedrock => col%nbedrock , & ! Input: [real(r8) (:,:) ] depth to bedrock (m) + snl => col%snl , & ! Input: [integer (:) ] number of snow layers + ! [PORTED by Hui Tang: jbot_sno = 0 (no NVP) or -1 (NVP occupies layer 0)] + jbot_sno => col%jbot_sno , & ! Input: [integer (:) ] real bottom of snow (0 or -1) + dz => col%dz , & ! Input: [real(r8) (:,:) ] layer depth (m) + zi => col%zi , & ! Input: [real(r8) (:,:) ] interface level below a "z" level (m) + z => col%z , & ! Input: [real(r8) (:,:) ] layer thickness (m) nlev_improad => urbanparams_inst%nlev_improad , & ! Input: [integer (:) ] number of impervious road layers tk_wall => urbanparams_inst%tk_wall , & ! Input: [real(r8) (:,:) ] thermal conductivity of urban wall @@ -738,7 +803,9 @@ subroutine SoilThermProp (bounds, num_urbanc, filter_urbanc, num_nolakec, filter ! Thermal conductivity of snow ! Only examine levels from snl(c)+1 -> 0 where snl(c) < 1 - if (snl(c)+1 < 1 .AND. (j >= snl(c)+1) .AND. (j <= 0)) then + ! [PORTED by Hui Tang: skip j=0 when NVP occupies that layer; handled below] + if (snl(c)+1 < 1 .AND. (j >= snl(c)+1) .AND. (j <= 0) .AND. & + .NOT. (use_nvp .AND. jbot_sno(c) == -1 .AND. j == 0)) then bw(c,j) = (h2osoi_ice(c,j)+h2osoi_liq(c,j))/(frac_sno(c)*dz(c,j)) l = col%landunit(c) @@ -778,9 +845,33 @@ subroutine SoilThermProp (bounds, num_urbanc, filter_urbanc, num_nolakec, filter write(iulog,*) ' ERROR: unknown snow_thermal_cond_method value: ', snow_thermal_cond_method call endrun(msg=errMsg(sourcefile, __LINE__)) end select - end if ! close land unit if statement + end if ! close land unit if statement end if + ! [PORTED by Hui Tang: NVP layer thermal conductivity at j=0] + ! Farouki-style mixing between dry NVP matrix and pore water/ice, + ! analogous to the soil Kersten-number formula above. + if (use_nvp .and. jbot_sno(c) == -1 .and. j == 0) then + if (dz(c,0) > 0._r8) then + satw_nvp = min(1._r8, (h2osoi_liq(c,0)/denh2o + h2osoi_ice(c,0)/denice) & + / (dz(c,0) * watsat_nvp)) + else + satw_nvp = 0._r8 + end if + if (satw_nvp > 1.e-6_r8) then + if (t_soisno(c,0) >= tfrz) then + dke_nvp = max(0._r8, log10(satw_nvp) + 1.0_r8) + thk_sat_nvp = thk_dry_nvp**(1._r8 - watsat_nvp) * tkwat**watsat_nvp + else + dke_nvp = satw_nvp + thk_sat_nvp = thk_dry_nvp**(1._r8 - watsat_nvp) * tkice**watsat_nvp + end if + thk(c,0) = dke_nvp * thk_sat_nvp + (1._r8 - dke_nvp) * thk_dry_nvp + else + thk(c,0) = thk_dry_nvp + end if + end if + end do end do @@ -829,6 +920,17 @@ subroutine SoilThermProp (bounds, num_urbanc, filter_urbanc, num_nolakec, filter end do end do + ! [PORTED by Hui Tang: NVP-soil interface conductivity when NVP at layer 0 — nvpc filter] + ! The standard loop above uses j >= snl(c)+1; when snl=0 this starts at j=1 and + ! skips j=0. Compute tk(c,0) explicitly here for NVP cases with or without snow. + if (use_nvp) then + do fc = 1, num_nvpc + c = filter_nvpc(fc) + tk(c,0) = thk(c,0)*thk(c,1)*(z(c,1)-z(c,0)) & + /(thk(c,0)*(z(c,1)-zi(c,0))+thk(c,1)*(zi(c,0)-z(c,0))) + end do + end if + ! calculate thermal conductivity of h2osfc do fc = 1, num_nolakec c = filter_nolakec(fc) @@ -881,11 +983,13 @@ subroutine SoilThermProp (bounds, num_urbanc, filter_urbanc, num_nolakec, filter end do ! Snow heat capacity + ! [PORTED by Hui Tang: guard j=0 when NVP is there; NVP cv handled below] do j = -nlevsno+1,0 do fc = 1,num_nolakec c = filter_nolakec(fc) - if (snl(c)+1 < 1 .and. j >= snl(c)+1) then + if (snl(c)+1 < 1 .and. j >= snl(c)+1 .and. & + .NOT. (use_nvp .and. jbot_sno(c) == -1 .and. j == 0)) then if (frac_sno(c) > 0._r8) then cv(c,j) = max(thin_sfclayer,(cpliq*h2osoi_liq(c,j) + cpice*h2osoi_ice(c,j))/frac_sno(c)) else @@ -894,6 +998,19 @@ subroutine SoilThermProp (bounds, num_urbanc, filter_urbanc, num_nolakec, filter end if end do end do + + ! [PORTED by Hui Tang: NVP layer heat capacity at j=0 — nvpc filter] + ! Soil-style formula: dry matrix + pore water/ice contributions. + ! cv = csol_nvp*(1-watsat_nvp)*dz + cpliq*h2osoi_liq + cpice*h2osoi_ice + if (use_nvp) then + do fc = 1, num_nvpc + c = filter_nvpc(fc) + cv(c,0) = max(thin_sfclayer, & + csol_nvp*(1._r8 - watsat_nvp)*dz(c,0) & + + cpliq*h2osoi_liq(c,0) + cpice*h2osoi_ice(c,0)) + end do + end if + call t_stopf( 'SoilThermProp' ) end associate @@ -1567,10 +1684,13 @@ subroutine ComputeGroundHeatFluxAndDeriv(bounds, & integer , intent(in) :: filter_nolakep( : ) ! patch filter for non-lake points integer , intent(in) :: num_nolakec ! number of column non-lake points in column filter integer , intent(in) :: filter_nolakec( : ) ! column filter for non-lake points + integer , intent(in) :: num_nvpc ! [PORTED by Hui Tang: number of NVP-active columns] + integer , intent(in) :: filter_nvpc( : ) ! [PORTED by Hui Tang: NVP-active column filter] real(r8) , intent(out) :: hs_h2osfc( bounds%begc: ) ! heat flux on standing water [W/m2] real(r8) , intent(out) :: hs_top_snow( bounds%begc: ) ! heat flux on top snow layer [W/m2] real(r8) , intent(out) :: hs_soil( bounds%begc: ) ! heat flux on soil [W/m2] real(r8) , intent(out) :: hs_top (bounds%begc: ) ! net energy flux into surface layer (col) [W/m2] + real(r8) , intent(out) :: hs_nvp( bounds%begc: ) ! [PORTED by Hui Tang: net surface flux at NVP layer 0] [W/m2] real(r8) , intent(out) :: dhsdT( bounds%begc: ) ! temperature derivative of "hs" [col] real(r8) , intent(out) :: sabg_lyr_col( bounds%begc:, -nlevsno+1: ) ! absorbed solar radiation (col,lyr) [W/m2] type(atm2lnd_type) , intent(in) :: atm2lnd_inst @@ -1596,6 +1716,9 @@ subroutine ComputeGroundHeatFluxAndDeriv(bounds, & real(r8) :: eflx_gnet_snow ! real(r8) :: eflx_gnet_soil ! real(r8) :: eflx_gnet_h2osfc ! + ! [PORTED by Hui Tang: NVP surface flux variables] + real(r8) :: lwrad_emit_nvp(bounds%begc:bounds%endc) ! NVP LW emission [W/m2] + real(r8) :: eflx_gnet_nvp ! net surface flux at NVP layer, patch-level [W/m2] !----------------------------------------------------------------------- ! Enforce expected array sizes @@ -1652,7 +1775,11 @@ subroutine ComputeGroundHeatFluxAndDeriv(bounds, & sabg_snow => solarabs_inst%sabg_snow_patch , & ! Input: [real(r8) (:) ] solar radiation absorbed by snow (W/m**2) sabg_chk => solarabs_inst%sabg_chk_patch , & ! Output: [real(r8) (:) ] sum of soil/snow using current fsno, for balance check sabg_lyr => solarabs_inst%sabg_lyr_patch , & ! Output: [real(r8) (:,:) ] absorbed solar radiation (pft,lyr) [W/m2] - + ! [PORTED by Hui Tang: NVP surface flux computation] + jbot_sno => col%jbot_sno , & ! Input: [integer (:) ] bottom snow layer index (0 or -1 for NVP) + eflx_sh_nvp => energyflux_inst%eflx_sh_nvp_patch , & ! Input: [real(r8) (:) ] sensible heat flux from NVP [W/m2] + qflx_ev_nvp => waterfluxbulk_inst%qflx_ev_nvp_patch , & ! Input: [real(r8) (:) ] evaporation flux from NVP [mm/s] + begc => bounds%begc , & ! Input: [integer ] beginning column index endc => bounds%endc & ! Input: [integer ] ending column index ) @@ -1670,6 +1797,12 @@ subroutine ComputeGroundHeatFluxAndDeriv(bounds, & lwrad_emit_snow(c) = emg(c) * sb * t_soisno(c,snl(c)+1)**4 lwrad_emit_soil(c) = emg(c) * sb * t_soisno(c,1)**4 lwrad_emit_h2osfc(c) = emg(c) * sb * t_h2osfc(c)**4 + ! [PORTED by Hui Tang: NVP LW emission from layer 0 temperature] + if (col%nvp_layer_active(c)) then + lwrad_emit_nvp(c) = emg(c) * sb * t_soisno(c,0)**4 + else + lwrad_emit_nvp(c) = 0._r8 + end if end do hs_soil(begc:endc) = 0._r8 @@ -1756,6 +1889,7 @@ subroutine ComputeGroundHeatFluxAndDeriv(bounds, & sabg_lyr_col(begc:endc,-nlevsno+1:1) = 0._r8 hs_top(begc:endc) = 0._r8 hs_top_snow(begc:endc) = 0._r8 + hs_nvp(begc:endc) = 0._r8 ! [PORTED by Hui Tang: NVP surface flux] do fp = 1,num_nolakep p = filter_nolakep(fp) @@ -1782,6 +1916,17 @@ subroutine ComputeGroundHeatFluxAndDeriv(bounds, & do j = lyr_top,1,1 sabg_lyr_col(c,j) = sabg_lyr_col(c,j) + sabg_lyr(p,j) * patch%wtcol(p) enddo + + ! [PORTED by Hui Tang: accumulate NVP surface BC and layer-0 solar absorption] + ! NVP layer 0 is above soil layer 1 when active (jbot_sno=-1, snl=0). + ! Its surface BC uses sabg_lyr(p,0) and NVP-specific turbulent/LW fluxes. + if (col%nvp_layer_active(c)) then + eflx_gnet_nvp = sabg_lyr(p,0) + dlrad(p) & + + (1._r8-frac_veg_nosno(p))*emg(c)*forc_lwrad(c) - lwrad_emit_nvp(c) & + - (eflx_sh_nvp(p) + qflx_ev_nvp(p)*htvp(c)) + hs_nvp(c) = hs_nvp(c) + eflx_gnet_nvp * patch%wtcol(p) + sabg_lyr_col(c,0) = sabg_lyr_col(c,0) + sabg_lyr(p,0) * patch%wtcol(p) + end if else hs_top(c) = hs_top(c) + eflx_gnet(p)*patch%wtcol(p) @@ -1911,7 +2056,7 @@ end subroutine ComputeHeatDiffFluxAndFactor !----------------------------------------------------------------------- subroutine SetRHSVec(bounds, num_nolakec, filter_nolakec, dtime, & - hs_h2osfc, hs_top_snow, hs_soil, hs_top, dhsdT, sabg_lyr_col, tk, & + hs_h2osfc, hs_top_snow, hs_soil, hs_top, hs_nvp, dhsdT, sabg_lyr_col, tk, & tk_h2osfc, fact, fn, c_h2osfc, dz_h2osfc, & temperature_inst, waterdiagnosticbulk_inst, rvector) @@ -1943,6 +2088,7 @@ subroutine SetRHSVec(bounds, num_nolakec, filter_nolakec, dtime, & real(r8) , intent(in) :: hs_top_snow( bounds%begc: ) ! heat flux on top snow layer [W/m2] real(r8) , intent(in) :: hs_soil( bounds%begc: ) ! heat flux on soil [W/m2] real(r8) , intent(in) :: hs_top( bounds%begc: ) ! net energy flux into surface layer (col) [W/m2] + real(r8) , intent(in) :: hs_nvp( bounds%begc: ) ! [PORTED by Hui Tang: surface heat flux at NVP layer 0] [W/m2] real(r8) , intent(in) :: dhsdT( bounds%begc: ) ! temperature derivative of "hs" [col] real(r8) , intent(in) :: sabg_lyr_col( bounds%begc: , -nlevsno+1: ) ! absorbed solar radiation (col,lyr) [W/m2] real(r8) , intent(in) :: tk( bounds%begc: , -nlevsno+1: ) ! thermal conductivity [W/(m K)] @@ -2002,6 +2148,7 @@ subroutine SetRHSVec(bounds, num_nolakec, filter_nolakec, dtime, & call SetRHSVec_Snow(bounds, num_nolakec, filter_nolakec, & hs_top_snow( begc:endc ), & hs_top( begc:endc ), & + hs_nvp( begc:endc ), & dhsdT( begc:endc ), & sabg_lyr_col (begc:endc, -nlevsno+1: ), & fact( begc:endc, -nlevsno+1: ), & @@ -2053,7 +2200,7 @@ end subroutine SetRHSVec !----------------------------------------------------------------------- subroutine SetRHSVec_Snow(bounds, num_nolakec, filter_nolakec, & - hs_top_snow, hs_top, dhsdT, sabg_lyr_col, & + hs_top_snow, hs_top, hs_nvp, dhsdT, sabg_lyr_col, & fact, fn, t_soisno, t_h2osfc, rt) ! ! !DESCRIPTION: @@ -2064,6 +2211,7 @@ subroutine SetRHSVec_Snow(bounds, num_nolakec, filter_nolakec, & use column_varcon , only : icol_roof, icol_sunwall, icol_shadewall use column_varcon , only : icol_road_perv, icol_road_imperv use clm_varpar , only : nlevsno, nlevmaxurbgrnd + use clm_varctl , only : use_nvp ! [PORTED by Hui Tang: NVP top-layer surface BC] ! ! !ARGUMENTS: implicit none @@ -2072,6 +2220,7 @@ subroutine SetRHSVec_Snow(bounds, num_nolakec, filter_nolakec, & integer , intent(in) :: filter_nolakec(:) ! column filter for non-lake points real(r8), intent(in) :: hs_top_snow( bounds%begc: ) ! heat flux on top snow layer [W/m2] real(r8), intent(in) :: hs_top( bounds%begc: ) ! net energy flux into surface layer (col) [W/m2] + real(r8), intent(in) :: hs_nvp( bounds%begc: ) ! [PORTED by Hui Tang: surface heat flux at NVP layer 0] [W/m2] real(r8), intent(in) :: dhsdT( bounds%begc: ) ! temperature derivative of "hs" [col] real(r8), intent(in) :: sabg_lyr_col( bounds%begc: , -nlevsno+1: ) ! absorbed solar radiation (col,lyr) [W/m2] real(r8), intent(in) :: fact( bounds%begc: , -nlevsno+1: ) ! used in computing tridiagonal matrix [col, lev] @@ -2098,10 +2247,13 @@ subroutine SetRHSVec_Snow(bounds, num_nolakec, filter_nolakec, & SHR_ASSERT_ALL_FL((ubound(t_h2osfc) == (/bounds%endc/)), sourcefile, __LINE__) SHR_ASSERT_ALL_FL((ubound(rt) == (/bounds%endc, -1/)), sourcefile, __LINE__) - associate( & - begc => bounds%begc , & ! Input: [integer ] beginning column index - endc => bounds%endc , & ! Input: [integer ] ending column index - z => col%z & ! Input: [real(r8) (:,:) ] layer thickness [m] + associate( & + begc => bounds%begc , & ! Input: [integer ] beginning column index + endc => bounds%endc , & ! Input: [integer ] ending column index + z => col%z , & ! Input: [real(r8) (:,:) ] layer thickness [m] + ! [PORTED by Hui Tang: NVP top-layer detection] + snl => col%snl , & ! Input: [integer (:) ] number of snow layers + jbot_sno => col%jbot_sno & ! Input: [integer (:) ] bottom snow index (-1 = NVP active) ) ! Initialize @@ -2139,6 +2291,13 @@ subroutine SetRHSVec_Snow(bounds, num_nolakec, filter_nolakec, & rt(c,j-1) = t_soisno(c,j) + cnfac*fact(c,j)*( fn(c,j) - fn(c,j-1) ) rt(c,j-1) = rt(c,j-1) + fact(c,j)*sabg_lyr_col(c,j) + + ! [PORTED by Hui Tang: NVP layer 0 is the top surface when snl=0 and no snow] + ! When NVP active, jtop=-1 so layer 0 is in the TVD system but falls through + ! the conditions above (j=0 < snl+1=1). Apply NVP surface BC here. + else if (j == 0 .and. use_nvp .and. jbot_sno(c) == -1 .and. snl(c) == 0) then + rt(c,j-1) = t_soisno(c,0) + fact(c,0) * & + (hs_nvp(c) - dhsdT(c)*t_soisno(c,0) + cnfac*fn(c,0)) end if end do end do @@ -2226,6 +2385,7 @@ subroutine SetRHSVec_Soil(bounds, num_nolakec, filter_nolakec, & use column_varcon , only : icol_roof, icol_sunwall, icol_shadewall use column_varcon , only : icol_road_perv, icol_road_imperv use clm_varpar , only : nlevsno, nlevurb, nlevgrnd, nlevmaxurbgrnd + use clm_varctl , only : use_nvp ! [PORTED by Hui Tang: NVP interior layer treatment] ! ! !ARGUMENTS: implicit none @@ -2266,7 +2426,9 @@ subroutine SetRHSVec_Soil(bounds, num_nolakec, filter_nolakec, & associate(& begc => bounds%begc , & ! Input: [integer ] beginning column index - endc => bounds%endc & ! Input: [integer ] ending column index + endc => bounds%endc , & ! Input: [integer ] ending column index + ! [PORTED by Hui Tang: NVP interior layer treatment] + jbot_sno => col%jbot_sno & ! Input: [integer (:) ] bottom snow index (-1 = NVP active) ) ! Initialize @@ -2314,9 +2476,14 @@ subroutine SetRHSVec_Soil(bounds, num_nolakec, filter_nolakec, & (col%itype(c) == icol_road_imperv .or. & col%itype(c) == icol_road_perv)) then - if (j == col%snl(c)+1) then + ! [PORTED by Hui Tang: exclude NVP case — when NVP active, layer 1 is interior] + if (j == col%snl(c)+1 .and. .not. (use_nvp .and. jbot_sno(c) == -1)) then rt(c,j) = t_soisno(c,j) + fact(c,j)*( hs_top_snow(c) & - dhsdT(c)*t_soisno(c,j) + cnfac*fn(c,j) ) + ! [PORTED by Hui Tang: layer 1 below NVP is interior: conduction + transmitted solar] + else if (j == 1 .and. use_nvp .and. jbot_sno(c) == -1 .and. col%snl(c) == 0) then + rt(c,j) = t_soisno(c,j) + cnfac*fact(c,j)*( fn(c,j) - fn(c,j-1) ) + rt(c,j) = rt(c,j) + fact(c,j)*sabg_lyr_col(c,j) else if (j == 1) then ! this is the snow/soil interface layer rt(c,j) = t_soisno(c,j) + fact(c,j) & From ceedb0f8c5c6dc2fc85ccf17871e3a005929110a Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Fri, 10 Apr 2026 21:04:28 +0300 Subject: [PATCH 018/113] Add NVP sensible heat flux. --- src/biogeophys/EnergyFluxType.F90 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/biogeophys/EnergyFluxType.F90 b/src/biogeophys/EnergyFluxType.F90 index 655a00d4d3..105ddb65c4 100644 --- a/src/biogeophys/EnergyFluxType.F90 +++ b/src/biogeophys/EnergyFluxType.F90 @@ -29,6 +29,8 @@ module EnergyFluxType real(r8), pointer :: eflx_sh_snow_patch (:) ! patch sensible heat flux from snow (W/m**2) [+ to atm] real(r8), pointer :: eflx_sh_soil_patch (:) ! patch sensible heat flux from soil (W/m**2) [+ to atm] real(r8), pointer :: eflx_sh_h2osfc_patch (:) ! patch sensible heat flux from surface water (W/m**2) [+ to atm] + ! [PORTED by Hui Tang: NVP (moss/lichen) individual sensible heat flux] + real(r8), pointer :: eflx_sh_nvp_patch (:) ! patch sensible heat flux from NVP (W/m**2) [+ to atm] real(r8), pointer :: eflx_sh_tot_patch (:) ! patch total sensible heat flux (W/m**2) [+ to atm] real(r8), pointer :: eflx_sh_tot_u_patch (:) ! patch urban total sensible heat flux (W/m**2) [+ to atm] real(r8), pointer :: eflx_sh_tot_r_patch (:) ! patch rural total sensible heat flux (W/m**2) [+ to atm] @@ -193,6 +195,8 @@ subroutine InitAllocate(this, bounds) allocate( this%eflx_sh_snow_patch (begp:endp)) ; this%eflx_sh_snow_patch (:) = nan allocate( this%eflx_sh_soil_patch (begp:endp)) ; this%eflx_sh_soil_patch (:) = nan allocate( this%eflx_sh_h2osfc_patch (begp:endp)) ; this%eflx_sh_h2osfc_patch (:) = nan + ! [PORTED by Hui Tang: allocate NVP sensible heat flux array] + allocate( this%eflx_sh_nvp_patch (begp:endp)) ; this%eflx_sh_nvp_patch (:) = nan allocate( this%eflx_sh_tot_patch (begp:endp)) ; this%eflx_sh_tot_patch (:) = nan allocate( this%eflx_sh_tot_u_patch (begp:endp)) ; this%eflx_sh_tot_u_patch (:) = nan allocate( this%eflx_sh_tot_r_patch (begp:endp)) ; this%eflx_sh_tot_r_patch (:) = nan @@ -288,7 +292,7 @@ subroutine InitHistory(this, bounds, is_simple_buildtemp, is_prog_buildtemp) ! !USES: use shr_infnan_mod , only : nan => shr_infnan_nan, assignment(=) use clm_varpar , only : nlevgrnd - use clm_varctl , only : use_cn, use_hydrstress + use clm_varctl , only : use_cn, use_hydrstress, use_nvp ! [PORTED by Hui Tang: NVP history] use histFileMod , only : hist_addfld1d, hist_addfld2d, no_snow_normal use ncdio_pio , only : ncd_inqvdlen implicit none @@ -690,6 +694,14 @@ subroutine InitHistory(this, bounds, is_simple_buildtemp, is_prog_buildtemp) avgflag='A', long_name='solar radiation conservation error', & ptr_patch=this%errsol_patch, set_urb=spval) + ! [PORTED by Hui Tang: history field for NVP (moss/lichen) sensible heat flux] + if (use_nvp) then + this%eflx_sh_nvp_patch(begp:endp) = spval + call hist_addfld1d (fname='EFLX_SH_NVP', units='W/m^2', & + avgflag='A', long_name='sensible heat flux from nvp (moss/lichen)', & + ptr_patch=this%eflx_sh_nvp_patch, c2l_scale_type='urbanf', default='inactive') + end if + end subroutine InitHistory !----------------------------------------------------------------------- From 85fb103971f690f25c9c2e7fa3c52c571ed193ad Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Sat, 11 Apr 2026 10:34:19 +0300 Subject: [PATCH 019/113] Add NVP sensible and latent heat flux . --- src/biogeophys/BareGroundFluxesMod.F90 | 36 ++++++++--- src/biogeophys/CanopyFluxesMod.F90 | 83 +++++++++++++++++++------- src/biogeophys/SoilFluxesMod.F90 | 7 ++- 3 files changed, 97 insertions(+), 29 deletions(-) diff --git a/src/biogeophys/BareGroundFluxesMod.F90 b/src/biogeophys/BareGroundFluxesMod.F90 index d2feba4cf7..5d0e8cb5ab 100644 --- a/src/biogeophys/BareGroundFluxesMod.F90 +++ b/src/biogeophys/BareGroundFluxesMod.F90 @@ -83,7 +83,7 @@ subroutine BareGroundFluxes(bounds, num_noexposedvegp, filter_noexposedvegp, & use clm_varpar , only : nlevgrnd use clm_varcon , only : cpair, vkc, grav, denice, denh2o, tfrz use clm_varcon , only : beta_param, nu_param, meier_param3 - use clm_varctl , only : use_lch4, z0param_method + use clm_varctl , only : use_lch4, z0param_method, use_nvp use landunit_varcon , only : istsoil, istcrop use QSatMod , only : QSat use SurfaceResistanceMod , only : do_soilevap_beta,do_soil_resistance_sl14 @@ -217,21 +217,27 @@ subroutine BareGroundFluxes(bounds, num_noexposedvegp, filter_noexposedvegp, & t_grnd => temperature_inst%t_grnd_col , & ! Input: [real(r8) (:) ] ground surface temperature [K] thv => temperature_inst%thv_col , & ! Input: [real(r8) (:) ] virtual potential temperature (kelvin) thm => temperature_inst%thm_patch , & ! Input: [real(r8) (:) ] intermediate variable (forc_t+0.0098*forc_hgt_t_patch) - t_h2osfc => temperature_inst%t_h2osfc_col , & ! Input: [real(r8) (:) ] surface water temperature - beta => temperature_inst%beta_col , & ! Input: [real(r8) (:) ] coefficient of conective velocity [-] + t_h2osfc => temperature_inst%t_h2osfc_col , & ! Input: [real(r8) (:) ] surface water temperature + ! [PORTED by Hui Tang: NVP layer temperature for bare-ground NVP flux] + t_nvp_col => temperature_inst%t_nvp_col , & ! Input: [real(r8) (:) ] NVP layer temperature [K] + beta => temperature_inst%beta_col , & ! Input: [real(r8) (:) ] coefficient of conective velocity [-] qg_snow => waterdiagnosticbulk_inst%qg_snow_col , & ! Input: [real(r8) (:) ] specific humidity at snow surface [kg/kg] - qg_soil => waterdiagnosticbulk_inst%qg_soil_col , & ! Input: [real(r8) (:) ] specific humidity at soil surface [kg/kg] - qg_h2osfc => waterdiagnosticbulk_inst%qg_h2osfc_col , & ! Input: [real(r8) (:) ] specific humidity at h2osfc surface [kg/kg] + qg_soil => waterdiagnosticbulk_inst%qg_soil_col , & ! Input: [real(r8) (:) ] specific humidity at soil surface [kg/kg] + qg_h2osfc => waterdiagnosticbulk_inst%qg_h2osfc_col , & ! Input: [real(r8) (:) ] specific humidity at h2osfc surface [kg/kg] + ! [PORTED by Hui Tang: NVP surface specific humidity for bare-ground NVP flux] + qg_nvp => waterdiagnosticbulk_inst%qg_nvp_col , & ! Input: [real(r8) (:) ] NVP surface specific humidity [kg/kg] qg => waterdiagnosticbulk_inst%qg_col , & ! Input: [real(r8) (:) ] specific humidity at ground surface [kg/kg] dqgdT => waterdiagnosticbulk_inst%dqgdT_col , & ! Input: [real(r8) (:) ] temperature derivative of "qg" h2osoi_ice => waterstatebulk_inst%h2osoi_ice_col , & ! Input: [real(r8) (:,:) ] ice lens (kg/m2) h2osoi_liq => waterstatebulk_inst%h2osoi_liq_col , & ! Input: [real(r8) (:,:) ] liquid water (kg/m2) grnd_ch4_cond => ch4_inst%grnd_ch4_cond_patch , & ! Output: [real(r8) (:) ] tracer conductance for boundary layer [m/s] - eflx_sh_snow => energyflux_inst%eflx_sh_snow_patch , & ! Output: [real(r8) (:) ] sensible heat flux from snow (W/m**2) [+ to atm] - eflx_sh_soil => energyflux_inst%eflx_sh_soil_patch , & ! Output: [real(r8) (:) ] sensible heat flux from soil (W/m**2) [+ to atm] - eflx_sh_h2osfc => energyflux_inst%eflx_sh_h2osfc_patch , & ! Output: [real(r8) (:) ] sensible heat flux from soil (W/m**2) [+ to atm] + eflx_sh_snow => energyflux_inst%eflx_sh_snow_patch , & ! Output: [real(r8) (:) ] sensible heat flux from snow (W/m**2) [+ to atm] + eflx_sh_soil => energyflux_inst%eflx_sh_soil_patch , & ! Output: [real(r8) (:) ] sensible heat flux from soil (W/m**2) [+ to atm] + eflx_sh_h2osfc => energyflux_inst%eflx_sh_h2osfc_patch , & ! Output: [real(r8) (:) ] sensible heat flux from surface water (W/m**2) [+ to atm] + ! [PORTED by Hui Tang: NVP sensible heat flux for bare ground] + eflx_sh_nvp => energyflux_inst%eflx_sh_nvp_patch , & ! Output: [real(r8) (:) ] sensible heat flux from NVP (W/m**2) [+ to atm] eflx_sh_grnd => energyflux_inst%eflx_sh_grnd_patch , & ! Output: [real(r8) (:) ] sensible heat flux from ground (W/m**2) [+ to atm] eflx_sh_tot => energyflux_inst%eflx_sh_tot_patch , & ! Output: [real(r8) (:) ] total sensible heat flux (W/m**2) [+ to atm] taux => energyflux_inst%taux_patch , & ! Output: [real(r8) (:) ] wind (shear) stress: e-w (kg/m/s**2) @@ -274,6 +280,8 @@ subroutine BareGroundFluxes(bounds, num_noexposedvegp, filter_noexposedvegp, & qflx_ev_snow => waterfluxbulk_inst%qflx_ev_snow_patch , & ! Output: [real(r8) (:) ] evaporation flux from snow (mm H2O/s) [+ to atm] qflx_ev_soil => waterfluxbulk_inst%qflx_ev_soil_patch , & ! Output: [real(r8) (:) ] evaporation flux from soil (mm H2O/s) [+ to atm] qflx_ev_h2osfc => waterfluxbulk_inst%qflx_ev_h2osfc_patch , & ! Output: [real(r8) (:) ] evaporation flux from h2osfc (mm H2O/s) [+ to atm] + ! [PORTED by Hui Tang: NVP evaporation flux for bare ground] + qflx_ev_nvp => waterfluxbulk_inst%qflx_ev_nvp_patch , & ! Output: [real(r8) (:) ] evaporation flux from NVP (mm H2O/s) [+ to atm] qflx_evap_soi => waterfluxbulk_inst%qflx_evap_soi_patch , & ! Output: [real(r8) (:) ] soil evaporation (mm H2O/s) (+ = to atm) qflx_evap_tot => waterfluxbulk_inst%qflx_evap_tot_patch , & ! Output: [real(r8) (:) ] qflx_evap_soi + qflx_evap_can + qflx_tran_veg qflx_tran_veg => waterfluxbulk_inst%qflx_tran_veg_patch , & ! Output: [real(r8) (:) ] vegetation transpiration (mm H2O/s) (+ = to atm) @@ -478,6 +486,12 @@ subroutine BareGroundFluxes(bounds, num_noexposedvegp, filter_noexposedvegp, & eflx_sh_snow(p) = -raih*(thm(p)-t_soisno(c,snl(c)+1)) eflx_sh_soil(p) = -raih*(thm(p)-t_soisno(c,1)) eflx_sh_h2osfc(p) = -raih*(thm(p)-t_h2osfc(c)) + ! [PORTED by Hui Tang: NVP sensible heat flux for bare ground, analogous to snow/h2osfc] + if (use_nvp .and. col%frac_nvp(c) > 0._r8) then + eflx_sh_nvp(p) = -raih*(thm(p)-t_nvp_col(c)) + else + eflx_sh_nvp(p) = 0._r8 + end if ! water fluxes from soil qflx_tran_veg(p) = 0._r8 @@ -489,6 +503,12 @@ subroutine BareGroundFluxes(bounds, num_noexposedvegp, filter_noexposedvegp, & qflx_ev_snow(p) = -raiw*(forc_q(c) - qg_snow(c)) qflx_ev_soil(p) = -raiw*(forc_q(c) - qg_soil(c)) qflx_ev_h2osfc(p) = -raiw*(forc_q(c) - qg_h2osfc(c)) + ! [PORTED by Hui Tang: NVP evaporation flux for bare ground, analogous to snow/h2osfc] + if (use_nvp .and. col%frac_nvp(c) > 0._r8) then + qflx_ev_nvp(p) = -raiw*(forc_q(c) - qg_nvp(c)) + else + qflx_ev_nvp(p) = 0._r8 + end if ! 2 m height air temperature t_ref2m(p) = thm(p) + temp1(p)*dth(p)*(1._r8/temp12m(p) - 1._r8/temp1(p)) diff --git a/src/biogeophys/CanopyFluxesMod.F90 b/src/biogeophys/CanopyFluxesMod.F90 index 29374a7b70..3a328d23f1 100644 --- a/src/biogeophys/CanopyFluxesMod.F90 +++ b/src/biogeophys/CanopyFluxesMod.F90 @@ -14,7 +14,8 @@ module CanopyFluxesMod use shr_log_mod , only : errMsg => shr_log_errMsg use abortutils , only : endrun use clm_varctl , only : iulog, use_cn, use_lch4, use_c13, use_cndv, use_fates, & - use_luna, use_hydrstress, use_biomass_heat_storage, z0param_method + use_luna, use_hydrstress, use_biomass_heat_storage, z0param_method, & + use_nvp use clm_varpar , only : nlevgrnd, nlevsno, nlevcan, mxpft use pftconMod , only : pftcon use decompMod , only : bounds_type, subgrid_level_patch @@ -399,10 +400,14 @@ subroutine CanopyFluxes(bounds, num_exposedvegp, filter_exposedvegp, real(r8) :: delt_snow real(r8) :: delt_soil real(r8) :: delt_h2osfc + ! [PORTED by Hui Tang: NVP individual ground flux temporaries] + real(r8) :: delt_nvp + real(r8) :: frac_nvp_eff ! NVP effective fraction (excludes snow and h2osfc) real(r8) :: lw_grnd real(r8) :: delq_snow real(r8) :: delq_soil real(r8) :: delq_h2osfc + real(r8) :: delq_nvp real(r8) :: dt_veg(bounds%begp:bounds%endp) ! change in t_veg, last iteration (Kelvin) integer :: jtop(bounds%begc:bounds%endc) ! lbning integer :: filterc_tmp(bounds%endp-bounds%begp+1) ! temporary variable @@ -548,9 +553,11 @@ subroutine CanopyFluxes(bounds, num_exposedvegp, filter_exposedvegp, z0qv => frictionvel_inst%z0qv_patch , & ! Output: [real(r8) (:) ] roughness length over vegetation, latent heat [m] rb1 => frictionvel_inst%rb1_patch , & ! Output: [real(r8) (:) ] boundary layer resistance (s/m) - t_h2osfc => temperature_inst%t_h2osfc_col , & ! Input: [real(r8) (:) ] surface water temperature - t_soisno => temperature_inst%t_soisno_col , & ! Input: [real(r8) (:,:) ] soil temperature (Kelvin) - t_grnd => temperature_inst%t_grnd_col , & ! Input: [real(r8) (:) ] ground surface temperature [K] + t_h2osfc => temperature_inst%t_h2osfc_col , & ! Input: [real(r8) (:) ] surface water temperature + t_soisno => temperature_inst%t_soisno_col , & ! Input: [real(r8) (:,:) ] soil temperature (Kelvin) + t_grnd => temperature_inst%t_grnd_col , & ! Input: [real(r8) (:) ] ground surface temperature [K] + ! [PORTED by Hui Tang: NVP layer temperature for NVP ground flux diagnostics] + t_nvp_col => temperature_inst%t_nvp_col , & ! Input: [real(r8) (:) ] NVP (moss/lichen) layer temperature [K] thv => temperature_inst%thv_col , & ! Input: [real(r8) (:) ] virtual potential temperature (kelvin) thm => temperature_inst%thm_patch , & ! Input: [real(r8) (:) ] intermediate variable (forc_t+0.0098*forc_hgt_t_patch) emv => temperature_inst%emv_patch , & ! Input: [real(r8) (:) ] vegetation emissivity @@ -565,10 +572,12 @@ subroutine CanopyFluxes(bounds, num_exposedvegp, filter_exposedvegp, fdry => waterdiagnosticbulk_inst%fdry_patch , & ! Input: [real(r8) (:) ] fraction of foliage that is green and dry [-] frac_sno => waterdiagnosticbulk_inst%frac_sno_eff_col , & ! Input: [real(r8) (:) ] fraction of ground covered by snow (0 to 1) snow_depth => waterdiagnosticbulk_inst%snow_depth_col , & ! Input: [real(r8) (:) ] snow height (m) - qg_snow => waterdiagnosticbulk_inst%qg_snow_col , & ! Input: [real(r8) (:) ] specific humidity at snow surface [kg/kg] - qg_soil => waterdiagnosticbulk_inst%qg_soil_col , & ! Input: [real(r8) (:) ] specific humidity at soil surface [kg/kg] - qg_h2osfc => waterdiagnosticbulk_inst%qg_h2osfc_col , & ! Input: [real(r8) (:) ] specific humidity at h2osfc surface [kg/kg] - qg => waterdiagnosticbulk_inst%qg_col , & ! Input: [real(r8) (:) ] specific humidity at ground surface [kg/kg] + qg_snow => waterdiagnosticbulk_inst%qg_snow_col , & ! Input: [real(r8) (:) ] specific humidity at snow surface [kg/kg] + qg_soil => waterdiagnosticbulk_inst%qg_soil_col , & ! Input: [real(r8) (:) ] specific humidity at soil surface [kg/kg] + qg_h2osfc => waterdiagnosticbulk_inst%qg_h2osfc_col , & ! Input: [real(r8) (:) ] specific humidity at h2osfc surface [kg/kg] + ! [PORTED by Hui Tang: NVP surface specific humidity for ground evap diagnostic] + qg_nvp => waterdiagnosticbulk_inst%qg_nvp_col , & ! Input: [real(r8) (:) ] specific humidity at NVP surface [kg/kg] + qg => waterdiagnosticbulk_inst%qg_col , & ! Input: [real(r8) (:) ] specific humidity at ground surface [kg/kg] dqgdT => waterdiagnosticbulk_inst%dqgdT_col , & ! Input: [real(r8) (:) ] temperature derivative of "qg" h2osoi_ice => waterstatebulk_inst%h2osoi_ice_col , & ! Input: [real(r8) (:,:) ] ice lens (kg/m2) @@ -587,10 +596,12 @@ subroutine CanopyFluxes(bounds, num_exposedvegp, filter_exposedvegp, qflx_tran_veg => waterfluxbulk_inst%qflx_tran_veg_patch , & ! Output: [real(r8) (:) ] vegetation transpiration (mm H2O/s) (+ = to atm) qflx_evap_veg => waterfluxbulk_inst%qflx_evap_veg_patch , & ! Output: [real(r8) (:) ] vegetation evaporation (mm H2O/s) (+ = to atm) - qflx_evap_soi => waterfluxbulk_inst%qflx_evap_soi_patch , & ! Output: [real(r8) (:) ] soil evaporation (mm H2O/s) (+ = to atm) - qflx_ev_snow => waterfluxbulk_inst%qflx_ev_snow_patch , & ! Output: [real(r8) (:) ] evaporation flux from snow (mm H2O/s) [+ to atm] - qflx_ev_soil => waterfluxbulk_inst%qflx_ev_soil_patch , & ! Output: [real(r8) (:) ] evaporation flux from soil (mm H2O/s) [+ to atm] - qflx_ev_h2osfc => waterfluxbulk_inst%qflx_ev_h2osfc_patch , & ! Output: [real(r8) (:) ] evaporation flux from h2osfc (mm H2O/s) [+ to atm] + qflx_evap_soi => waterfluxbulk_inst%qflx_evap_soi_patch , & ! Output: [real(r8) (:) ] soil evaporation (mm H2O/s) (+ = to atm) + qflx_ev_snow => waterfluxbulk_inst%qflx_ev_snow_patch , & ! Output: [real(r8) (:) ] evaporation flux from snow (mm H2O/s) [+ to atm] + qflx_ev_soil => waterfluxbulk_inst%qflx_ev_soil_patch , & ! Output: [real(r8) (:) ] evaporation flux from soil (mm H2O/s) [+ to atm] + qflx_ev_h2osfc => waterfluxbulk_inst%qflx_ev_h2osfc_patch , & ! Output: [real(r8) (:) ] evaporation flux from h2osfc (mm H2O/s) [+ to atm] + ! [PORTED by Hui Tang: NVP evaporation flux diagnostic] + qflx_ev_nvp => waterfluxbulk_inst%qflx_ev_nvp_patch , & ! Output: [real(r8) (:) ] evaporation flux from NVP (mm H2O/s) [+ to atm] gs_mol_sun => photosyns_inst%gs_mol_sun_patch , & ! Input: [real(r8) (:) ] patch sunlit leaf stomatal conductance (umol H2O/m**2/s) gs_mol_sha => photosyns_inst%gs_mol_sha_patch , & ! Input: [real(r8) (:) ] patch shaded leaf stomatal conductance (umol H2O/m**2/s) rssun => photosyns_inst%rssun_patch , & ! Output: [real(r8) (:) ] leaf sunlit stomatal resistance (s/m) (output from Photosynthesis) @@ -610,9 +621,11 @@ subroutine CanopyFluxes(bounds, num_exposedvegp, filter_exposedvegp, dlrad => energyflux_inst%dlrad_patch , & ! Output: [real(r8) (:) ] downward longwave radiation below the canopy [W/m2] ulrad => energyflux_inst%ulrad_patch , & ! Output: [real(r8) (:) ] upward longwave radiation above the canopy [W/m2] cgrnd => energyflux_inst%cgrnd_patch , & ! Output: [real(r8) (:) ] deriv. of soil energy flux wrt to soil temp [w/m2/k] - eflx_sh_snow => energyflux_inst%eflx_sh_snow_patch , & ! Output: [real(r8) (:) ] sensible heat flux from snow (W/m**2) [+ to atm] - eflx_sh_h2osfc => energyflux_inst%eflx_sh_h2osfc_patch , & ! Output: [real(r8) (:) ] sensible heat flux from soil (W/m**2) [+ to atm] - eflx_sh_soil => energyflux_inst%eflx_sh_soil_patch , & ! Output: [real(r8) (:) ] sensible heat flux from soil (W/m**2) [+ to atm] + eflx_sh_snow => energyflux_inst%eflx_sh_snow_patch , & ! Output: [real(r8) (:) ] sensible heat flux from snow (W/m**2) [+ to atm] + eflx_sh_h2osfc => energyflux_inst%eflx_sh_h2osfc_patch , & ! Output: [real(r8) (:) ] sensible heat flux from surface water (W/m**2) [+ to atm] + eflx_sh_soil => energyflux_inst%eflx_sh_soil_patch , & ! Output: [real(r8) (:) ] sensible heat flux from soil (W/m**2) [+ to atm] + ! [PORTED by Hui Tang: NVP sensible heat flux diagnostic] + eflx_sh_nvp => energyflux_inst%eflx_sh_nvp_patch , & ! Output: [real(r8) (:) ] sensible heat flux from NVP (W/m**2) [+ to atm] eflx_sh_stem => energyflux_inst%eflx_sh_stem_patch , & ! Output: [real(r8) (:) ] sensible heat flux from stems (W/m**2) [+ to atm] eflx_sh_veg => energyflux_inst%eflx_sh_veg_patch , & ! Output: [real(r8) (:) ] sensible heat flux from leaves (W/m**2) [+ to atm] eflx_sh_grnd => energyflux_inst%eflx_sh_grnd_patch , & ! Output: [real(r8) (:) ] sensible heat flux from ground (W/m**2) [+ to atm] @@ -1298,9 +1311,16 @@ subroutine CanopyFluxes(bounds, num_exposedvegp, filter_exposedvegp, erre = efe(p) - efeold end if ! fractionate ground emitted longwave + ! [PORTED by Hui Tang: include NVP layer in lw_grnd blend] + if (use_nvp) then + frac_nvp_eff = min(col%frac_nvp(c), max(0._r8, 1._r8 - frac_sno(c) - frac_h2osfc(c))) + else + frac_nvp_eff = 0._r8 + end if lw_grnd=(frac_sno(c)*t_soisno(c,snl(c)+1)**4 & - +(1._r8-frac_sno(c)-frac_h2osfc(c))*t_soisno(c,1)**4 & - +frac_h2osfc(c)*t_h2osfc(c)**4) + +(1._r8-frac_sno(c)-frac_h2osfc(c)-frac_nvp_eff)*t_soisno(c,1)**4 & + +frac_h2osfc(c)*t_h2osfc(c)**4 & + +frac_nvp_eff*t_nvp_col(c)**4) dt_veg(p) = ((1._r8-frac_rad_abs_by_stem(p))*(sabv(p) + air(p) & + bir(p)*t_veg(p)**4 + cir(p)*lw_grnd) & @@ -1467,10 +1487,16 @@ subroutine CanopyFluxes(bounds, num_exposedvegp, filter_exposedvegp, g = patch%gridcell(p) ! Energy balance check in canopy - + ! [PORTED by Hui Tang: include NVP in lw_grnd for energy balance check] + if (use_nvp) then + frac_nvp_eff = min(col%frac_nvp(c), max(0._r8, 1._r8 - frac_sno(c) - frac_h2osfc(c))) + else + frac_nvp_eff = 0._r8 + end if lw_grnd=(frac_sno(c)*t_soisno(c,snl(c)+1)**4 & - +(1._r8-frac_sno(c)-frac_h2osfc(c))*t_soisno(c,1)**4 & - +frac_h2osfc(c)*t_h2osfc(c)**4) + +(1._r8-frac_sno(c)-frac_h2osfc(c)-frac_nvp_eff)*t_soisno(c,1)**4 & + +frac_h2osfc(c)*t_h2osfc(c)**4 & + +frac_nvp_eff*t_nvp_col(c)**4) err(p) = (1.0_r8-frac_rad_abs_by_stem(p))*(sabv(p) + air(p) + bir(p)*tlbef(p)**3 & *(tlbef(p) + 4._r8*dt_veg(p)) + cir(p)*lw_grnd) & @@ -1515,6 +1541,13 @@ subroutine CanopyFluxes(bounds, num_exposedvegp, filter_exposedvegp, eflx_sh_snow(p) = cpair*forc_rho(c)*wtg(p)*delt_snow eflx_sh_soil(p) = cpair*forc_rho(c)*wtg(p)*delt_soil eflx_sh_h2osfc(p) = cpair*forc_rho(c)*wtg(p)*delt_h2osfc + ! [PORTED by Hui Tang: NVP individual sensible heat flux, analogous to snow/h2osfc] + if (use_nvp .and. col%frac_nvp(c) > 0._r8) then + delt_nvp = wtal(p)*t_nvp_col(c)-wtl0(p)*t_veg(p)-wta0(p)*thm(p)-wtstem0(p)*t_stem(p) + eflx_sh_nvp(p) = cpair*forc_rho(c)*wtg(p)*delt_nvp + else + eflx_sh_nvp(p) = 0._r8 + end if qflx_evap_soi(p) = forc_rho(c)*wtgq(p)*delq(p) ! compute individual latent heat fluxes @@ -1527,6 +1560,16 @@ subroutine CanopyFluxes(bounds, num_exposedvegp, filter_exposedvegp, delq_h2osfc = wtalq(p)*qg_h2osfc(c)-wtlq0(p)*qsatl(p)-wtaq0(p)*forc_q(c) qflx_ev_h2osfc(p) = forc_rho(c)*wtgq(p)*delq_h2osfc + ! [PORTED by Hui Tang: NVP individual latent heat flux, analogous to snow/h2osfc] + ! qflx_evap_soi already includes NVP because qg(c) blends NVP in SurfaceHumidityMod. + ! This is the diagnostic breakdown of the NVP contribution. + if (use_nvp .and. col%frac_nvp(c) > 0._r8) then + delq_nvp = wtalq(p)*qg_nvp(c)-wtlq0(p)*qsatl(p)-wtaq0(p)*forc_q(c) + qflx_ev_nvp(p) = forc_rho(c)*wtgq(p)*delq_nvp + else + qflx_ev_nvp(p) = 0._r8 + end if + ! 2 m height air temperature t_ref2m(p) = thm(p) + temp1(p)*dth(p)*(1._r8/temp12m(p) - 1._r8/temp1(p)) diff --git a/src/biogeophys/SoilFluxesMod.F90 b/src/biogeophys/SoilFluxesMod.F90 index 44e6d0e1cd..7278c2779f 100644 --- a/src/biogeophys/SoilFluxesMod.F90 +++ b/src/biogeophys/SoilFluxesMod.F90 @@ -134,7 +134,9 @@ subroutine SoilFluxes (bounds, num_urbanl, filter_urbanl, & qflx_soliddew_to_top_layer => waterfluxbulk_inst%qflx_soliddew_to_top_layer_patch , & ! Output: [real(r8) (:) ] rate of solid water deposited on top soil or snow layer (frost) (mm H2O /s) [+] qflx_ev_snow => waterfluxbulk_inst%qflx_ev_snow_patch , & ! In/Out: [real(r8) (:) ] evaporation flux from snow (mm H2O/s) [+ to atm] qflx_ev_soil => waterfluxbulk_inst%qflx_ev_soil_patch , & ! In/Out: [real(r8) (:) ] evaporation flux from soil (mm H2O/s) [+ to atm] - qflx_ev_h2osfc => waterfluxbulk_inst%qflx_ev_h2osfc_patch , & ! In/Out: [real(r8) (:) ] evaporation flux from soil (mm H2O/s) [+ to atm] + qflx_ev_h2osfc => waterfluxbulk_inst%qflx_ev_h2osfc_patch , & ! In/Out: [real(r8) (:) ] evaporation flux from h2osfc (mm H2O/s) [+ to atm] + ! [PORTED by Hui Tang: NVP evaporation flux linearization correction] + qflx_ev_nvp => waterfluxbulk_inst%qflx_ev_nvp_patch , & ! In/Out: [real(r8) (:) ] evaporation flux from NVP (mm H2O/s) [+ to atm] eflx_sh_grnd => energyflux_inst%eflx_sh_grnd_patch , & ! Output: [real(r8) (:) ] sensible heat flux from ground (W/m**2) [+ to atm] eflx_sh_veg => energyflux_inst%eflx_sh_veg_patch , & ! Output: [real(r8) (:) ] sensible heat flux from leaves (W/m**2) [+ to atm] @@ -197,11 +199,14 @@ subroutine SoilFluxes (bounds, num_urbanl, filter_urbanl, & if (lun%urbpoi(l)) then qflx_ev_soil(p) = 0._r8 qflx_ev_h2osfc(p) = 0._r8 + qflx_ev_nvp(p) = 0._r8 qflx_ev_snow(p) = qflx_evap_soi(p) else qflx_ev_snow(p) = qflx_ev_snow(p) + tinc(c)*cgrndl(p) qflx_ev_soil(p) = qflx_ev_soil(p) + tinc(c)*cgrndl(p) qflx_ev_h2osfc(p) = qflx_ev_h2osfc(p) + tinc(c)*cgrndl(p) + ! [PORTED by Hui Tang: apply linearization correction to NVP evaporation diagnostic] + qflx_ev_nvp(p) = qflx_ev_nvp(p) + tinc(c)*cgrndl(p) endif end do From 5b1eaf43446dde3b31c10d7b7ea5faa60f06ccd1 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Sat, 11 Apr 2026 23:59:55 +0300 Subject: [PATCH 020/113] Add surface humidity calculation for NVP (similar to soil). --- src/biogeophys/SurfaceHumidityMod.F90 | 69 +++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 5 deletions(-) diff --git a/src/biogeophys/SurfaceHumidityMod.F90 b/src/biogeophys/SurfaceHumidityMod.F90 index 25018211a9..87024f3eb7 100644 --- a/src/biogeophys/SurfaceHumidityMod.F90 +++ b/src/biogeophys/SurfaceHumidityMod.F90 @@ -11,11 +11,16 @@ module SurfaceHumidityMod use shr_kind_mod , only : r8 => shr_kind_r8 use decompMod , only : bounds_type use abortutils , only : endrun - use clm_varcon , only : denh2o, denice, roverg, tfrz, spval + use clm_varcon , only : denh2o, denice, roverg, tfrz, spval use column_varcon , only : icol_roof, icol_sunwall, icol_shadewall use column_varcon , only : icol_road_imperv, icol_road_perv use landunit_varcon , only : istice, istwet, istsoil, istcrop use clm_varpar , only : nlevgrnd + ! [PORTED by Hui Tang: use_nvp flag for NVP ground evaporation blending] + use clm_varctl , only : use_nvp + use NVPLayerDynamicsMod , only : NVPWaterRetentionCurve + ! [PORTED by Hui Tang: runtime-tunable NVP physics parameters] + use NVPParamsMod , only : n_van_nvp, alpha_van_nvp, watsat_nvp, watres_nvp use atm2lndType , only : atm2lnd_type use SoilStateType , only : soilstate_type use TemperatureType , only : temperature_type @@ -70,6 +75,11 @@ subroutine CalculateSurfaceHumidity(bounds, & real(r8) :: qsatgdT_snow ! d(qsatg)/dT, for snow real(r8) :: qsatgdT_soil ! d(qsatg)/dT, for soil real(r8) :: qsatgdT_h2osfc ! d(qsatg)/dT, for h2osfc + ! [PORTED by Hui Tang: NVP ground humidity variables] + real(r8) :: qsatgdT_nvp ! d(qsatg)/dT, for NVP surface + real(r8) :: frac_nvp_eff ! effective NVP fraction for ground humidity blend + real(r8) :: hr_nvp ! alpha NVP + real(r8) :: psit_nvp ! negative potential of NVP real(r8) :: fac ! soil wetness of surface layer real(r8) :: psit ! negative potential of soil real(r8) :: hr ! alpha soil @@ -98,7 +108,8 @@ subroutine CalculateSurfaceHumidity(bounds, & qg => waterdiagnosticbulk_inst%qg_col , & ! Output: [real(r8) (:) ] ground specific humidity [kg/kg] qg_h2osfc => waterdiagnosticbulk_inst%qg_h2osfc_col , & ! Output: [real(r8) (:) ] specific humidity at h2osfc surface [kg/kg] dqgdT => waterdiagnosticbulk_inst%dqgdT_col , & ! Output: [real(r8) (:) ] d(qg)/dT - + ! [PORTED by Hui Tang: NVP ground humidity fields for ground evap blending] + qg_nvp => waterdiagnosticbulk_inst%qg_nvp_col , & ! Output: [real(r8) (:) ] NVP surface specific humidity [kg/kg] smpmin => soilstate_inst%smpmin_col , & ! Input: [real(r8) (:) ] restriction for min of soil potential (mm) sucsat => soilstate_inst%sucsat_col , & ! Input: [real(r8) (:,:) ] minimum soil suction (mm) watsat => soilstate_inst%watsat_col , & ! Input: [real(r8) (:,:) ] volumetric soil water at saturation (porosity) @@ -112,7 +123,9 @@ subroutine CalculateSurfaceHumidity(bounds, & t_h2osfc => temperature_inst%t_h2osfc_col , & ! Input: [real(r8) (:) ] surface water temperature t_soisno => temperature_inst%t_soisno_col , & ! Input: [real(r8) (:,:) ] soil temperature (Kelvin) - t_grnd => temperature_inst%t_grnd_col & ! Input: [real(r8) (:) ] ground temperature (Kelvin) + t_grnd => temperature_inst%t_grnd_col , & ! Input: [real(r8) (:) ] ground temperature (Kelvin) + ! [PORTED by Hui Tang: NVP layer temperature for NVP surface humidity] + t_nvp_col => temperature_inst%t_nvp_col & ! Input: [real(r8) (:) ] NVP (moss/lichen) temperature (Kelvin) ) do fc = 1,num_nolakec @@ -136,8 +149,35 @@ subroutine CalculateSurfaceHumidity(bounds, & psit = max(smpmin(c), psit) ! modify qred to account for h2osfc hr = exp(psit/roverg/t_soisno(c,1)) - qred = (1._r8 - frac_sno_eff(c) - frac_h2osfc(c))*hr & - + frac_sno_eff(c) + frac_h2osfc(c) + + ! [PORTED by Hui Tang: NVP effective fraction for ground humidity blend] + ! NVP occupies area not covered by snow or surface water + if (use_nvp) then + ! Compute NVP surface humidity as a function of NVP water retention curve + ! --- NVP volumetric water content (clamped to valid range) --- + if (dz(c,0) > 0._r8) then + vol_ice = min(watsat_nvp, h2osoi_ice(c,0)/(dz(c,0)*denice)) + eff_porosity = watsat_nvp-vol_ice + vol_liq = min(eff_porosity, h2osoi_liq(c,0)/(dz(c,0)*denh2o)) + psit_nvp = NVPWaterRetentionCurve(vol_liq, n_van_nvp, alpha_van_nvp, & + watsat_nvp, watres_nvp, psi_nvp) + hr_nvp = exp(psit_nvp/roverg/t_nvp_col(c)) + end if + else + hr_nvp = 0._r8 + end if + + ! [PORTED by Hui Tang: NVP effective fraction for ground humidity blend] + ! NVP occupies area not covered by snow or surface water + if (use_nvp) then + frac_nvp_eff = min(col%frac_nvp(c), max(0._r8, 1._r8 - frac_sno_eff(c) - frac_h2osfc(c))) + qred = (1._r8 - frac_sno_eff(c) - frac_h2osfc(c) - frac_nvp_eff)*hr & + + frac_sno_eff(c) + frac_h2osfc(c) + frac_nvp_eff*hr_nvp + else + frac_nvp_eff = 0._r8 + qred = (1._r8 - frac_sno_eff(c) - frac_h2osfc(c))*hr & + + frac_sno_eff(c) + frac_h2osfc(c) + end if soilalpha(c) = qred else if (col%itype(c) == icol_road_perv) then @@ -215,6 +255,25 @@ subroutine CalculateSurfaceHumidity(bounds, & qg(c) = frac_sno_eff(c)*qg_snow(c) + (1._r8 - frac_sno_eff(c) - frac_h2osfc(c))*qg_soil(c) & + frac_h2osfc(c) * qg_h2osfc(c) + ! [PORTED by Hui Tang: NVP ground evaporation blending] + ! When NVP is active, include NVP surface humidity in qg blend. + ! NVP occupies area not covered by snow or surface water. + ! qg_nvp = hr_nvp * qsat(t_nvp): hr_nvp acts as surface RH. + if (use_nvp) then + qg_nvp(c) = qg_soil(c) ! default when no NVP coverage; recomputed below if frac_nvp_eff > 0 + if (frac_nvp_eff > 0._r8) then + call QSat(t_nvp_col(c), forc_pbot(c), qsatg, & + qsdT = qsatgdT_nvp) + qg_nvp(c) = hr_nvp(c) * qsatg + ! Adjust qg and dqgdT: reduce bare-soil contribution by frac_nvp_eff, add NVP term + qg(c) = qg(c) - frac_nvp_eff * qg_soil(c) + frac_nvp_eff * qg_nvp(c) + dqgdT(c) = dqgdT(c) - frac_nvp_eff * hr * qsatgdT_soil & + + frac_nvp_eff * hr_nvp(c) * qsatgdT_nvp + end if + else + qg_nvp(c) = qg_soil(c) + end if + else call QSat(t_grnd(c), forc_pbot(c), qsatg, & qsdT = qsatgdT) From 522d8f58156f809c840eb800ff70d3b7a09915c2 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Sun, 12 Apr 2026 00:50:43 +0300 Subject: [PATCH 021/113] Add influence of ice content on NVP water retention. --- src/biogeophys/NVPLayerDynamicsMod.F90 | 34 +++++++++++++++----------- src/biogeophys/SurfaceHumidityMod.F90 | 17 ++++++++++--- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/src/biogeophys/NVPLayerDynamicsMod.F90 b/src/biogeophys/NVPLayerDynamicsMod.F90 index 3c6a525ee0..c3ef50b6ff 100644 --- a/src/biogeophys/NVPLayerDynamicsMod.F90 +++ b/src/biogeophys/NVPLayerDynamicsMod.F90 @@ -45,7 +45,7 @@ module NVPLayerDynamicsMod use WaterDiagnosticBulkType, only : waterdiagnosticbulk_type ! [PORTED by Hui Tang: soilstate for bidirectional NVP-soil Darcy flux] use SoilStateType , only : soilstate_type - use clm_varcon , only : cpliq, cpice, denh2o, roverg + use clm_varcon , only : cpliq, cpice, denh2o, roverg, tfrz, denice ! [PORTED by Hui Tang: use_nvp_undersnow flag to deactivate NVP when snow present] use clm_varctl , only : use_nvp_undersnow use QSatMod , only : QSat @@ -194,7 +194,7 @@ end subroutine UpdateNVPLayer ! =========================================================================== - subroutine NVPWaterRetentionCurve(vol_liq, n_van, alpha_van, watsat, watres, smp) + subroutine NVPWaterRetentionCurve(vol_liq, eff_porosity, n_van, alpha_van, watsat, watres, smp) ! ------------------------------------------------------------------------- ! Convert NVP volumetric liquid water content to soil water potential [mm] ! using the van Genuchten (1980) retention curve formulation. @@ -203,6 +203,7 @@ subroutine NVPWaterRetentionCurve(vol_liq, n_van, alpha_van, watsat, watres, smp ! ! Arguments: ! vol_liq — volumetric liquid water content [m3 m-3] + ! eff_porosity — effective porosity [m3 m-3] ! n_van — van Genuchten shape parameter n [-] (> 1) ! alpha_van — van Genuchten inverse air-entry pressure [cm-1 * 10] ! watsat — saturated volumetric water content (= theta_nvp_max) [m3 m-3] @@ -210,6 +211,7 @@ subroutine NVPWaterRetentionCurve(vol_liq, n_van, alpha_van, watsat, watres, smp ! smp — soil/NVP matric potential [mm] (negative; more negative = drier) ! ------------------------------------------------------------------------- real(r8), intent(in) :: vol_liq + real(r8), intent(in) :: eff_porosity real(r8), intent(in) :: n_van real(r8), intent(in) :: alpha_van real(r8), intent(in) :: watsat @@ -217,12 +219,9 @@ subroutine NVPWaterRetentionCurve(vol_liq, n_van, alpha_van, watsat, watres, smp real(r8), intent(out) :: smp real(r8) :: m_van ! van Genuchten m = 1 - 1/n - real(r8) :: eff_porosity ! effective porosity [m3 m-3] real(r8) :: satfrac ! effective saturation fraction [-] m_van = 1.0_r8 - 1.0_r8 / n_van - ! NVP is assumed to have no ice fraction (layer-0 is always above 0 C in practice) - eff_porosity = max(0.01_r8, watsat) satfrac = (vol_liq - watres) / (eff_porosity - watres) satfrac = max(0.0_r8, min(1.0_r8, satfrac)) ! clamp to [0, 1] @@ -236,7 +235,7 @@ end subroutine NVPWaterRetentionCurve ! =========================================================================== - subroutine NVPHydraulicConductivity(vol_liq, n_van, watsat, watres, ksat, khydr) + subroutine NVPHydraulicConductivity(vol_liq, eff_porosity, n_van, watsat, watres, ksat, khydr) ! ------------------------------------------------------------------------- ! Compute NVP hydraulic conductivity [m s-1] using the Mualem-van Genuchten ! (1976/1980) formulation. @@ -245,6 +244,7 @@ subroutine NVPHydraulicConductivity(vol_liq, n_van, watsat, watres, ksat, khydr) ! ! Arguments: ! vol_liq — volumetric liquid water content [m3 m-3] + ! eff_porosity — effective porosity [m3 m-3] ! n_van — van Genuchten shape parameter n [-] ! watsat — saturated volumetric water content [m3 m-3] ! watres — residual volumetric water content [m3 m-3] @@ -252,6 +252,7 @@ subroutine NVPHydraulicConductivity(vol_liq, n_van, watsat, watres, ksat, khydr) ! khydr — unsaturated hydraulic conductivity [m s-1] ! ------------------------------------------------------------------------- real(r8), intent(in) :: vol_liq + real(r8), intent(in) :: eff_porosity real(r8), intent(in) :: n_van real(r8), intent(in) :: watsat real(r8), intent(in) :: watres @@ -259,12 +260,9 @@ subroutine NVPHydraulicConductivity(vol_liq, n_van, watsat, watres, ksat, khydr) real(r8), intent(out) :: khydr real(r8) :: m_van ! van Genuchten m = 1 - 1/n - real(r8) :: eff_porosity ! effective porosity [m3 m-3] real(r8) :: satfrac ! effective saturation fraction [-] m_van = 1.0_r8 - 1.0_r8 / n_van - ! NVP is assumed to have no ice - eff_porosity = max(0.01_r8, watsat) satfrac = (vol_liq - watres) / (eff_porosity - watres) satfrac = max(0.0_r8, min(1.0_r8, satfrac)) ! clamp to [0, 1] @@ -367,7 +365,7 @@ end subroutine NVPEvaporation ! [PORTED by Hui Tang: NVP column water balance — gravity drainage from layer 0 to soil layer 1] ! [PORTED by Hui Tang: bidirectional Darcy flux between NVP layer 0 and soil layer 1] subroutine NVPWaterBalance_Column(bounds, dtime, waterfluxbulk_inst, waterstate_inst, & - waterdiagnosticbulk_inst, soilstate_inst) + waterdiagnosticbulk_inst, soilstate_inst, temperature_inst) ! ------------------------------------------------------------------------- ! Update h2osoi_liq(c,0) for the NVP layer and compute the net water ! exchange with soil layer 1 via a bidirectional Darcy flux. @@ -438,6 +436,7 @@ subroutine NVPWaterBalance_Column(bounds, dtime, waterfluxbulk_inst, waterstate_ frac_h2osfc_col => waterdiagnosticbulk_inst%frac_h2osfc_col, & fwet_nvp_col => waterdiagnosticbulk_inst%fwet_nvp_col, & vwc_nvp_col => waterdiagnosticbulk_inst%vwc_nvp_col & ! [PORTED by Hui Tang: volumetric water content] + t_nvp_col => temperature_inst%t_nvp_col & ! Input: [real(r8) (:) ] NVP (moss/lichen) temperature (Kelvin) ) do c = bounds%begc, bounds%endc @@ -457,16 +456,23 @@ subroutine NVPWaterBalance_Column(bounds, dtime, waterfluxbulk_inst, waterstate_ ! --- NVP volumetric water content (clamped to valid range) --- if (col%dz(c,0) > 0._r8) then - vol_liq = h2osoi_liq(c,0) / (denh2o * col%dz(c,0)) ! [m3 m-3] - vol_liq = max(0._r8, min(watsat_nvp, vol_liq)) + if (t_nvp_col(c) >= tfrz) then + ! For unfrozen soil + vol_ice = min(watsat_nvp, h2osoi_ice(c,0)/(col%dz(c,0)*denice)) + eff_porosity = watsat_nvp-vol_ice + vol_liq = min(eff_porosity, h2osoi_liq(c,0)/(col%dz(c,0)*denh2o)) + else + ! For frozen soil, assume NVP water content is at residual (unavailable for evaporation) + vol_liq = watres_nvp + end if else vol_liq = 0._r8 end if ! --- NVP van Genuchten matric potential and hydraulic conductivity --- - call NVPWaterRetentionCurve(vol_liq, n_van_nvp, alpha_van_nvp, & + call NVPWaterRetentionCurve(vol_liq, eff_porosity, alpha_van_nvp, & watsat_nvp, watres_nvp, psi_nvp) - call NVPHydraulicConductivity(vol_liq, n_van_nvp, watsat_nvp, watres_nvp, & + call NVPHydraulicConductivity(vol_liq, eff_porosity, watsat_nvp, watres_nvp, & ksat_nvp, khydr_nvp) K_nvp_mms = khydr_nvp * 1000._r8 ! m/s → mm/s diff --git a/src/biogeophys/SurfaceHumidityMod.F90 b/src/biogeophys/SurfaceHumidityMod.F90 index 87024f3eb7..207abb88b7 100644 --- a/src/biogeophys/SurfaceHumidityMod.F90 +++ b/src/biogeophys/SurfaceHumidityMod.F90 @@ -156,12 +156,21 @@ subroutine CalculateSurfaceHumidity(bounds, & ! Compute NVP surface humidity as a function of NVP water retention curve ! --- NVP volumetric water content (clamped to valid range) --- if (dz(c,0) > 0._r8) then - vol_ice = min(watsat_nvp, h2osoi_ice(c,0)/(dz(c,0)*denice)) - eff_porosity = watsat_nvp-vol_ice - vol_liq = min(eff_porosity, h2osoi_liq(c,0)/(dz(c,0)*denh2o)) - psit_nvp = NVPWaterRetentionCurve(vol_liq, n_van_nvp, alpha_van_nvp, & + if (t_soisno(c,0) >= tfrz) then + ! For unfrozen soil + vol_ice = min(watsat_nvp, h2osoi_ice(c,0)/(dz(c,0)*denice)) + eff_porosity = watsat_nvp-vol_ice + vol_liq = min(eff_porosity, h2osoi_liq(c,0)/(dz(c,0)*denh2o)) + else + ! For frozen soil, assume NVP water content is at residual (unavailable for evaporation) + vol_liq = watres_nvp + end if + psit_nvp = NVPWaterRetentionCurve(vol_liq, eff_porosity, alpha_van_nvp, & watsat_nvp, watres_nvp, psi_nvp) hr_nvp = exp(psit_nvp/roverg/t_nvp_col(c)) + else + ! If dz(c,0) is not positive, set hr_nvp to 0 + hr_nvp = 0._r8 end if else hr_nvp = 0._r8 From fea0c85d8cf5c60d795e8c96a83aa610defa82ac Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Sun, 12 Apr 2026 22:25:26 +0300 Subject: [PATCH 022/113] Add NVP water state variable. --- src/biogeophys/WaterStateType.F90 | 37 ++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/src/biogeophys/WaterStateType.F90 b/src/biogeophys/WaterStateType.F90 index 35441d65d9..8a666baf7f 100644 --- a/src/biogeophys/WaterStateType.F90 +++ b/src/biogeophys/WaterStateType.F90 @@ -13,7 +13,8 @@ module WaterStateType use abortutils , only : endrun use decompMod , only : bounds_type use decompMod , only : subgrid_level_patch, subgrid_level_column, subgrid_level_landunit, subgrid_level_gridcell - use clm_varctl , only : use_bedrock, use_excess_ice, iulog + ! [PORTED by Hui Tang: add use_nvp for nvp (moss/lichen) water content field] + use clm_varctl , only : use_bedrock, use_excess_ice, iulog, use_nvp use spmdMod , only : masterproc use clm_varctl , only : use_fates, use_hillslope use clm_varpar , only : nlevgrnd, nlevsoi, nlevurb, nlevmaxurbgrnd, nlevsno @@ -40,6 +41,8 @@ module WaterStateType real(r8), pointer :: h2osoi_vol_col (:,:) ! col volumetric soil water (0<=h2osoi_vol<=watsat) [m3/m3] (nlevgrnd) real(r8), pointer :: h2osoi_vol_prs_grc (:,:) ! grc volumetric soil water prescribed (0<=h2osoi_vol<=watsat) [m3/m3] (nlevgrnd) real(r8), pointer :: h2osfc_col (:) ! col surface water (mm H2O) + ! [PORTED by Hui Tang: nvp (moss/lichen) column water content] + real(r8), pointer :: h2onvp_col (:) ! col nvp (moss/lichen) water content (mm H2O) real(r8), pointer :: snocan_patch (:) ! patch canopy snow water (mm H2O) real(r8), pointer :: liqcan_patch (:) ! patch canopy liquid water (mm H2O) @@ -151,6 +154,10 @@ subroutine InitAllocate(this, bounds, tracer_vars) call AllocateVar1d(var = this%h2osfc_col, name = 'h2osfc_col', & container = tracer_vars, & bounds = bounds, subgrid_level = subgrid_level_column) + ! [PORTED by Hui Tang: allocate nvp (moss/lichen) column water content via tracer container] + call AllocateVar1d(var = this%h2onvp_col, name = 'h2onvp_col', & + container = tracer_vars, & + bounds = bounds, subgrid_level = subgrid_level_column) call AllocateVar1d(var = this%wa_col, name = 'wa_col', & container = tracer_vars, & bounds = bounds, subgrid_level = subgrid_level_column) @@ -182,7 +189,7 @@ subroutine InitHistory(this, bounds, use_aquifer_layer) ! ! !USES: use histFileMod , only : hist_addfld1d, hist_addfld2d, no_snow_normal - use clm_varctl , only : use_soil_moisture_streams + use clm_varctl , only : use_soil_moisture_streams, use_nvp use GridcellType , only : grc ! ! !ARGUMENTS: @@ -317,6 +324,16 @@ subroutine InitHistory(this, bounds, use_aquifer_layer) ! can be provided through FATES specific history diagnostics ! if need be. + ! [PORTED by Hui Tang: register nvp (moss/lichen) water content history field] + if (use_nvp) then + this%h2onvp_col(begc:endc) = spval + call hist_addfld1d ( & + fname=this%info%fname('H2ONVP'), & + units='mm', & + avgflag='A', & + long_name=this%info%lname('nvp (moss/lichen) water content'), & + ptr_col=this%h2onvp_col, default='active') + end if end subroutine InitHistory @@ -360,6 +377,8 @@ subroutine InitCold(this, bounds, & associate(snl => col%snl) this%h2osfc_col(bounds%begc:bounds%endc) = 0._r8 + ! [PORTED by Hui Tang: initialize nvp (moss/lichen) water content to 0] + this%h2onvp_col(bounds%begc:bounds%endc) = 0._r8 this%snocan_patch(bounds%begp:bounds%endp) = 0._r8 this%liqcan_patch(bounds%begp:bounds%endp) = 0._r8 this%stream_water_volume_lun(bounds%begl:bounds%endl) = 0._r8 @@ -601,7 +620,7 @@ subroutine Restart(this, bounds, ncid, flag, & use landunit_varcon , only : istcrop, istdlak, istsoil use column_varcon , only : icol_roof, icol_sunwall, icol_shadewall use clm_time_manager , only : is_first_step, is_restart - use clm_varctl , only : bound_h2osoi, nsrest, nsrContinue + use clm_varctl , only : bound_h2osoi, nsrest, nsrContinue, use_nvp use ncdio_pio , only : file_desc_t, ncd_double use ExcessIceStreamType, only : UseExcessIceStreams use restUtilMod , only : restartvar, RestartExcessIceIssue @@ -850,6 +869,18 @@ subroutine Restart(this, bounds, ncid, flag, & endif ! end if if-read flag + ! [PORTED by Hui Tang: restart I/O for nvp (moss/lichen) water content] + if (use_nvp) then + call restartvar(ncid=ncid, flag=flag, varname=this%info%fname('H2ONVP'), & + xtype=ncd_double, dim1name='column', & + long_name=this%info%lname('nvp (moss/lichen) water content'), & + units='mm', & + interpinic_flag='interp', readvar=readvar, data=this%h2onvp_col) + if (flag == 'read' .and. .not. readvar) then + this%h2onvp_col(bounds%begc:bounds%endc) = 0.0_r8 + end if + end if + end subroutine Restart !----------------------------------------------------------------------- From 6caec1bf5ea27e9d18b24af690a0a7a0f3b83b53 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Sun, 12 Apr 2026 22:28:49 +0300 Subject: [PATCH 023/113] Add NVP water flux variables. --- src/biogeophys/WaterFluxBulkType.F90 | 42 +++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/src/biogeophys/WaterFluxBulkType.F90 b/src/biogeophys/WaterFluxBulkType.F90 index eb0a1d3303..8b5f8730f4 100644 --- a/src/biogeophys/WaterFluxBulkType.F90 +++ b/src/biogeophys/WaterFluxBulkType.F90 @@ -37,6 +37,12 @@ module WaterFluxBulkType real(r8), pointer :: qflx_ev_soil_col (:) ! col evaporation heat flux from soil (mm H2O/s) [+ to atm] real(r8), pointer :: qflx_ev_h2osfc_patch (:) ! patch evaporation heat flux from soil (mm H2O/s) [+ to atm] real(r8), pointer :: qflx_ev_h2osfc_col (:) ! col evaporation heat flux from soil (mm H2O/s) [+ to atm] + ! [PORTED by Hui Tang: NVP (moss/lichen) ground evaporation flux] + real(r8), pointer :: qflx_ev_nvp_patch (:) ! patch evaporation flux from NVP (mm H2O/s) [+ to atm] + real(r8), pointer :: qflx_ev_nvp_col (:) ! col evaporation flux from NVP (mm H2O/s) [+ to atm] + ! [PORTED by Hui Tang: NVP water infiltration and drainage fluxes] + real(r8), pointer :: qflx_nvp_infl_col (:) ! col water arriving at top of NVP layer (mm H2O/s) [diagnostic] + real(r8), pointer :: qflx_nvp_drain_col (:) ! col drainage from NVP layer 0 to soil layer 1 (mm H2O/s) real(r8), pointer :: qflx_adv_col (:,:) ! col advective flux across different soil layer interfaces [mm H2O/s] [+ downward] real(r8), pointer :: qflx_rootsoi_col (:,:) ! col root and soil water exchange [mm H2O/s] [+ into root] @@ -122,6 +128,12 @@ subroutine InitBulkAllocate(this, bounds) allocate( this%qflx_ev_soil_col (begc:endc)) ; this%qflx_ev_soil_col (:) = nan allocate( this%qflx_ev_h2osfc_patch (begp:endp)) ; this%qflx_ev_h2osfc_patch (:) = nan allocate( this%qflx_ev_h2osfc_col (begc:endc)) ; this%qflx_ev_h2osfc_col (:) = nan + ! [PORTED by Hui Tang: allocate NVP evaporation flux arrays] + allocate( this%qflx_ev_nvp_patch (begp:endp)) ; this%qflx_ev_nvp_patch (:) = nan + allocate( this%qflx_ev_nvp_col (begc:endc)) ; this%qflx_ev_nvp_col (:) = nan + ! [PORTED by Hui Tang: allocate NVP infiltration and drainage flux arrays] + allocate( this%qflx_nvp_infl_col (begc:endc)) ; this%qflx_nvp_infl_col (:) = nan + allocate( this%qflx_nvp_drain_col (begc:endc)) ; this%qflx_nvp_drain_col (:) = nan allocate(this%qflx_drain_vr_col (begc:endc,1:nlevsoi)) ; this%qflx_drain_vr_col (:,:) = nan allocate(this%qflx_adv_col (begc:endc,0:nlevsoi)) ; this%qflx_adv_col (:,:) = nan @@ -147,6 +159,7 @@ subroutine InitBulkHistory(this, bounds) ! ! !USES: use histFileMod , only : hist_addfld1d, hist_addfld2d, no_snow_normal + use clm_varctl , only : use_nvp ! [PORTED by Hui Tang: NVP history fields] ! ! !ARGUMENTS: class(waterfluxbulk_type), intent(in) :: this @@ -244,7 +257,34 @@ subroutine InitBulkHistory(this, bounds) avgflag='A', & long_name=this%info%lname('Annual ET'), & ptr_col=this%AnnET, c2l_scale_type='urbanf', default='inactive') - + + ! [PORTED by Hui Tang: history fields for NVP (moss/lichen) water fluxes] + if (use_nvp) then + this%qflx_ev_nvp_patch(begp:endp) = spval + call hist_addfld1d ( & + fname=this%info%fname('QFLX_EV_NVP'), units='mm/s', & + avgflag='A', long_name=this%info%lname('evaporation flux from nvp (moss/lichen)'), & + ptr_patch=this%qflx_ev_nvp_patch, c2l_scale_type='urbanf', default='inactive') + + this%qflx_ev_nvp_col(begc:endc) = spval + call hist_addfld1d ( & + fname=this%info%fname('QFLX_EV_NVP_COL'), units='mm/s', & + avgflag='A', long_name=this%info%lname('column evaporation flux from nvp (moss/lichen)'), & + ptr_col=this%qflx_ev_nvp_col, c2l_scale_type='urbanf', default='inactive') + + this%qflx_nvp_infl_col(begc:endc) = spval + call hist_addfld1d ( & + fname=this%info%fname('QFLX_NVP_INFL'), units='mm/s', & + avgflag='A', long_name=this%info%lname('water arriving at top of nvp (moss/lichen) layer'), & + ptr_col=this%qflx_nvp_infl_col, c2l_scale_type='urbanf', default='inactive') + + this%qflx_nvp_drain_col(begc:endc) = spval + call hist_addfld1d ( & + fname=this%info%fname('QFLX_NVP_DRAIN'), units='mm/s', & + avgflag='A', long_name=this%info%lname('drainage from nvp (moss/lichen) layer to soil'), & + ptr_col=this%qflx_nvp_drain_col, c2l_scale_type='urbanf', default='inactive') + end if + end subroutine InitBulkHistory From 56a9757cf49d767895db7d4c2090087414a30bb6 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Sun, 12 Apr 2026 22:35:08 +0300 Subject: [PATCH 024/113] Add diagnostic NVP water variables. --- src/biogeophys/WaterDiagnosticBulkType.F90 | 44 ++++++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/src/biogeophys/WaterDiagnosticBulkType.F90 b/src/biogeophys/WaterDiagnosticBulkType.F90 index 48aeef73aa..8413ca2280 100644 --- a/src/biogeophys/WaterDiagnosticBulkType.F90 +++ b/src/biogeophys/WaterDiagnosticBulkType.F90 @@ -16,7 +16,8 @@ module WaterDiagnosticBulkType use shr_log_mod , only : errMsg => shr_log_errMsg use decompMod , only : bounds_type use abortutils , only : endrun - use clm_varctl , only : use_cn, iulog, use_luna, use_hillslope + ! [PORTED by Hui Tang: add use_nvp for nvp (moss/lichen) wet fraction field] + use clm_varctl , only : use_cn, iulog, use_luna, use_hillslope, use_nvp use clm_varpar , only : nlevgrnd, nlevsno, nlevcan, nlevsoi use clm_varcon , only : spval use LandunitType , only : lun @@ -76,6 +77,9 @@ module WaterDiagnosticBulkType real(r8), pointer :: wf_col (:) ! col soil water as frac. of whc for top 0.05 m (0-1) real(r8), pointer :: wf2_col (:) ! col soil water as frac. of whc for top 0.17 m (0-1) real(r8), pointer :: fwet_patch (:) ! patch canopy fraction that is wet (0 to 1) + ! [PORTED by Hui Tang: nvp (moss/lichen) column wet fraction and volumetric water content] + real(r8), pointer :: fwet_nvp_col (:) ! col nvp (moss/lichen) wet fraction (0 to 1) + real(r8), pointer :: vwc_nvp_col (:) ! col nvp (moss/lichen) volumetric liquid water content (m3 m-3) real(r8), pointer :: fcansno_patch (:) ! patch canopy fraction that is snow covered (0 to 1) real(r8), pointer :: fdry_patch (:) ! patch canopy fraction of foliage that is green and dry [-] (new) @@ -230,6 +234,9 @@ subroutine InitBulkAllocate(this, bounds) allocate(this%wf_col (begc:endc)) ; this%wf_col (:) = nan allocate(this%wf2_col (begc:endc)) ; this%wf2_col (:) = nan allocate(this%fwet_patch (begp:endp)) ; this%fwet_patch (:) = nan + ! [PORTED by Hui Tang: allocate nvp (moss/lichen) column wet fraction and VWC, initialized to 0] + allocate(this%fwet_nvp_col (begc:endc)) ; this%fwet_nvp_col (:) = 0.0_r8 + allocate(this%vwc_nvp_col (begc:endc)) ; this%vwc_nvp_col (:) = 0.0_r8 allocate(this%fcansno_patch (begp:endp)) ; this%fcansno_patch (:) = nan allocate(this%fdry_patch (begp:endp)) ; this%fdry_patch (:) = nan allocate(this%qflx_prec_intr_patch (begp:endp)) ; this%qflx_prec_intr_patch (:) = nan @@ -247,7 +254,7 @@ subroutine InitBulkHistory(this, bounds) ! !USES: use shr_infnan_mod , only : nan => shr_infnan_nan, assignment(=) use histFileMod , only : hist_addfld1d, hist_addfld2d, no_snow_normal, no_snow_zero - use clm_varctl , only : use_excess_ice + use clm_varctl , only : use_excess_ice, use_nvp ! ! !ARGUMENTS: class(waterdiagnosticbulk_type), intent(in) :: this @@ -374,6 +381,25 @@ subroutine InitBulkHistory(this, bounds) ptr_patch=this%rh10_af_patch, set_spec=spval, default='inactive') endif + ! [PORTED by Hui Tang: register nvp (moss/lichen) wet fraction and VWC history fields] + if (use_nvp) then + this%fwet_nvp_col(begc:endc) = spval + call hist_addfld1d ( & + fname=this%info%fname('FWET_NVP'), & + units='proportion', & + avgflag='A', & + long_name=this%info%lname('nvp (moss/lichen) wet fraction'), & + ptr_col=this%fwet_nvp_col, default='active') + + this%vwc_nvp_col(begc:endc) = spval + call hist_addfld1d ( & + fname=this%info%fname('VWC_NVP'), & + units='m3 m-3', & + avgflag='A', & + long_name=this%info%lname('nvp (moss/lichen) volumetric liquid water content'), & + ptr_col=this%vwc_nvp_col, default='active') + end if + ! Fractions this%frac_h2osfc_col(begc:endc) = spval @@ -794,7 +820,7 @@ subroutine RestartBulk(this, bounds, ncid, flag, writing_finidat_interp_dest_fil use spmdMod , only : masterproc use clm_varcon , only : pondmx, watmin, spval, nameg use column_varcon , only : icol_roof, icol_sunwall, icol_shadewall - use clm_varctl , only : bound_h2osoi, use_excess_ice, nsrest, nsrContinue + use clm_varctl , only : bound_h2osoi, use_excess_ice, nsrest, nsrContinue, use_nvp use ncdio_pio , only : file_desc_t, ncd_io, ncd_double use restUtilMod use ExcessIceStreamType, only : UseExcessIceStreams @@ -989,6 +1015,18 @@ subroutine RestartBulk(this, bounds, ncid, flag, writing_finidat_interp_dest_fil end if endif + ! [PORTED by Hui Tang: restart I/O for nvp (moss/lichen) wet fraction] + if (use_nvp) then + call restartvar(ncid=ncid, flag=flag, varname=this%info%fname('FWET_NVP'), & + xtype=ncd_double, dim1name='column', & + long_name=this%info%lname('nvp (moss/lichen) wet fraction'), & + units='proportion', & + interpinic_flag='interp', readvar=readvar, data=this%fwet_nvp_col) + if (flag == 'read' .and. .not. readvar) then + this%fwet_nvp_col(bounds%begc:bounds%endc) = 0.0_r8 + end if + end if + end subroutine RestartBulk !----------------------------------------------------------------------- From 5d4c676f885a10d9c55fc8cca96ee2eb1ac6b15f Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Sun, 12 Apr 2026 22:36:00 +0300 Subject: [PATCH 025/113] Add NVP surface specific humidity. --- src/biogeophys/WaterDiagnosticType.F90 | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/biogeophys/WaterDiagnosticType.F90 b/src/biogeophys/WaterDiagnosticType.F90 index 57be0e62af..7177ca0822 100644 --- a/src/biogeophys/WaterDiagnosticType.F90 +++ b/src/biogeophys/WaterDiagnosticType.F90 @@ -47,6 +47,8 @@ module WaterDiagnosticType real(r8), pointer :: qg_snow_col (:) ! col ground specific humidity [kg/kg] real(r8), pointer :: qg_soil_col (:) ! col ground specific humidity [kg/kg] real(r8), pointer :: qg_h2osfc_col (:) ! col ground specific humidity [kg/kg] + ! [PORTED by Hui Tang: NVP surface specific humidity, used in ground evap blending] + real(r8), pointer :: qg_nvp_col (:) ! col NVP surface specific humidity [kg/kg] real(r8), pointer :: qg_col (:) ! col ground specific humidity [kg/kg] real(r8), pointer :: qaf_lun (:) ! lun urban canopy air specific humidity (kg/kg) @@ -129,6 +131,10 @@ subroutine InitAllocate(this, bounds, tracer_vars) call AllocateVar1d(var = this%qg_h2osfc_col, name = 'qg_h2osfc_col', & container = tracer_vars, & bounds = bounds, subgrid_level = subgrid_level_column) + ! [PORTED by Hui Tang: NVP surface specific humidity, used in ground evap blending] + call AllocateVar1d(var = this%qg_nvp_col, name = 'qg_nvp_col', & + container = tracer_vars, & + bounds = bounds, subgrid_level = subgrid_level_column) call AllocateVar1d(var = this%qg_col, name = 'qg_col', & container = tracer_vars, & bounds = bounds, subgrid_level = subgrid_level_column) From b8109a6577dfa20bf02ba71dc62f1ed88257a59a Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Sun, 12 Apr 2026 22:40:36 +0300 Subject: [PATCH 026/113] Modify soil surface water flux due to NVP presence. --- src/biogeophys/SoilHydrologyMod.F90 | 33 ++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/src/biogeophys/SoilHydrologyMod.F90 b/src/biogeophys/SoilHydrologyMod.F90 index 753ddc59fc..92eeafbca6 100644 --- a/src/biogeophys/SoilHydrologyMod.F90 +++ b/src/biogeophys/SoilHydrologyMod.F90 @@ -9,7 +9,8 @@ module SoilHydrologyMod use shr_log_mod , only : errMsg => shr_log_errMsg use abortutils , only : endrun use decompMod , only : bounds_type, subgrid_level_column - use clm_varctl , only : iulog, use_vichydro + ! [PORTED by Hui Tang: add use_nvp for NVP water infiltration] + use clm_varctl , only : iulog, use_vichydro, use_nvp use clm_varcon , only : ispval use clm_varcon , only : denh2o, denice, rpi use clm_varcon , only : pondmx_urban @@ -310,8 +311,10 @@ subroutine SetQflxInputs(bounds, num_hydrologyc, filter_hydrologyc, & ! ! !LOCAL VARIABLES: integer :: fc, c - real(r8) :: qflx_evap ! evaporation for this column - real(r8) :: fsno ! copy of frac_sno + real(r8) :: qflx_evap ! evaporation for this column + real(r8) :: fsno ! copy of frac_sno + ! [PORTED by Hui Tang: NVP water infiltration] + real(r8) :: frac_nvp_eff ! effective NVP area fraction (not covered by h2osfc) [-] character(len=*), parameter :: subname = 'SetQflxInputs' !----------------------------------------------------------------------- @@ -341,6 +344,7 @@ subroutine SetQflxInputs(bounds, num_hydrologyc, filter_hydrologyc, & ! ------------------------------------------------------------------------ ! Partition surface inputs between soil and h2osfc + ! [PORTED by Hui Tang: also partition out the NVP fraction] ! ------------------------------------------------------------------------ if (snl(c) >= 0) then @@ -352,11 +356,21 @@ subroutine SetQflxInputs(bounds, num_hydrologyc, filter_hydrologyc, & qflx_evap=qflx_ev_soil(c) endif - qflx_in_soil(c) = (1._r8 - frac_h2osfc(c)) * (qflx_top_soil(c) - qflx_sat_excess_surf(c)) + ! [PORTED by Hui Tang: compute effective NVP area (not blocked by surface water, + ! but snow does not block — water percolates through to NVP layer beneath snow)] + if (use_nvp) then + frac_nvp_eff = min(col%frac_nvp(c), max(0._r8, 1._r8 - frac_h2osfc(c))) + else + frac_nvp_eff = 0._r8 + end if + + ! Soil receives the non-h2osfc, non-NVP fraction; NVP handled by NVPWaterBalance_Column + qflx_in_soil(c) = (1._r8 - frac_h2osfc(c) - frac_nvp_eff) * & + (qflx_top_soil(c) - qflx_sat_excess_surf(c)) qflx_top_soil_to_h2osfc(c) = frac_h2osfc(c) * (qflx_top_soil(c) - qflx_sat_excess_surf(c)) - ! remove evaporation (snow treated in SnowHydrology) - qflx_in_soil(c) = qflx_in_soil(c) - (1.0_r8 - fsno - frac_h2osfc(c))*qflx_evap + ! remove evaporation from bare-soil fraction only (snow and NVP evap handled separately) + qflx_in_soil(c) = qflx_in_soil(c) - (1.0_r8 - fsno - frac_h2osfc(c) - frac_nvp_eff)*qflx_evap qflx_top_soil_to_h2osfc(c) = qflx_top_soil_to_h2osfc(c) - frac_h2osfc(c) * qflx_ev_h2osfc(c) end do @@ -444,12 +458,15 @@ subroutine Infiltration(bounds, num_hydrologyc, filter_hydrologyc, & associate( & qflx_infl => waterfluxbulk_inst%qflx_infl_col , & ! Output: [real(r8) (:) ] infiltration (mm H2O /s) qflx_in_soil_limited => waterfluxbulk_inst%qflx_in_soil_limited_col , & ! Input: [real(r8) (:) ] surface input to soil, limited by max infiltration rate (mm H2O /s) - qflx_h2osfc_drain => waterfluxbulk_inst%qflx_h2osfc_drain_col & ! Input: [real(r8) (:) ] bottom drainage from h2osfc (mm H2O /s) + qflx_h2osfc_drain => waterfluxbulk_inst%qflx_h2osfc_drain_col , & ! Input: [real(r8) (:) ] bottom drainage from h2osfc (mm H2O /s) + ! [PORTED by Hui Tang: NVP drainage to soil layer 1 enters total infiltration] + qflx_nvp_drain_col => waterfluxbulk_inst%qflx_nvp_drain_col & ! Input: [real(r8) (:) ] drainage from NVP layer 0 to soil layer 1 (mm H2O /s) ) do fc = 1, num_hydrologyc c = filter_hydrologyc(fc) - qflx_infl(c) = qflx_in_soil_limited(c) + qflx_h2osfc_drain(c) + ! [PORTED by Hui Tang: add NVP drainage to total infiltration into soil layer 1] + qflx_infl(c) = qflx_in_soil_limited(c) + qflx_h2osfc_drain(c) + qflx_nvp_drain_col(c) end do end associate From 85d56f953ee5229c4189f137e5e50979a82f0814 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Sun, 12 Apr 2026 22:42:56 +0300 Subject: [PATCH 027/113] Add NVP layer water balance calculation. --- src/biogeophys/HydrologyNoDrainageMod.F90 | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/biogeophys/HydrologyNoDrainageMod.F90 b/src/biogeophys/HydrologyNoDrainageMod.F90 index 8f294c652e..48c3fba9e1 100644 --- a/src/biogeophys/HydrologyNoDrainageMod.F90 +++ b/src/biogeophys/HydrologyNoDrainageMod.F90 @@ -7,7 +7,9 @@ Module HydrologyNoDrainageMod use shr_kind_mod , only : r8 => shr_kind_r8 use shr_log_mod , only : errMsg => shr_log_errMsg use decompMod , only : bounds_type - use clm_varctl , only : iulog, use_vichydro, use_fates + ! [PORTED by Hui Tang: add use_nvp and NVPWaterBalance_Column for NVP water infiltration] + use clm_varctl , only : iulog, use_vichydro, use_fates, use_nvp + use NVPLayerDynamicsMod, only : NVPWaterBalance_Column use clm_varcon , only : denh2o, denice, rpi, spval use CLMFatesInterfaceMod, only : hlm_fates_interface_type use atm2lndType , only : atm2lnd_type @@ -309,6 +311,15 @@ subroutine HydrologyNoDrainage(bounds, & bounds, num_hydrologyc, filter_hydrologyc, lun, col, & soilhydrology_inst, soilstate_inst, b_waterflux_inst) + ! [PORTED by Hui Tang: NVP water balance — gravity drainage from NVP layer 0 to soil] + ! Must be after SnowWater (qflx_rain_plus_snomelt finalised) and after column + ! aggregation of qflx_ev_nvp_col (done in clm_driver p2c). Must be before + ! SetQflxInputs so qflx_nvp_drain_col is available for qflx_infl. + if (use_nvp) then + call NVPWaterBalance_Column(bounds, dtime, b_waterflux_inst, & + b_waterstate_inst, b_waterdiagnostic_inst, soilstate_inst, temperature_inst) + end if + call SetQflxInputs(bounds, num_hydrologyc, filter_hydrologyc, & b_waterflux_inst, b_waterdiagnostic_inst) From 50c6bf869508a8e134ad305356eedd5c4eb00434 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Sun, 12 Apr 2026 22:45:33 +0300 Subject: [PATCH 028/113] Add NVP photosynthesis wrapper. --- src/utils/clmfates_interfaceMod.F90 | 94 +++++++++++++++++++++++++++-- 1 file changed, 89 insertions(+), 5 deletions(-) diff --git a/src/utils/clmfates_interfaceMod.F90 b/src/utils/clmfates_interfaceMod.F90 index 5098caf283..368e1e1ea2 100644 --- a/src/utils/clmfates_interfaceMod.F90 +++ b/src/utils/clmfates_interfaceMod.F90 @@ -256,6 +256,8 @@ module CLMFatesInterfaceMod procedure, public :: wrap_sunfrac procedure, public :: wrap_btran procedure, public :: wrap_photosynthesis + ! [PORTED by Hui Tang: separate NVP photosynthesis call for clm_driver] + procedure, public :: wrap_nvp_photosynthesis procedure, public :: wrap_accumulatefluxes procedure, public :: prep_canopyfluxes procedure, public :: wrap_canopy_radiation @@ -377,7 +379,7 @@ subroutine CLMFatesGlobals1(surf_numpft,surf_numcft,maxsoil_patches) call set_fates_ctrlparms('parteh_mode',ival=fates_parteh_mode) - + ! [PORTED by Hui Tang: pass nvp (moss/lichen) switches to FATES] if (use_nvp) then pass_nvp = 1 @@ -1286,7 +1288,7 @@ subroutine dynamics_driv(this, nc, bounds_clump, & this%fates(nc)%bc_in(s)%wind24_pa(ifp) = & atm2lnd_inst%wind24_patch(p) - + end do ! Here we use the same logic as the pft_areafrac initialization to get an array with values for each pft @@ -1586,7 +1588,7 @@ subroutine wrap_update_hlmfates_dyn(this, nc, bounds_clump, & type(waterdiagnosticbulk_type) , intent(inout) :: waterdiagnosticbulk_inst type(canopystate_type) , intent(inout) :: canopystate_inst type(soilbiogeochem_carbonflux_type), intent(inout) :: soilbiogeochem_carbonflux_inst - + ! is this being called during a read from restart sequence (if so then use the restarted fates ! snow depth variable rather than the CLM variable). @@ -2817,6 +2819,89 @@ end subroutine wrap_photosynthesis ! ====================================================================================== + subroutine wrap_nvp_photosynthesis(this, nc, bounds, & + atm2lnd_inst, temperature_inst, waterdiagnosticbulk_inst) + + ! [PORTED by Hui Tang: separate NVP (moss/lichen) photosynthesis call. + ! + ! NVP lacks stomata so its photosynthesis must NOT go through the CanopyFluxes + ! iterative solver (which is designed for stomata-bearing vegetation and maps outputs + ! to rssun/rssha). Instead this routine is called once from clm_driver after + ! CanopyFluxes has converged. + ! + ! Role: re-run FatesPlantRespPhotosynthDrive with the correct NVP surface temperature + ! (t_nvp_pa) and wetness (fwet_nvp_pa) so that FATES accumulates accurate NVP carbon + ! fluxes (GPP, maintenance respiration) in bc_out. No CLM-side output is mapped back + ! (NVP has no stomatal resistance to write to rssun/rssha). + ! + ! waterdiagnosticbulk_inst (needed for fwet_nvp_col) is NOT available inside + ! wrap_photosynthesis / CanopyFluxes, which is the compile-time reason for this split.] + + use decompMod , only : bounds_type + use FatesPlantRespPhotosynthMod , only : FatesPlantRespPhotosynthDrive + use FatesSynchronizedParsMod , only : get_step_size_real + + ! !ARGUMENTS: + class(hlm_fates_interface_type), intent(inout) :: this + integer, intent(in) :: nc + type(bounds_type), intent(in) :: bounds + type(atm2lnd_type), intent(in) :: atm2lnd_inst + type(temperature_type), intent(in) :: temperature_inst + type(waterdiagnosticbulk_type), intent(in) :: waterdiagnosticbulk_inst + + integer :: s, c, p, ifp + real(r8) :: dtime + + call t_startf('fates_nvp_psn') + + associate( & + t_veg => temperature_inst%t_veg_patch , & + tgcm => temperature_inst%thm_patch , & + forc_pbot => atm2lnd_inst%forc_pbot_downscaled_col ) + + do s = 1, this%fates(nc)%nsites + + c = this%f2hmap(nc)%fcolumn(s) + + this%fates(nc)%bc_in(s)%forc_pbot = forc_pbot(c) + + do ifp = 1, this%fates(nc)%sites(s)%youngest_patch%patchno + p = ifp + col%patchi(c) + + ! Re-enable processing for all patches (reset from 3 → 2) + this%fates(nc)%bc_in(s)%filter_photo_pa(ifp) = 2 + + ! Update t_veg / tgcm with post-convergence values from CanopyFluxes + this%fates(nc)%bc_in(s)%t_veg_pa(ifp) = t_veg(p) + this%fates(nc)%bc_in(s)%tgcm_pa(ifp) = tgcm(p) + + ! [PORTED by Hui Tang: NVP-specific inputs — column quantities broadcast to patch] + this%fates(nc)%bc_in(s)%t_nvp_pa(ifp) = temperature_inst%t_nvp_col(c) + this%fates(nc)%bc_in(s)%fwet_nvp_pa(ifp) = waterdiagnosticbulk_inst%fwet_nvp_col(c) + + end do + end do + + dtime = get_step_size_real() + + ! Re-run FATES photosynthesis: this overwrites bc_out carbon fluxes with + ! values computed using correct post-convergence NVP temperature/wetness. + ! rssun/rssha are NOT mapped back — NVP has no stomata. + call FatesPlantRespPhotosynthDrive( & + this%fates(nc)%nsites, & + this%fates(nc)%sites, & + this%fates(nc)%bc_in, & + this%fates(nc)%bc_out, & + dtime) + + end associate + + call t_stopf('fates_nvp_psn') + + end subroutine wrap_nvp_photosynthesis + + ! ====================================================================================== + subroutine wrap_accumulatefluxes(this, nc, fn, filterp) ! !ARGUMENTS: @@ -3005,7 +3090,7 @@ subroutine wrap_canopy_radiation(this, bounds_clump, nc, fcansno, surfalb_inst) ftdd(p,:) = this%fates(nc)%bc_out(s)%ftdd_parb(ifp,:) ftid(p,:) = this%fates(nc)%bc_out(s)%ftid_parb(ifp,:) ftii(p,:) = this%fates(nc)%bc_out(s)%ftii_parb(ifp,:) - + end do ! [PORTED by Hui Tang: transfer NVP Beer's law absorptance from bc_out to surfalb_inst] @@ -4135,7 +4220,6 @@ subroutine GetLandusePFTData(bounds, landuse_pft_file, landuse_pft_map, landuse_ end subroutine GetLandusePFTData - !----------------------------------------------------------------------- end module CLMFatesInterfaceMod From 922ab69192d54027de672cc9a2dcab498ac9c709 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Sun, 12 Apr 2026 22:48:48 +0300 Subject: [PATCH 029/113] Add separate photosynthesis call for NVP. --- src/main/clm_driver.F90 | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/clm_driver.F90 b/src/main/clm_driver.F90 index 38d13a71f8..1c5fb555e7 100644 --- a/src/main/clm_driver.F90 +++ b/src/main/clm_driver.F90 @@ -14,6 +14,7 @@ module clm_driver use CNSharedParamsMod , only : use_matrixcn use clm_varctl , only : use_crop, irrigate, ndep_from_cpl use clm_varctl , only : use_soil_moisture_streams, fates_radiation_model + use clm_varctl , only : use_nvp ! [PORTED by Hui Tang: NVP photosynthesis flag] use clm_varctl , only : use_cropcal_streams, is_cold_start, nsrest, nsrStartup use clm_time_manager , only : get_nstep, is_beg_curr_day, is_beg_curr_year use clm_time_manager , only : get_prev_date, is_first_step @@ -779,6 +780,16 @@ subroutine clm_drv(doalb, nextsw_cday, declinp1, declin, rstwr, nlend, rdate, ro deallocate(downreg_patch, leafn_patch, froot_carbon, croot_carbon) call t_stopf('canflux') + ! [PORTED by Hui Tang: NVP (moss/lichen) photosynthesis — separate from CanopyFluxes. + ! NVP lacks stomata so it must not go through the CanopyFluxes iterative solver. + ! Called after CanopyFluxes convergence so that post-convergence t_veg and t_nvp_col + ! are available, and waterdiagnosticbulk_inst (needed for fwet_nvp_col) is in scope.] + if (use_fates .and. use_nvp) then + call clm_fates%wrap_nvp_photosynthesis(nc, bounds_clump, & + atm2lnd_inst, temperature_inst, & + water_inst%waterdiagnosticbulk_inst) + end if + ! Fluxes for all urban landunits call t_startf('uflux') From 6b68bdc9cc3dbfa7bd8451dfcf89238119e9f164 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Sun, 12 Apr 2026 22:51:07 +0300 Subject: [PATCH 030/113] Add call for building NVP filter. --- src/main/clm_driver.F90 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/clm_driver.F90 b/src/main/clm_driver.F90 index 1c5fb555e7..d172dc98ac 100644 --- a/src/main/clm_driver.F90 +++ b/src/main/clm_driver.F90 @@ -68,7 +68,7 @@ module clm_driver use ch4Mod , only : ch4, ch4_init_gridcell_balance_check, ch4_init_column_balance_check use VOCEmissionMod , only : VOCEmission ! - use filterMod , only : setFilters + use filterMod , only : setFilters, setNVPcFilter ! [PORTED by Hui Tang: NVP column filter] ! use atm2lndMod , only : downscale_forcings, set_atm2lnd_water_tracers use lnd2atmMod , only : lnd2atm @@ -906,6 +906,7 @@ subroutine clm_drv(doalb, nextsw_cday, declinp1, declin, rstwr, nlend, rdate, ro filter(nc)%num_urbanc , filter(nc)%urbanc, & filter(nc)%num_nolakep , filter(nc)%nolakep, & filter(nc)%num_nolakec , filter(nc)%nolakec, & + filter(nc)%num_nvpc , filter(nc)%nvpc, & ! [PORTED by Hui Tang: NVP column filter] atm2lnd_inst, urbanparams_inst, canopystate_inst, water_inst%waterstatebulk_inst, & water_inst%waterdiagnosticbulk_inst, water_inst%waterfluxbulk_inst, & solarabs_inst, soilstate_inst, energyflux_inst, temperature_inst, urbantv_inst) @@ -1213,6 +1214,10 @@ subroutine clm_drv(doalb, nextsw_cday, declinp1, declin, rstwr, nlend, rdate, ro ! call to reweight_wrapup, if it's needed at all. call setFilters( bounds_clump, glc_behavior ) + ! [PORTED by Hui Tang: rebuild NVP column filter after FATES dynamics + ! updates col%nvp_layer_active / jbot_sno via UpdateNVPLayer] + if (use_nvp) call setNVPcFilter(bounds_clump) + end if From 09c5ad02a14989b697e37668571d3125ca9b73c1 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Sun, 12 Apr 2026 22:52:18 +0300 Subject: [PATCH 031/113] Add patch to column integration for NVP evaporation flux. --- src/main/clm_driver.F90 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/clm_driver.F90 b/src/main/clm_driver.F90 index d172dc98ac..3699e79601 100644 --- a/src/main/clm_driver.F90 +++ b/src/main/clm_driver.F90 @@ -1711,6 +1711,11 @@ subroutine clm_drv_patch2col (bounds, & waterfluxbulk_inst%qflx_ev_h2osfc_patch(bounds%begp:bounds%endp), & waterfluxbulk_inst%qflx_ev_h2osfc_col(bounds%begc:bounds%endc)) + ! [PORTED by Hui Tang: aggregate NVP evaporation flux from patches to column] + call p2c (bounds, num_nolakec, filter_nolakec, & + waterfluxbulk_inst%qflx_ev_nvp_patch(bounds%begp:bounds%endp), & + waterfluxbulk_inst%qflx_ev_nvp_col(bounds%begc:bounds%endc)) + ! Averaging for patch water flux variables call p2c (bounds, num_nolakec, filter_nolakec, & From d05b7e3682fb520efe8e006072131c319b048265 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Sun, 12 Apr 2026 22:53:32 +0300 Subject: [PATCH 032/113] Add restart option for NVP layer. --- src/main/clm_instMod.F90 | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/clm_instMod.F90 b/src/main/clm_instMod.F90 index 7d9a0f6ad2..a2cbd1f695 100644 --- a/src/main/clm_instMod.F90 +++ b/src/main/clm_instMod.F90 @@ -9,6 +9,9 @@ module clm_instMod use decompMod , only : bounds_type use clm_varpar , only : ndecomp_pools, nlevdecomp_full use clm_varctl , only : use_cn, use_c13, use_c14, use_lch4, use_cndv, use_fates, use_fates_bgc + ! [PORTED by Hui Tang: NVP column geometry restart] + use clm_varctl , only : use_nvp + use NVPLayerDynamicsMod, only : NVPLayerRestart use clm_varctl , only : iulog use clm_varctl , only : use_crop, snow_cover_fraction_method, paramfile use clm_varctl , only : use_excess_ice @@ -628,6 +631,12 @@ subroutine clm_instRest(bounds, ncid, flag, writing_finidat_interp_dest_file) end if + ! [PORTED by Hui Tang: restart NVP column geometry — must follow FATES restart + ! so that FATES cohort state is already restored when NVP layer is reactivated] + if (use_nvp) then + call NVPLayerRestart(bounds, ncid, flag=flag) + end if + end subroutine clm_instRest end module clm_instMod From ec18da5c39b72fd490b6bee2f18f1d1688fa2f9a Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Tue, 21 Apr 2026 23:25:24 +0300 Subject: [PATCH 033/113] Remove redundant update of flx _absdv and flx_absiv for NVP layer when snow is absent. --- src/biogeophys/SurfaceRadiationMod.F90 | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/biogeophys/SurfaceRadiationMod.F90 b/src/biogeophys/SurfaceRadiationMod.F90 index 91b8ee07d2..430da65617 100644 --- a/src/biogeophys/SurfaceRadiationMod.F90 +++ b/src/biogeophys/SurfaceRadiationMod.F90 @@ -765,7 +765,7 @@ subroutine SurfaceRadiation(bounds, num_nourbanp, filter_nourbanp, & ! per unit flux incident on NVP). trd/tri are below-canopy direct/diffuse fluxes. ! sabg(p) is unchanged (ground total = NVP + soil via modified albedo). ! sabg_soil is corrected because it was computed using soil-only albedo (albsod). - if (use_nvp) then + if (use_nvp .and. col%nvp_layer_active(patch%column(p))) then sabg_lyr(p,0) = 0._r8 do ib = 1, nband sabg_lyr(p,0) = sabg_lyr(p,0) + & @@ -775,13 +775,6 @@ subroutine SurfaceRadiation(bounds, num_nourbanp, filter_nourbanp, & sabg_lyr(p,0) = max(0._r8, min(sabg_lyr(p,0), sabg_lyr(p,1))) sabg_lyr(p,1) = sabg_lyr(p,1) - sabg_lyr(p,0) sabg_soil(p) = sabg_soil(p) - sabg_lyr(p,0) - ! [PORTED by Hui Tang: no-snow - store VIS canopy transmittances for NVP photosynthesis PAR] - ! ftdd(p,1) and ftii(p,1) are dimensionless fractions of direct/diffuse VIS - ! reaching the ground (below vascular canopy). Stored in layer 0 of the SNICAR - ! flx_abs arrays (unused by SNICAR when snl==0). Retrieved in wrap_sunfrac - ! at the next timestep for bc_in%flx_absdv/flx_absiv (one-timestep lag). - flx_absdv(c,:) = surfalb_inst%fabd_nvp_col(c,:) - flx_absiv(c,:) = surfalb_inst%fabi_nvp_col(c,:) end if ! CASE 2: Snow layers present: absorbed radiation is scaled according to From 2bfdf63815d6b819ac357bc66bd6ad6d928b3292 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Sun, 26 Apr 2026 10:45:45 +0300 Subject: [PATCH 034/113] Fixing bugs during compiling phase, no scientific changes. --- src/biogeophys/NVPLayerDynamicsMod.F90 | 36 ++++++++++++++++---------- src/biogeophys/SnowSnicarMod.F90 | 4 ++- src/biogeophys/SoilTemperatureMod.F90 | 11 +++++++- src/biogeophys/SurfaceHumidityMod.F90 | 8 +++--- src/main/clm_driver.F90 | 2 +- src/main/clm_instMod.F90 | 2 +- src/utils/clmfates_interfaceMod.F90 | 10 ++++--- 7 files changed, 47 insertions(+), 26 deletions(-) diff --git a/src/biogeophys/NVPLayerDynamicsMod.F90 b/src/biogeophys/NVPLayerDynamicsMod.F90 index c3ef50b6ff..a60e8a9a65 100644 --- a/src/biogeophys/NVPLayerDynamicsMod.F90 +++ b/src/biogeophys/NVPLayerDynamicsMod.F90 @@ -341,7 +341,7 @@ subroutine NVPEvaporation(theta_nvp, t_nvp, forc_pbot, rho_atm, q_atm, raw, & rnvp = rnvp_min + rnvp_amp * (1.0_r8 - satfrac)**rnvp_exp ! --- 3. Van Genuchten matric potential [mm] --- - call NVPWaterRetentionCurve(theta_nvp, n_van, alpha_van, watsat, watres, psi_nvp) + call NVPWaterRetentionCurve(theta_nvp, eff_porosity, n_van, alpha_van, watsat, watres, psi_nvp) ! --- 4. Kelvin activity correction: alpha = exp(psi / (roverg * T)) --- ! roverg = R_w/g * 1000 [mm K] so that psi [mm] / (roverg [mm K] * T [K]) is @@ -408,11 +408,16 @@ subroutine NVPWaterBalance_Column(bounds, dtime, waterfluxbulk_inst, waterstate_ class(waterstate_type), intent(inout) :: waterstate_inst type(waterdiagnosticbulk_type), intent(inout) :: waterdiagnosticbulk_inst type(soilstate_type), intent(in) :: soilstate_inst + ! [PORTED by Hui Tang: temperature_inst needed for ice/liquid partitioning of NVP layer] + type(temperature_type), intent(in) :: temperature_inst integer :: c real(r8) :: frac_h2osfc ! fractional area with surface water [-] real(r8) :: frac_nvp_eff ! effective NVP area fraction (not covered by h2osfc) [-] real(r8) :: vol_liq ! NVP volumetric liquid water content [m3 m-3] + ! [PORTED by Hui Tang: locals for ice partitioning of NVP layer water] + real(r8) :: vol_ice ! NVP volumetric ice content [m3 m-3] + real(r8) :: eff_porosity ! effective porosity (watsat - vol_ice) [m3 m-3] real(r8) :: khydr_nvp ! NVP unsaturated hydraulic conductivity [m s-1] real(r8) :: K_nvp_mms ! khydr_nvp converted to mm/s real(r8) :: K_soil1 ! soil layer 1 hydraulic conductivity [mm/s] @@ -430,12 +435,13 @@ subroutine NVPWaterBalance_Column(bounds, dtime, waterfluxbulk_inst, waterstate_ qflx_nvp_infl_col => waterfluxbulk_inst%qflx_nvp_infl_col, & qflx_nvp_drain_col => waterfluxbulk_inst%qflx_nvp_drain_col, & h2osoi_liq => waterstate_inst%h2osoi_liq_col, & + h2osoi_ice => waterstate_inst%h2osoi_ice_col, & ! [PORTED by Hui Tang: ice content for porosity reduction] h2onvp_col => waterstate_inst%h2onvp_col, & ! [PORTED by Hui Tang: sync diagnostic copy] smp_l => soilstate_inst%smp_l_col, & hk_l => soilstate_inst%hk_l_col, & frac_h2osfc_col => waterdiagnosticbulk_inst%frac_h2osfc_col, & fwet_nvp_col => waterdiagnosticbulk_inst%fwet_nvp_col, & - vwc_nvp_col => waterdiagnosticbulk_inst%vwc_nvp_col & ! [PORTED by Hui Tang: volumetric water content] + vwc_nvp_col => waterdiagnosticbulk_inst%vwc_nvp_col, & ! [PORTED by Hui Tang: volumetric water content] t_nvp_col => temperature_inst%t_nvp_col & ! Input: [real(r8) (:) ] NVP (moss/lichen) temperature (Kelvin) ) @@ -470,9 +476,9 @@ subroutine NVPWaterBalance_Column(bounds, dtime, waterfluxbulk_inst, waterstate_ end if ! --- NVP van Genuchten matric potential and hydraulic conductivity --- - call NVPWaterRetentionCurve(vol_liq, eff_porosity, alpha_van_nvp, & + call NVPWaterRetentionCurve(vol_liq, eff_porosity, n_van_nvp, alpha_van_nvp, & watsat_nvp, watres_nvp, psi_nvp) - call NVPHydraulicConductivity(vol_liq, eff_porosity, watsat_nvp, watres_nvp, & + call NVPHydraulicConductivity(vol_liq, eff_porosity, n_van_nvp, watsat_nvp, watres_nvp, & ksat_nvp, khydr_nvp) K_nvp_mms = khydr_nvp * 1000._r8 ! m/s → mm/s @@ -561,8 +567,10 @@ subroutine NVPLayerRestart(bounds, ncid, flag) ! h2onvp_col and t_nvp_col are restarted in WaterStateType and ! TemperatureType respectively. ! ------------------------------------------------------------------------- - use ncdio_pio , only : file_desc_t, ncd_double, ncd_int, ncd_log - use restFileMod , only : restartvar + use ncdio_pio , only : file_desc_t, ncd_double, ncd_int + ! [PORTED by Hui Tang: use restUtilMod (not restFileMod) for restartvar — restFileMod + ! uses clm_instMod, creating clm_instMod ↔ NVPLayerDynamicsMod cycle in build deps] + use restUtilMod , only : restartvar ! ! !ARGUMENTS: type(bounds_type) , intent(in) :: bounds @@ -591,14 +599,8 @@ subroutine NVPLayerRestart(bounds, ncid, flag) col%frac_nvp(bounds%begc:bounds%endc) = 0._r8 end if - ! Logical flag: .true. when NVP occupies layer index 0 - call restartvar(ncid=ncid, flag=flag, varname='NVP_LAYER_ACTIVE', xtype=ncd_log, & - dim1name='column', & - long_name='flag: NVP layer occupies vertical index 0', units='', & - interpinic_flag='interp', readvar=readvar, data=col%nvp_layer_active) - if (flag == 'read' .and. .not. readvar) then - col%nvp_layer_active(bounds%begc:bounds%endc) = .false. - end if + ! [PORTED by Hui Tang: nvp_layer_active is fully redundant with jbot_sno (active iff jbot_sno == -1). + ! restartvar has no logical-array overload, so we restart only JBOT_SNO and derive the flag below.] ! Bottom index of active snow: 0 = no NVP, -1 = NVP present at layer 0 call restartvar(ncid=ncid, flag=flag, varname='JBOT_SNO', xtype=ncd_int, & @@ -609,6 +611,12 @@ subroutine NVPLayerRestart(bounds, ncid, flag) col%jbot_sno(bounds%begc:bounds%endc) = 0 end if + ! Derive nvp_layer_active from jbot_sno on read (covers both restart and cold-start paths) + if (flag == 'read') then + col%nvp_layer_active(bounds%begc:bounds%endc) = & + (col%jbot_sno(bounds%begc:bounds%endc) == -1) + end if + end subroutine NVPLayerRestart end module NVPLayerDynamicsMod diff --git a/src/biogeophys/SnowSnicarMod.F90 b/src/biogeophys/SnowSnicarMod.F90 index 8cb6c2bdfa..a124210e80 100644 --- a/src/biogeophys/SnowSnicarMod.F90 +++ b/src/biogeophys/SnowSnicarMod.F90 @@ -187,9 +187,11 @@ subroutine readParams( ncid ) end subroutine readParams !----------------------------------------------------------------------- + ! [PORTED by Hui Tang: optional NVP layer-0 inputs (nvp_tau_col, nvp_omega_*_col) for SNICAR Approach B] subroutine SNICAR_RT (bounds, num_nourbanc, filter_nourbanc, & coszen, flg_slr_in, h2osno_liq, h2osno_ice, h2osno_total, snw_rds, & - mss_cnc_aer_in, albsfc, albout, flx_abs, waterdiagnosticbulk_inst) + mss_cnc_aer_in, albsfc, albout, flx_abs, waterdiagnosticbulk_inst, & + nvp_tau_col, nvp_omega_vis_col, nvp_omega_nir_col) ! ! !DESCRIPTION: ! Determine reflectance of, and vertically-resolved solar absorption in, diff --git a/src/biogeophys/SoilTemperatureMod.F90 b/src/biogeophys/SoilTemperatureMod.F90 index c66a283674..61fda6ad35 100644 --- a/src/biogeophys/SoilTemperatureMod.F90 +++ b/src/biogeophys/SoilTemperatureMod.F90 @@ -90,8 +90,10 @@ module SoilTemperatureMod contains !----------------------------------------------------------------------- + ! [PORTED by Hui Tang: added num_nvpc/filter_nvpc args for NVP-active column processing] subroutine SoilTemperature(bounds, num_urbanl, filter_urbanl, num_urbanc, filter_urbanc, & num_nolakep, filter_nolakep, num_nolakec, filter_nolakec, & + num_nvpc, filter_nvpc, & atm2lnd_inst, urbanparams_inst, canopystate_inst, waterstatebulk_inst, waterdiagnosticbulk_inst, waterfluxbulk_inst,& solarabs_inst, soilstate_inst, energyflux_inst, temperature_inst, urbantv_inst) ! @@ -327,6 +329,7 @@ subroutine SoilTemperature(bounds, num_urbanl, filter_urbanl, num_urbanc, filter tk_h2osfc(begc:endc) = nan call SoilThermProp(bounds, num_urbanc, filter_urbanc, num_nolakec, filter_nolakec, & + num_nvpc, filter_nvpc, & tk(begc:endc, :), & cv(begc:endc, :), & tk_h2osfc(begc:endc), & @@ -656,7 +659,9 @@ subroutine SoilTemperature(bounds, num_urbanl, filter_urbanl, num_urbanc, filter end subroutine SoilTemperature !----------------------------------------------------------------------- + ! [PORTED by Hui Tang: added num_nvpc/filter_nvpc args for NVP thermal property override] subroutine SoilThermProp (bounds, num_urbanc, filter_urbanc, num_nolakec, filter_nolakec, & + num_nvpc, filter_nvpc, & tk, cv, tk_h2osfc, & urbanparams_inst, temperature_inst, waterstatebulk_inst, waterdiagnosticbulk_inst, soilstate_inst) @@ -691,6 +696,8 @@ subroutine SoilThermProp (bounds, num_urbanc, filter_urbanc, num_nolakec, filter integer , intent(in) :: filter_urbanc(:) ! urban column filter integer , intent(in) :: num_nolakec ! number of column non-lake points in column filter integer , intent(in) :: filter_nolakec(:) ! column filter for non-lake points + integer , intent(in) :: num_nvpc ! [PORTED by Hui Tang: number of NVP-active columns] + integer , intent(in) :: filter_nvpc(:) ! [PORTED by Hui Tang: NVP-active column filter] real(r8) , intent(out) :: cv( bounds%begc: , -nlevsno+1: ) ! heat capacity [J/(m2 K) ] [col, lev] real(r8) , intent(out) :: tk( bounds%begc: , -nlevsno+1: ) ! thermal conductivity at the layer interface [W/(m K) ] [col, lev] real(r8) , intent(out) :: tk_h2osfc( bounds%begc: ) ! thermal conductivity of h2osfc [W/(m K) ] [col] @@ -1657,9 +1664,11 @@ subroutine Phasechange (bounds, num_nolakec, filter_nolakec, dhsdT, & end subroutine Phasechange !----------------------------------------------------------------------- + ! [PORTED by Hui Tang: added num_nvpc/filter_nvpc/hs_nvp args for NVP layer 0 heat flux] subroutine ComputeGroundHeatFluxAndDeriv(bounds, & num_nolakep, filter_nolakep, num_nolakec, filter_nolakec, & - hs_h2osfc, hs_top_snow, hs_soil, hs_top, dhsdT, sabg_lyr_col, & + num_nvpc, filter_nvpc, & + hs_h2osfc, hs_top_snow, hs_soil, hs_top, hs_nvp, dhsdT, sabg_lyr_col, & atm2lnd_inst, urbanparams_inst, canopystate_inst, waterdiagnosticbulk_inst, & waterfluxbulk_inst, solarabs_inst, energyflux_inst, temperature_inst) ! diff --git a/src/biogeophys/SurfaceHumidityMod.F90 b/src/biogeophys/SurfaceHumidityMod.F90 index 207abb88b7..d433add51e 100644 --- a/src/biogeophys/SurfaceHumidityMod.F90 +++ b/src/biogeophys/SurfaceHumidityMod.F90 @@ -165,8 +165,8 @@ subroutine CalculateSurfaceHumidity(bounds, & ! For frozen soil, assume NVP water content is at residual (unavailable for evaporation) vol_liq = watres_nvp end if - psit_nvp = NVPWaterRetentionCurve(vol_liq, eff_porosity, alpha_van_nvp, & - watsat_nvp, watres_nvp, psi_nvp) + call NVPWaterRetentionCurve(vol_liq, eff_porosity, n_van_nvp, alpha_van_nvp, & + watsat_nvp, watres_nvp, psit_nvp) hr_nvp = exp(psit_nvp/roverg/t_nvp_col(c)) else ! If dz(c,0) is not positive, set hr_nvp to 0 @@ -273,11 +273,11 @@ subroutine CalculateSurfaceHumidity(bounds, & if (frac_nvp_eff > 0._r8) then call QSat(t_nvp_col(c), forc_pbot(c), qsatg, & qsdT = qsatgdT_nvp) - qg_nvp(c) = hr_nvp(c) * qsatg + qg_nvp(c) = hr_nvp * qsatg ! Adjust qg and dqgdT: reduce bare-soil contribution by frac_nvp_eff, add NVP term qg(c) = qg(c) - frac_nvp_eff * qg_soil(c) + frac_nvp_eff * qg_nvp(c) dqgdT(c) = dqgdT(c) - frac_nvp_eff * hr * qsatgdT_soil & - + frac_nvp_eff * hr_nvp(c) * qsatgdT_nvp + + frac_nvp_eff * hr_nvp * qsatgdT_nvp end if else qg_nvp(c) = qg_soil(c) diff --git a/src/main/clm_driver.F90 b/src/main/clm_driver.F90 index 3699e79601..0851d7cfb1 100644 --- a/src/main/clm_driver.F90 +++ b/src/main/clm_driver.F90 @@ -640,7 +640,7 @@ subroutine clm_drv(doalb, nextsw_cday, declinp1, declin, rstwr, nlend, rdate, ro ! over the patch index range defined by bounds_clump%begp:bounds_proc%endp if(use_fates) then - call clm_fates%wrap_sunfrac(nc,atm2lnd_inst, canopystate_inst) + call clm_fates%wrap_sunfrac(nc, atm2lnd_inst, canopystate_inst, surfalb_inst) else call CanopySunShadeFracs(filter(nc)%nourbanp,filter(nc)%num_nourbanp, & atm2lnd_inst, surfalb_inst, canopystate_inst, & diff --git a/src/main/clm_instMod.F90 b/src/main/clm_instMod.F90 index a2cbd1f695..2790182774 100644 --- a/src/main/clm_instMod.F90 +++ b/src/main/clm_instMod.F90 @@ -11,7 +11,6 @@ module clm_instMod use clm_varctl , only : use_cn, use_c13, use_c14, use_lch4, use_cndv, use_fates, use_fates_bgc ! [PORTED by Hui Tang: NVP column geometry restart] use clm_varctl , only : use_nvp - use NVPLayerDynamicsMod, only : NVPLayerRestart use clm_varctl , only : iulog use clm_varctl , only : use_crop, snow_cover_fraction_method, paramfile use clm_varctl , only : use_excess_ice @@ -522,6 +521,7 @@ subroutine clm_instRest(bounds, ncid, flag, writing_finidat_interp_dest_file) use UrbanParamsType , only : IsSimpleBuildTemp, IsProgBuildTemp use decompMod , only : get_proc_bounds, get_proc_clumps, get_clump_bounds use clm_varpar , only : nlevsno + use NVPLayerDynamicsMod, only : NVPLayerRestart ! ! !DESCRIPTION: diff --git a/src/utils/clmfates_interfaceMod.F90 b/src/utils/clmfates_interfaceMod.F90 index 368e1e1ea2..a89b0bdd52 100644 --- a/src/utils/clmfates_interfaceMod.F90 +++ b/src/utils/clmfates_interfaceMod.F90 @@ -85,8 +85,6 @@ module CLMFatesInterfaceMod use clm_varctl , only : use_nvp use clm_varctl , only : use_nvp_undersnow use clm_varctl , only : nvp_rad_model_ground - ! [PORTED by Hui Tang: NVP layer geometry updater] - use NVPLayerDynamicsMod, only : UpdateNVPLayer use clm_varcon , only : tfrz use clm_varcon , only : spval use clm_varcon , only : denice @@ -1139,7 +1137,9 @@ subroutine dynamics_driv(this, nc, bounds_clump, & type(bounds_type),intent(in) :: bounds_clump type(atm2lnd_type) , intent(in) :: atm2lnd_inst type(soilstate_type) , intent(in) :: soilstate_inst - type(temperature_type) , intent(in) :: temperature_inst + ! [PORTED by Hui Tang: intent(inout) — passed down to wrap_update_hlmfates_dyn → UpdateNVPLayer, + ! which writes temperature_inst%t_soisno_col(c,0) on NVP layer activation/deactivation] + type(temperature_type) , intent(inout) :: temperature_inst type(active_layer_type) , intent(in) :: active_layer_inst integer , intent(in) :: nc type(waterstatebulk_type) , intent(inout) :: waterstatebulk_inst @@ -1582,6 +1582,9 @@ subroutine wrap_update_hlmfates_dyn(this, nc, bounds_clump, & ! provides boundary conditions (such as vegetation fractional coverage) ! --------------------------------------------------------------------------------- + ! [PORTED by Hui Tang: moved from module level to break clmfatesinterfacemod compilation delay] + use NVPLayerDynamicsMod, only : UpdateNVPLayer + class(hlm_fates_interface_type), intent(inout) :: this type(bounds_type),intent(in) :: bounds_clump integer , intent(in) :: nc @@ -2839,7 +2842,6 @@ subroutine wrap_nvp_photosynthesis(this, nc, bounds, & use decompMod , only : bounds_type use FatesPlantRespPhotosynthMod , only : FatesPlantRespPhotosynthDrive - use FatesSynchronizedParsMod , only : get_step_size_real ! !ARGUMENTS: class(hlm_fates_interface_type), intent(inout) :: this From bc302be7a5bd5de59a43d8a14c989019afce6202 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Tue, 28 Apr 2026 15:50:03 +0300 Subject: [PATCH 035/113] Improve treatment when water in NVP becomes ice. --- src/biogeophys/NVPLayerDynamicsMod.F90 | 16 ++++++++++++---- src/biogeophys/SurfaceHumidityMod.F90 | 14 ++++++++------ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/biogeophys/NVPLayerDynamicsMod.F90 b/src/biogeophys/NVPLayerDynamicsMod.F90 index a60e8a9a65..e361f9d8e5 100644 --- a/src/biogeophys/NVPLayerDynamicsMod.F90 +++ b/src/biogeophys/NVPLayerDynamicsMod.F90 @@ -224,7 +224,8 @@ subroutine NVPWaterRetentionCurve(vol_liq, eff_porosity, n_van, alpha_van, watsa m_van = 1.0_r8 - 1.0_r8 / n_van satfrac = (vol_liq - watres) / (eff_porosity - watres) - satfrac = max(0.0_r8, min(1.0_r8, satfrac)) ! clamp to [0, 1] + ! [PORTED by Hui Tang: clamp away from 0 — satfrac**(1/-m_van) = 0**negative = Inf otherwise] + satfrac = max(1.0e-6_r8, min(1.0_r8, satfrac)) ! van Genuchten retention curve: psi in units of (1/alpha_van) smp = -(1.0_r8 / alpha_van) * & @@ -461,6 +462,9 @@ subroutine NVPWaterBalance_Column(bounds, dtime, waterfluxbulk_inst, waterstate_ qflx_nvp_infl_col(c) = frac_nvp_eff * qflx_rain_plus_snomelt(c) ! [mm/s] ! --- NVP volumetric water content (clamped to valid range) --- + ! [PORTED by Hui Tang: initialise eff_porosity and vol_ice on every branch — they are + ! passed to NVPWaterRetentionCurve / NVPHydraulicConductivity below; uninitialised + ! values would cause undefined behaviour or divide-by-zero inside the curves.] if (col%dz(c,0) > 0._r8) then if (t_nvp_col(c) >= tfrz) then ! For unfrozen soil @@ -468,11 +472,15 @@ subroutine NVPWaterBalance_Column(bounds, dtime, waterfluxbulk_inst, waterstate_ eff_porosity = watsat_nvp-vol_ice vol_liq = min(eff_porosity, h2osoi_liq(c,0)/(col%dz(c,0)*denh2o)) else - ! For frozen soil, assume NVP water content is at residual (unavailable for evaporation) - vol_liq = watres_nvp + ! For frozen NVP, water is at residual (unavailable for liquid evaporation) + vol_ice = watsat_nvp + eff_porosity = watsat_nvp + vol_liq = watres_nvp end if else - vol_liq = 0._r8 + vol_ice = 0._r8 + eff_porosity = watsat_nvp + vol_liq = 0._r8 end if ! --- NVP van Genuchten matric potential and hydraulic conductivity --- diff --git a/src/biogeophys/SurfaceHumidityMod.F90 b/src/biogeophys/SurfaceHumidityMod.F90 index d433add51e..9677474d82 100644 --- a/src/biogeophys/SurfaceHumidityMod.F90 +++ b/src/biogeophys/SurfaceHumidityMod.F90 @@ -157,17 +157,19 @@ subroutine CalculateSurfaceHumidity(bounds, & ! --- NVP volumetric water content (clamped to valid range) --- if (dz(c,0) > 0._r8) then if (t_soisno(c,0) >= tfrz) then - ! For unfrozen soil + ! For unfrozen soil — compute matric potential from van Genuchten curve vol_ice = min(watsat_nvp, h2osoi_ice(c,0)/(dz(c,0)*denice)) eff_porosity = watsat_nvp-vol_ice vol_liq = min(eff_porosity, h2osoi_liq(c,0)/(dz(c,0)*denh2o)) + call NVPWaterRetentionCurve(vol_liq, eff_porosity, n_van_nvp, alpha_van_nvp, & + watsat_nvp, watres_nvp, psit_nvp) + hr_nvp = exp(psit_nvp/roverg/t_nvp_col(c)) else - ! For frozen soil, assume NVP water content is at residual (unavailable for evaporation) - vol_liq = watres_nvp + ! [PORTED by Hui Tang: frozen NVP — water is unavailable for liquid evaporation; + ! treat surface as saturated over ice (hr_nvp=1) so qg_nvp=qsat(t_nvp). + ! Calling NVPWaterRetentionCurve here is invalid: satfrac=0 → 0**(-1/m_van)=Inf.] + hr_nvp = 1._r8 end if - call NVPWaterRetentionCurve(vol_liq, eff_porosity, n_van_nvp, alpha_van_nvp, & - watsat_nvp, watres_nvp, psit_nvp) - hr_nvp = exp(psit_nvp/roverg/t_nvp_col(c)) else ! If dz(c,0) is not positive, set hr_nvp to 0 hr_nvp = 0._r8 From 6f1096a1a153fff38f315e9fbdba43cdf4d6205c Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Tue, 28 Apr 2026 15:52:13 +0300 Subject: [PATCH 036/113] Separate 'use_nvp' and 'col%nvp_layer'-related variables in the if statements. --- src/biogeophys/SurfaceRadiationMod.F90 | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/biogeophys/SurfaceRadiationMod.F90 b/src/biogeophys/SurfaceRadiationMod.F90 index 430da65617..ed6071aec0 100644 --- a/src/biogeophys/SurfaceRadiationMod.F90 +++ b/src/biogeophys/SurfaceRadiationMod.F90 @@ -765,7 +765,11 @@ subroutine SurfaceRadiation(bounds, num_nourbanp, filter_nourbanp, & ! per unit flux incident on NVP). trd/tri are below-canopy direct/diffuse fluxes. ! sabg(p) is unchanged (ground total = NVP + soil via modified albedo). ! sabg_soil is corrected because it was computed using soil-only albedo (albsod). - if (use_nvp .and. col%nvp_layer_active(patch%column(p))) then + ! [PORTED by Hui Tang: nest the NVP guard — Fortran does not short-circuit .and., + ! and the NVP arrays are only allocated when use_nvp=.true.; combining the + ! use_nvp check with array access in one .and. dereferences a null pointer.] + if (use_nvp) then + if (col%nvp_layer_active(patch%column(p))) then sabg_lyr(p,0) = 0._r8 do ib = 1, nband sabg_lyr(p,0) = sabg_lyr(p,0) + & @@ -776,6 +780,7 @@ subroutine SurfaceRadiation(bounds, num_nourbanp, filter_nourbanp, & sabg_lyr(p,1) = sabg_lyr(p,1) - sabg_lyr(p,0) sabg_soil(p) = sabg_soil(p) - sabg_lyr(p,0) end if + end if ! [PORTED by Hui Tang: close outer use_nvp guard] ! CASE 2: Snow layers present: absorbed radiation is scaled according to ! flux factors computed by SNICAR @@ -796,9 +801,12 @@ subroutine SurfaceRadiation(bounds, num_nourbanp, filter_nourbanp, & ! When use_nvp and SNICAR NVP layer-0 is active, flx_absdv(c,0)/flx_absiv(c,0) ! already hold NVP absorption (set by SNICAR_RT above), so sabg_lyr(p,0) is ! correct from the SNICAR loop above. Correct sabg_soil to use SNICAR soil layer. - if (use_nvp .and. surfalb_inst%nvp_tau_col(c) > 0._r8) then - ! sabg_lyr(p,1) = SNICAR soil-layer absorption (excludes NVP); use it directly. - sabg_soil(p) = sabg_lyr(p,1) + ! [PORTED by Hui Tang: nest the NVP guard — see line ~768 for rationale] + if (use_nvp) then + if (surfalb_inst%nvp_tau_col(c) > 0._r8) then + ! sabg_lyr(p,1) = SNICAR soil-layer absorption (excludes NVP); use it directly. + sabg_soil(p) = sabg_lyr(p,1) + end if end if ! Divide absorbed by total, to get fraction absorbed in subsurface From 16945c6741337abbdf533ad70e9370769ebbc5b3 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Tue, 28 Apr 2026 16:07:12 +0300 Subject: [PATCH 037/113] Add NVP resitance to evaporation into Canopy and bareground flux calculation similar to soilresis. --- src/biogeophys/BareGroundFluxesMod.F90 | 38 +++++++++--- src/biogeophys/BiogeophysPreFluxCalcsMod.F90 | 8 +++ src/biogeophys/CanopyFluxesMod.F90 | 42 ++++++++++--- src/biogeophys/NVPParamsMod.F90 | 4 ++ src/biogeophys/SoilStateType.F90 | 12 +++- src/biogeophys/SurfaceResistanceMod.F90 | 65 +++++++++++++++++++- 6 files changed, 148 insertions(+), 21 deletions(-) diff --git a/src/biogeophys/BareGroundFluxesMod.F90 b/src/biogeophys/BareGroundFluxesMod.F90 index 5d0e8cb5ab..47dabdbd67 100644 --- a/src/biogeophys/BareGroundFluxesMod.F90 +++ b/src/biogeophys/BareGroundFluxesMod.F90 @@ -140,6 +140,7 @@ subroutine BareGroundFluxes(bounds, num_noexposedvegp, filter_noexposedvegp, & real(r8) :: raw ! moisture resistance [s/m] real(r8) :: raih ! temporary variable [kg/m2/s] real(r8) :: raiw ! temporary variable [kg/m2/s] + real(r8) :: r_surf_eff ! [PORTED by Hui Tang: area-weighted soil+NVP surface resistance [s/m]] real(r8) :: fm(bounds%begp:bounds%endp) ! needed for BGC only to diagnose 10m wind speed real(r8) :: e_ref2m ! 2 m height surface saturated vapor pressure [Pa] real(r8) :: qsat_ref2m ! 2 m height surface saturated specific humidity [kg/kg] @@ -162,7 +163,8 @@ subroutine BareGroundFluxes(bounds, num_noexposedvegp, filter_noexposedvegp, & associate( & dhsdt_canopy => energyflux_inst%dhsdt_canopy_patch , & ! Output: [real(r8) (:) ] change in heat storage of stem (W/m**2) [+ to atm] eflx_sh_stem => energyflux_inst%eflx_sh_stem_patch , & ! Output: [real(r8) (:) ] sensible heat flux from stems (W/m**2) [+ to atm] - soilresis => soilstate_inst%soilresis_col , & ! Input: [real(r8) (:,:) ] evaporative soil resistance (s/m) + soilresis => soilstate_inst%soilresis_col , & ! Input: [real(r8) (:,:) ] evaporative soil resistance (s/m) + rnvp_col => soilstate_inst%rnvp_col , & ! [PORTED by Hui Tang: NVP surface evaporative resistance (s/m)] snl => col%snl , & ! Input: [integer (:) ] number of snow layers dz => col%dz , & ! Input: [real(r8) (:,:) ] layer depth (m) zii => col%zii , & ! Input: [real(r8) (:) ] convective boundary height [m] @@ -459,8 +461,21 @@ subroutine BareGroundFluxes(bounds, num_noexposedvegp, filter_noexposedvegp, & end if endif if(do_soil_resistance_sl14())then - ! Swenson & Lawrence 2014 soil resistance is applied - raiw = forc_rho(c)/(raw+soilresis(c)) + ! [PORTED by Hui Tang: Swenson & Lawrence 2014 soil resistance, blended with NVP + ! surface resistance by area fraction. Linear (Kirchhoff series-on-each-fraction) + ! blend: r_eff = (1-f_nvp)*soilresis + f_nvp*rnvp. The qg(c) blend in + ! SurfaceHumidityMod already weights humidities by the same fractions, so this + ! matches the area weighting consistently. When f_nvp=0, r_eff=soilresis exactly.] + if (use_nvp) then + if (col%frac_nvp(c) > 0._r8) then + ! If there is some NVP fraction, blend soil resistance and NVP resistance in series + r_surf_eff = (1._r8 - col%frac_nvp(c)) * soilresis(c) & + + col%frac_nvp(c) * rnvp_col(c) + end if + else + r_surf_eff = soilresis(c) + end if + raiw = forc_rho(c)/(raw + r_surf_eff) endif end if @@ -487,8 +502,10 @@ subroutine BareGroundFluxes(bounds, num_noexposedvegp, filter_noexposedvegp, & eflx_sh_soil(p) = -raih*(thm(p)-t_soisno(c,1)) eflx_sh_h2osfc(p) = -raih*(thm(p)-t_h2osfc(c)) ! [PORTED by Hui Tang: NVP sensible heat flux for bare ground, analogous to snow/h2osfc] - if (use_nvp .and. col%frac_nvp(c) > 0._r8) then - eflx_sh_nvp(p) = -raih*(thm(p)-t_nvp_col(c)) + if (use_nvp) then + if (col%frac_nvp(c) > 0._r8) then + eflx_sh_nvp(p) = -raih*(thm(p)-t_nvp_col(c)) + end if else eflx_sh_nvp(p) = 0._r8 end if @@ -503,9 +520,14 @@ subroutine BareGroundFluxes(bounds, num_noexposedvegp, filter_noexposedvegp, & qflx_ev_snow(p) = -raiw*(forc_q(c) - qg_snow(c)) qflx_ev_soil(p) = -raiw*(forc_q(c) - qg_soil(c)) qflx_ev_h2osfc(p) = -raiw*(forc_q(c) - qg_h2osfc(c)) - ! [PORTED by Hui Tang: NVP evaporation flux for bare ground, analogous to snow/h2osfc] - if (use_nvp .and. col%frac_nvp(c) > 0._r8) then - qflx_ev_nvp(p) = -raiw*(forc_q(c) - qg_nvp(c)) + ! [PORTED by Hui Tang: NVP evaporation flux for bare ground. + ! Apply NVP surface resistance rnvp_col in series with the aerodynamic resistance raw, + ! mirroring the soilresis treatment at the raiw line above. The effective conductance + ! for NVP is forc_rho/(raw + rnvp), so qflx_ev_nvp = -conductance * (q_atm - qg_nvp).] + if (use_nvp) then + if (col%frac_nvp(c) > 0._r8) then + qflx_ev_nvp(p) = -raiw*(forc_q(c) - qg_nvp(c)) + end if else qflx_ev_nvp(p) = 0._r8 end if diff --git a/src/biogeophys/BiogeophysPreFluxCalcsMod.F90 b/src/biogeophys/BiogeophysPreFluxCalcsMod.F90 index 574af6f782..2f7a69abe2 100644 --- a/src/biogeophys/BiogeophysPreFluxCalcsMod.F90 +++ b/src/biogeophys/BiogeophysPreFluxCalcsMod.F90 @@ -33,6 +33,8 @@ module BiogeophysPreFluxCalcsMod use WaterDiagnosticBulkType , only : waterdiagnosticbulk_type use WaterStateBulkType , only : waterstatebulk_type use SurfaceResistanceMod , only : calc_soilevap_resis + ! [PORTED by Hui Tang: NVP surface resistance computed alongside soil resistance] + use SurfaceResistanceMod , only : calc_nvp_resis use WaterFluxBulkType , only : waterfluxbulk_type ! @@ -116,6 +118,12 @@ subroutine BiogeophysPreFluxCalcs(bounds, & waterstatebulk_inst, waterdiagnosticbulk_inst, & temperature_inst) + ! [PORTED by Hui Tang: compute NVP surface resistance into soilstate_inst%rnvp_col + ! for use by BareGroundFluxesMod / CanopyFluxesMod NVP evaporation paths] + call calc_nvp_resis(bounds, & + num_nolakec, filter_nolakec, & + soilstate_inst, waterdiagnosticbulk_inst, temperature_inst) + end subroutine BiogeophysPreFluxCalcs !----------------------------------------------------------------------- diff --git a/src/biogeophys/CanopyFluxesMod.F90 b/src/biogeophys/CanopyFluxesMod.F90 index 3a328d23f1..f1dd67712a 100644 --- a/src/biogeophys/CanopyFluxesMod.F90 +++ b/src/biogeophys/CanopyFluxesMod.F90 @@ -320,6 +320,8 @@ subroutine CanopyFluxes(bounds, num_exposedvegp, filter_exposedvegp, real(r8) :: wtaq ! latent heat conductance for air [m/s] real(r8) :: wtlq ! latent heat conductance for leaf [m/s] real(r8) :: wtgq(bounds%begp:bounds%endp) ! latent heat conductance for ground [m/s] + real(r8) :: wtgq_nvp ! [PORTED by Hui Tang: latent heat conductance for NVP, with rnvp] + real(r8) :: r_surf_eff ! [PORTED by Hui Tang: area-weighted soil+NVP surface resistance [s/m]] real(r8) :: wtaq0(bounds%begp:bounds%endp) ! normalized latent heat conductance for air [-] real(r8) :: wtlq0(bounds%begp:bounds%endp) ! normalized latent heat conductance for leaf [-] real(r8) :: wtgq0 ! normalized heat conductance for ground [-] @@ -455,6 +457,7 @@ subroutine CanopyFluxes(bounds, num_exposedvegp, filter_exposedvegp, t_stem => temperature_inst%t_stem_patch , & ! Output: [real(r8) (:) ] stem temperature (Kelvin) dhsdt_canopy => energyflux_inst%dhsdt_canopy_patch , & ! Output: [real(r8) (:) ] change in heat storage of stem (W/m**2) [+ to atm] soilresis => soilstate_inst%soilresis_col , & ! Input: [real(r8) (:) ] soil evaporative resistance + rnvp_col => soilstate_inst%rnvp_col , & ! [PORTED by Hui Tang: NVP surface evaporative resistance (s/m)] snl => col%snl , & ! Input: [integer (:) ] number of snow layers dayl => grc%dayl , & ! Input: [real(r8) (:) ] daylength (s) max_dayl => grc%max_dayl , & ! Input: [real(r8) (:) ] maximum daylength for this grid cell (s) @@ -1282,7 +1285,19 @@ subroutine CanopyFluxes(bounds, num_exposedvegp, filter_exposedvegp, wtgq(p) = soilbeta(c)*frac_veg_nosno(p)/(raw(p,below_canopy)+rdl) endif if (do_soil_resistance_sl14()) then - wtgq(p) = frac_veg_nosno(p)/(raw(p,below_canopy)+soilresis(c)) + ! [PORTED by Hui Tang: blend NVP surface resistance into the column wtgq + ! by area fraction — same Kirchhoff linear blend as in BareGroundFluxesMod. + ! When f_nvp=0, r_eff=soilresis exactly so non-NVP columns are unaffected.] + if (use_nvp) then + if (frac_nvp(c) > 0._r8) then + ! blend soilresis and rnvp_col by the fraction of NVP in the column + r_surf_eff = (1._r8 - col%frac_nvp(c)) * soilresis(c) & + + col%frac_nvp(c) * rnvp_col(c) + end if + else + r_surf_eff = soilresis(c) + end if + wtgq(p) = frac_veg_nosno(p)/(raw(p,below_canopy) + r_surf_eff) endif end if @@ -1542,9 +1557,11 @@ subroutine CanopyFluxes(bounds, num_exposedvegp, filter_exposedvegp, eflx_sh_soil(p) = cpair*forc_rho(c)*wtg(p)*delt_soil eflx_sh_h2osfc(p) = cpair*forc_rho(c)*wtg(p)*delt_h2osfc ! [PORTED by Hui Tang: NVP individual sensible heat flux, analogous to snow/h2osfc] - if (use_nvp .and. col%frac_nvp(c) > 0._r8) then - delt_nvp = wtal(p)*t_nvp_col(c)-wtl0(p)*t_veg(p)-wta0(p)*thm(p)-wtstem0(p)*t_stem(p) - eflx_sh_nvp(p) = cpair*forc_rho(c)*wtg(p)*delt_nvp + if (use_nvp) then + if (col%frac_nvp(c) > 0._r8) then + delt_nvp = wtal(p)*t_nvp_col(c)-wtl0(p)*t_veg(p)-wta0(p)*thm(p)-wtstem0(p)*t_stem(p) + eflx_sh_nvp(p) = cpair*forc_rho(c)*wtg(p)*delt_nvp + end if else eflx_sh_nvp(p) = 0._r8 end if @@ -1560,12 +1577,17 @@ subroutine CanopyFluxes(bounds, num_exposedvegp, filter_exposedvegp, delq_h2osfc = wtalq(p)*qg_h2osfc(c)-wtlq0(p)*qsatl(p)-wtaq0(p)*forc_q(c) qflx_ev_h2osfc(p) = forc_rho(c)*wtgq(p)*delq_h2osfc - ! [PORTED by Hui Tang: NVP individual latent heat flux, analogous to snow/h2osfc] - ! qflx_evap_soi already includes NVP because qg(c) blends NVP in SurfaceHumidityMod. - ! This is the diagnostic breakdown of the NVP contribution. - if (use_nvp .and. col%frac_nvp(c) > 0._r8) then - delq_nvp = wtalq(p)*qg_nvp(c)-wtlq0(p)*qsatl(p)-wtaq0(p)*forc_q(c) - qflx_ev_nvp(p) = forc_rho(c)*wtgq(p)*delq_nvp + ! [PORTED by Hui Tang: NVP individual latent heat flux. + ! Apply NVP surface resistance rnvp_col in series with raw(p,below_canopy), + ! mirroring the soilresis treatment at the wtgq line ~1285. wtgq_nvp replaces + ! the canopy-iteration wtgq (which was computed with soilresis) so the NVP + ! diagnostic reflects the moss/lichen-specific surface resistance.] + if (use_nvp) then + if (col%frac_nvp(c) > 0._r8) then + delq_nvp = wtalq(p)*qg_nvp(c)-wtlq0(p)*qsatl(p)-wtaq0(p)*forc_q(c) + wtgq_nvp = frac_veg_nosno(p) / (raw(p,below_canopy) + rnvp_col(c)) + qflx_ev_nvp(p) = forc_rho(c) * wtgq_nvp * delq_nvp + end if else qflx_ev_nvp(p) = 0._r8 end if diff --git a/src/biogeophys/NVPParamsMod.F90 b/src/biogeophys/NVPParamsMod.F90 index 652c12eb6d..125d1b410f 100644 --- a/src/biogeophys/NVPParamsMod.F90 +++ b/src/biogeophys/NVPParamsMod.F90 @@ -35,6 +35,10 @@ module NVPParamsMod real(r8) :: rnvp_min = 10.0_r8 ! minimum surface resistance when saturated [s m-1] real(r8) :: rnvp_amp = 500.0_r8 ! amplitude of resistance increase when dry [s m-1] real(r8) :: rnvp_exp = 3.0_r8 ! exponent of dryness function [-] + ! [PORTED by Hui Tang: surface resistance applied when NVP is frozen — typical literature + ! range for ice/snow surfaces is ~1000-3000 s/m; default 1500 s/m suppresses but does + ! not zero sublimation. Set to a very large value (e.g. 1e6) to disable evap when frozen.] + real(r8) :: rnvp_ice = 1500.0_r8 ! NVP resistance when frozen [s m-1] ! Hydraulic properties (Mualem-van Genuchten) real(r8) :: ksat_nvp = 1.0e-4_r8 ! saturated hydraulic conductivity [m s-1] diff --git a/src/biogeophys/SoilStateType.F90 b/src/biogeophys/SoilStateType.F90 index e491613ceb..c3c90ee1cb 100644 --- a/src/biogeophys/SoilStateType.F90 +++ b/src/biogeophys/SoilStateType.F90 @@ -45,6 +45,8 @@ module SoilStateType real(r8), pointer :: sucsat_col (:,:) ! col minimum soil suction (mm) (nlevgrnd) real(r8), pointer :: dsl_col (:) ! col dry surface layer thickness (mm) real(r8), pointer :: soilresis_col (:) ! col soil evaporative resistance S&L14 (s/m) + ! [PORTED by Hui Tang: NVP (moss/lichen) surface evaporative resistance, analogous to soilresis_col] + real(r8), pointer :: rnvp_col (:) ! col NVP surface evaporative resistance (s/m) real(r8), pointer :: soilbeta_col (:) ! col factor that reduces ground evaporation L&P1992(-) real(r8), pointer :: soilalpha_col (:) ! col factor that reduces ground saturated specific humidity (-) real(r8), pointer :: soilalpha_u_col (:) ! col urban factor that reduces ground saturated specific humidity (-) @@ -144,7 +146,9 @@ subroutine InitAllocate(this, bounds) allocate(this%watfc_col (begc:endc,nlevgrnd)) ; this%watfc_col (:,:) = nan allocate(this%sucsat_col (begc:endc,nlevgrnd)) ; this%sucsat_col (:,:) = spval allocate(this%dsl_col (begc:endc)) ; this%dsl_col (:) = spval!nan - allocate(this%soilresis_col (begc:endc)) ; this%soilresis_col (:) = spval!nan + allocate(this%soilresis_col (begc:endc)) ; this%soilresis_col (:) = spval!nan + ! [PORTED by Hui Tang: allocate NVP surface resistance unconditionally — small array, safe defaults] + allocate(this%rnvp_col (begc:endc)) ; this%rnvp_col (:) = spval allocate(this%soilbeta_col (begc:endc)) ; this%soilbeta_col (:) = nan allocate(this%soilalpha_col (begc:endc)) ; this%soilalpha_col (:) = nan allocate(this%soilalpha_u_col (begc:endc)) ; this%soilalpha_u_col (:) = nan @@ -308,6 +312,12 @@ subroutine InitHistory(this, bounds) avgflag='A', long_name='soil resistance to evaporation', & ptr_col=this%soilresis_col) + ! [PORTED by Hui Tang: NVP surface resistance history field, analogous to SOILRESIS] + this%rnvp_col(begc:endc) = spval + call hist_addfld1d (fname='RNVP', units='s/m', & + avgflag='A', long_name='NVP (moss/lichen) surface resistance to evaporation', & + ptr_col=this%rnvp_col, default='inactive') + this%dsl_col(begc:endc) = spval call hist_addfld1d (fname='DSL', units='mm', & avgflag='A', long_name='dry surface layer thickness', & diff --git a/src/biogeophys/SurfaceResistanceMod.F90 b/src/biogeophys/SurfaceResistanceMod.F90 index 9b04327252..38775c7f59 100644 --- a/src/biogeophys/SurfaceResistanceMod.F90 +++ b/src/biogeophys/SurfaceResistanceMod.F90 @@ -26,6 +26,8 @@ module SurfaceResistanceMod ! ! !PUBLIC MEMBER FUNCTIONS: public :: calc_soilevap_resis + ! [PORTED by Hui Tang: NVP surface resistance calculation, analogous to calc_soilevap_resis] + public :: calc_nvp_resis public :: do_soilevap_beta, do_soil_resistance_sl14 ! public :: init_soil_resistance public :: soil_resistance_readNL @@ -236,8 +238,67 @@ subroutine calc_soilevap_resis(bounds, num_nolakec, filter_nolakec, & end associate end subroutine calc_soilevap_resis - - !------------------------------------------------------------------------------ + + !------------------------------------------------------------------------------ + ! [PORTED by Hui Tang: compute NVP (moss/lichen) surface resistance, parallel to + ! calc_soilevap_resis. Uses fwet_nvp_col (NVP wet fraction = effective saturation) + ! as satfrac in the van de Griend & Owe / Daamen & Simmonds formula. When the NVP + ! layer is frozen, the empirical formula is replaced by the runtime-tunable rnvp_ice + ! to represent ice-surface resistance. When NVP is inactive (frac_nvp == 0 or + ! layer not active), rnvp_col is set to spval — callers must guard with use_nvp.] + subroutine calc_nvp_resis(bounds, num_nolakec, filter_nolakec, & + soilstate_inst, waterdiagnosticbulk_inst, temperature_inst) + ! + ! !USES: + use clm_varcon , only : tfrz, spval + use clm_varctl , only : use_nvp + use ColumnType , only : col + use NVPParamsMod , only : rnvp_min, rnvp_amp, rnvp_exp, rnvp_ice + use decompMod , only : bounds_type + ! + ! !ARGUMENTS: + implicit none + type(bounds_type) , intent(in) :: bounds + integer , intent(in) :: num_nolakec + integer , intent(in) :: filter_nolakec(:) + type(soilstate_type) , intent(inout) :: soilstate_inst + type(waterdiagnosticbulk_type) , intent(in) :: waterdiagnosticbulk_inst + type(temperature_type) , intent(in) :: temperature_inst + ! + ! !LOCAL VARIABLES: + integer :: c, fc + real(r8) :: satfrac + + if (.not. use_nvp) return + + associate( & + fwet_nvp_col => waterdiagnosticbulk_inst%fwet_nvp_col , & ! Input: NVP wet fraction [-] + t_soisno => temperature_inst%t_soisno_col , & ! Input: layer-0 temperature [K] + rnvp_col => soilstate_inst%rnvp_col & ! Output: NVP surface resistance [s/m] + ) + + do fc = 1, num_nolakec + c = filter_nolakec(fc) + + if (col%frac_nvp(c) > 0._r8 .and. col%nvp_layer_active(c)) then + if (t_soisno(c,0) >= tfrz) then + ! Unfrozen: van de Griend & Owe / Daamen & Simmonds dryness curve + satfrac = max(0._r8, min(1._r8, fwet_nvp_col(c))) + rnvp_col(c) = rnvp_min + rnvp_amp * (1._r8 - satfrac)**rnvp_exp + else + ! Frozen: literature ice/snow resistance, suppresses but doesn't zero sublimation + rnvp_col(c) = rnvp_ice + end if + else + rnvp_col(c) = spval + end if + end do + + end associate + + end subroutine calc_nvp_resis + + !------------------------------------------------------------------------------ subroutine calc_beta_leepielke1992(bounds, num_nolakec, filter_nolakec, & soilstate_inst, waterstatebulk_inst, waterdiagnosticbulk_inst, soilbeta) ! From c051fb8276eeb966c541ae074eb6f025b67a2c46 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Tue, 28 Apr 2026 17:50:00 +0300 Subject: [PATCH 038/113] Revert "Add NVP resitance to evaporation into Canopy and bareground flux calculation similar to soilresis." This reverts commit 16945c6741337abbdf533ad70e9370769ebbc5b3. --- src/biogeophys/BareGroundFluxesMod.F90 | 38 +++--------- src/biogeophys/BiogeophysPreFluxCalcsMod.F90 | 8 --- src/biogeophys/CanopyFluxesMod.F90 | 42 +++---------- src/biogeophys/NVPParamsMod.F90 | 4 -- src/biogeophys/SoilStateType.F90 | 12 +--- src/biogeophys/SurfaceResistanceMod.F90 | 65 +------------------- 6 files changed, 21 insertions(+), 148 deletions(-) diff --git a/src/biogeophys/BareGroundFluxesMod.F90 b/src/biogeophys/BareGroundFluxesMod.F90 index 47dabdbd67..5d0e8cb5ab 100644 --- a/src/biogeophys/BareGroundFluxesMod.F90 +++ b/src/biogeophys/BareGroundFluxesMod.F90 @@ -140,7 +140,6 @@ subroutine BareGroundFluxes(bounds, num_noexposedvegp, filter_noexposedvegp, & real(r8) :: raw ! moisture resistance [s/m] real(r8) :: raih ! temporary variable [kg/m2/s] real(r8) :: raiw ! temporary variable [kg/m2/s] - real(r8) :: r_surf_eff ! [PORTED by Hui Tang: area-weighted soil+NVP surface resistance [s/m]] real(r8) :: fm(bounds%begp:bounds%endp) ! needed for BGC only to diagnose 10m wind speed real(r8) :: e_ref2m ! 2 m height surface saturated vapor pressure [Pa] real(r8) :: qsat_ref2m ! 2 m height surface saturated specific humidity [kg/kg] @@ -163,8 +162,7 @@ subroutine BareGroundFluxes(bounds, num_noexposedvegp, filter_noexposedvegp, & associate( & dhsdt_canopy => energyflux_inst%dhsdt_canopy_patch , & ! Output: [real(r8) (:) ] change in heat storage of stem (W/m**2) [+ to atm] eflx_sh_stem => energyflux_inst%eflx_sh_stem_patch , & ! Output: [real(r8) (:) ] sensible heat flux from stems (W/m**2) [+ to atm] - soilresis => soilstate_inst%soilresis_col , & ! Input: [real(r8) (:,:) ] evaporative soil resistance (s/m) - rnvp_col => soilstate_inst%rnvp_col , & ! [PORTED by Hui Tang: NVP surface evaporative resistance (s/m)] + soilresis => soilstate_inst%soilresis_col , & ! Input: [real(r8) (:,:) ] evaporative soil resistance (s/m) snl => col%snl , & ! Input: [integer (:) ] number of snow layers dz => col%dz , & ! Input: [real(r8) (:,:) ] layer depth (m) zii => col%zii , & ! Input: [real(r8) (:) ] convective boundary height [m] @@ -461,21 +459,8 @@ subroutine BareGroundFluxes(bounds, num_noexposedvegp, filter_noexposedvegp, & end if endif if(do_soil_resistance_sl14())then - ! [PORTED by Hui Tang: Swenson & Lawrence 2014 soil resistance, blended with NVP - ! surface resistance by area fraction. Linear (Kirchhoff series-on-each-fraction) - ! blend: r_eff = (1-f_nvp)*soilresis + f_nvp*rnvp. The qg(c) blend in - ! SurfaceHumidityMod already weights humidities by the same fractions, so this - ! matches the area weighting consistently. When f_nvp=0, r_eff=soilresis exactly.] - if (use_nvp) then - if (col%frac_nvp(c) > 0._r8) then - ! If there is some NVP fraction, blend soil resistance and NVP resistance in series - r_surf_eff = (1._r8 - col%frac_nvp(c)) * soilresis(c) & - + col%frac_nvp(c) * rnvp_col(c) - end if - else - r_surf_eff = soilresis(c) - end if - raiw = forc_rho(c)/(raw + r_surf_eff) + ! Swenson & Lawrence 2014 soil resistance is applied + raiw = forc_rho(c)/(raw+soilresis(c)) endif end if @@ -502,10 +487,8 @@ subroutine BareGroundFluxes(bounds, num_noexposedvegp, filter_noexposedvegp, & eflx_sh_soil(p) = -raih*(thm(p)-t_soisno(c,1)) eflx_sh_h2osfc(p) = -raih*(thm(p)-t_h2osfc(c)) ! [PORTED by Hui Tang: NVP sensible heat flux for bare ground, analogous to snow/h2osfc] - if (use_nvp) then - if (col%frac_nvp(c) > 0._r8) then - eflx_sh_nvp(p) = -raih*(thm(p)-t_nvp_col(c)) - end if + if (use_nvp .and. col%frac_nvp(c) > 0._r8) then + eflx_sh_nvp(p) = -raih*(thm(p)-t_nvp_col(c)) else eflx_sh_nvp(p) = 0._r8 end if @@ -520,14 +503,9 @@ subroutine BareGroundFluxes(bounds, num_noexposedvegp, filter_noexposedvegp, & qflx_ev_snow(p) = -raiw*(forc_q(c) - qg_snow(c)) qflx_ev_soil(p) = -raiw*(forc_q(c) - qg_soil(c)) qflx_ev_h2osfc(p) = -raiw*(forc_q(c) - qg_h2osfc(c)) - ! [PORTED by Hui Tang: NVP evaporation flux for bare ground. - ! Apply NVP surface resistance rnvp_col in series with the aerodynamic resistance raw, - ! mirroring the soilresis treatment at the raiw line above. The effective conductance - ! for NVP is forc_rho/(raw + rnvp), so qflx_ev_nvp = -conductance * (q_atm - qg_nvp).] - if (use_nvp) then - if (col%frac_nvp(c) > 0._r8) then - qflx_ev_nvp(p) = -raiw*(forc_q(c) - qg_nvp(c)) - end if + ! [PORTED by Hui Tang: NVP evaporation flux for bare ground, analogous to snow/h2osfc] + if (use_nvp .and. col%frac_nvp(c) > 0._r8) then + qflx_ev_nvp(p) = -raiw*(forc_q(c) - qg_nvp(c)) else qflx_ev_nvp(p) = 0._r8 end if diff --git a/src/biogeophys/BiogeophysPreFluxCalcsMod.F90 b/src/biogeophys/BiogeophysPreFluxCalcsMod.F90 index 2f7a69abe2..574af6f782 100644 --- a/src/biogeophys/BiogeophysPreFluxCalcsMod.F90 +++ b/src/biogeophys/BiogeophysPreFluxCalcsMod.F90 @@ -33,8 +33,6 @@ module BiogeophysPreFluxCalcsMod use WaterDiagnosticBulkType , only : waterdiagnosticbulk_type use WaterStateBulkType , only : waterstatebulk_type use SurfaceResistanceMod , only : calc_soilevap_resis - ! [PORTED by Hui Tang: NVP surface resistance computed alongside soil resistance] - use SurfaceResistanceMod , only : calc_nvp_resis use WaterFluxBulkType , only : waterfluxbulk_type ! @@ -118,12 +116,6 @@ subroutine BiogeophysPreFluxCalcs(bounds, & waterstatebulk_inst, waterdiagnosticbulk_inst, & temperature_inst) - ! [PORTED by Hui Tang: compute NVP surface resistance into soilstate_inst%rnvp_col - ! for use by BareGroundFluxesMod / CanopyFluxesMod NVP evaporation paths] - call calc_nvp_resis(bounds, & - num_nolakec, filter_nolakec, & - soilstate_inst, waterdiagnosticbulk_inst, temperature_inst) - end subroutine BiogeophysPreFluxCalcs !----------------------------------------------------------------------- diff --git a/src/biogeophys/CanopyFluxesMod.F90 b/src/biogeophys/CanopyFluxesMod.F90 index f1dd67712a..3a328d23f1 100644 --- a/src/biogeophys/CanopyFluxesMod.F90 +++ b/src/biogeophys/CanopyFluxesMod.F90 @@ -320,8 +320,6 @@ subroutine CanopyFluxes(bounds, num_exposedvegp, filter_exposedvegp, real(r8) :: wtaq ! latent heat conductance for air [m/s] real(r8) :: wtlq ! latent heat conductance for leaf [m/s] real(r8) :: wtgq(bounds%begp:bounds%endp) ! latent heat conductance for ground [m/s] - real(r8) :: wtgq_nvp ! [PORTED by Hui Tang: latent heat conductance for NVP, with rnvp] - real(r8) :: r_surf_eff ! [PORTED by Hui Tang: area-weighted soil+NVP surface resistance [s/m]] real(r8) :: wtaq0(bounds%begp:bounds%endp) ! normalized latent heat conductance for air [-] real(r8) :: wtlq0(bounds%begp:bounds%endp) ! normalized latent heat conductance for leaf [-] real(r8) :: wtgq0 ! normalized heat conductance for ground [-] @@ -457,7 +455,6 @@ subroutine CanopyFluxes(bounds, num_exposedvegp, filter_exposedvegp, t_stem => temperature_inst%t_stem_patch , & ! Output: [real(r8) (:) ] stem temperature (Kelvin) dhsdt_canopy => energyflux_inst%dhsdt_canopy_patch , & ! Output: [real(r8) (:) ] change in heat storage of stem (W/m**2) [+ to atm] soilresis => soilstate_inst%soilresis_col , & ! Input: [real(r8) (:) ] soil evaporative resistance - rnvp_col => soilstate_inst%rnvp_col , & ! [PORTED by Hui Tang: NVP surface evaporative resistance (s/m)] snl => col%snl , & ! Input: [integer (:) ] number of snow layers dayl => grc%dayl , & ! Input: [real(r8) (:) ] daylength (s) max_dayl => grc%max_dayl , & ! Input: [real(r8) (:) ] maximum daylength for this grid cell (s) @@ -1285,19 +1282,7 @@ subroutine CanopyFluxes(bounds, num_exposedvegp, filter_exposedvegp, wtgq(p) = soilbeta(c)*frac_veg_nosno(p)/(raw(p,below_canopy)+rdl) endif if (do_soil_resistance_sl14()) then - ! [PORTED by Hui Tang: blend NVP surface resistance into the column wtgq - ! by area fraction — same Kirchhoff linear blend as in BareGroundFluxesMod. - ! When f_nvp=0, r_eff=soilresis exactly so non-NVP columns are unaffected.] - if (use_nvp) then - if (frac_nvp(c) > 0._r8) then - ! blend soilresis and rnvp_col by the fraction of NVP in the column - r_surf_eff = (1._r8 - col%frac_nvp(c)) * soilresis(c) & - + col%frac_nvp(c) * rnvp_col(c) - end if - else - r_surf_eff = soilresis(c) - end if - wtgq(p) = frac_veg_nosno(p)/(raw(p,below_canopy) + r_surf_eff) + wtgq(p) = frac_veg_nosno(p)/(raw(p,below_canopy)+soilresis(c)) endif end if @@ -1557,11 +1542,9 @@ subroutine CanopyFluxes(bounds, num_exposedvegp, filter_exposedvegp, eflx_sh_soil(p) = cpair*forc_rho(c)*wtg(p)*delt_soil eflx_sh_h2osfc(p) = cpair*forc_rho(c)*wtg(p)*delt_h2osfc ! [PORTED by Hui Tang: NVP individual sensible heat flux, analogous to snow/h2osfc] - if (use_nvp) then - if (col%frac_nvp(c) > 0._r8) then - delt_nvp = wtal(p)*t_nvp_col(c)-wtl0(p)*t_veg(p)-wta0(p)*thm(p)-wtstem0(p)*t_stem(p) - eflx_sh_nvp(p) = cpair*forc_rho(c)*wtg(p)*delt_nvp - end if + if (use_nvp .and. col%frac_nvp(c) > 0._r8) then + delt_nvp = wtal(p)*t_nvp_col(c)-wtl0(p)*t_veg(p)-wta0(p)*thm(p)-wtstem0(p)*t_stem(p) + eflx_sh_nvp(p) = cpair*forc_rho(c)*wtg(p)*delt_nvp else eflx_sh_nvp(p) = 0._r8 end if @@ -1577,17 +1560,12 @@ subroutine CanopyFluxes(bounds, num_exposedvegp, filter_exposedvegp, delq_h2osfc = wtalq(p)*qg_h2osfc(c)-wtlq0(p)*qsatl(p)-wtaq0(p)*forc_q(c) qflx_ev_h2osfc(p) = forc_rho(c)*wtgq(p)*delq_h2osfc - ! [PORTED by Hui Tang: NVP individual latent heat flux. - ! Apply NVP surface resistance rnvp_col in series with raw(p,below_canopy), - ! mirroring the soilresis treatment at the wtgq line ~1285. wtgq_nvp replaces - ! the canopy-iteration wtgq (which was computed with soilresis) so the NVP - ! diagnostic reflects the moss/lichen-specific surface resistance.] - if (use_nvp) then - if (col%frac_nvp(c) > 0._r8) then - delq_nvp = wtalq(p)*qg_nvp(c)-wtlq0(p)*qsatl(p)-wtaq0(p)*forc_q(c) - wtgq_nvp = frac_veg_nosno(p) / (raw(p,below_canopy) + rnvp_col(c)) - qflx_ev_nvp(p) = forc_rho(c) * wtgq_nvp * delq_nvp - end if + ! [PORTED by Hui Tang: NVP individual latent heat flux, analogous to snow/h2osfc] + ! qflx_evap_soi already includes NVP because qg(c) blends NVP in SurfaceHumidityMod. + ! This is the diagnostic breakdown of the NVP contribution. + if (use_nvp .and. col%frac_nvp(c) > 0._r8) then + delq_nvp = wtalq(p)*qg_nvp(c)-wtlq0(p)*qsatl(p)-wtaq0(p)*forc_q(c) + qflx_ev_nvp(p) = forc_rho(c)*wtgq(p)*delq_nvp else qflx_ev_nvp(p) = 0._r8 end if diff --git a/src/biogeophys/NVPParamsMod.F90 b/src/biogeophys/NVPParamsMod.F90 index 125d1b410f..652c12eb6d 100644 --- a/src/biogeophys/NVPParamsMod.F90 +++ b/src/biogeophys/NVPParamsMod.F90 @@ -35,10 +35,6 @@ module NVPParamsMod real(r8) :: rnvp_min = 10.0_r8 ! minimum surface resistance when saturated [s m-1] real(r8) :: rnvp_amp = 500.0_r8 ! amplitude of resistance increase when dry [s m-1] real(r8) :: rnvp_exp = 3.0_r8 ! exponent of dryness function [-] - ! [PORTED by Hui Tang: surface resistance applied when NVP is frozen — typical literature - ! range for ice/snow surfaces is ~1000-3000 s/m; default 1500 s/m suppresses but does - ! not zero sublimation. Set to a very large value (e.g. 1e6) to disable evap when frozen.] - real(r8) :: rnvp_ice = 1500.0_r8 ! NVP resistance when frozen [s m-1] ! Hydraulic properties (Mualem-van Genuchten) real(r8) :: ksat_nvp = 1.0e-4_r8 ! saturated hydraulic conductivity [m s-1] diff --git a/src/biogeophys/SoilStateType.F90 b/src/biogeophys/SoilStateType.F90 index c3c90ee1cb..e491613ceb 100644 --- a/src/biogeophys/SoilStateType.F90 +++ b/src/biogeophys/SoilStateType.F90 @@ -45,8 +45,6 @@ module SoilStateType real(r8), pointer :: sucsat_col (:,:) ! col minimum soil suction (mm) (nlevgrnd) real(r8), pointer :: dsl_col (:) ! col dry surface layer thickness (mm) real(r8), pointer :: soilresis_col (:) ! col soil evaporative resistance S&L14 (s/m) - ! [PORTED by Hui Tang: NVP (moss/lichen) surface evaporative resistance, analogous to soilresis_col] - real(r8), pointer :: rnvp_col (:) ! col NVP surface evaporative resistance (s/m) real(r8), pointer :: soilbeta_col (:) ! col factor that reduces ground evaporation L&P1992(-) real(r8), pointer :: soilalpha_col (:) ! col factor that reduces ground saturated specific humidity (-) real(r8), pointer :: soilalpha_u_col (:) ! col urban factor that reduces ground saturated specific humidity (-) @@ -146,9 +144,7 @@ subroutine InitAllocate(this, bounds) allocate(this%watfc_col (begc:endc,nlevgrnd)) ; this%watfc_col (:,:) = nan allocate(this%sucsat_col (begc:endc,nlevgrnd)) ; this%sucsat_col (:,:) = spval allocate(this%dsl_col (begc:endc)) ; this%dsl_col (:) = spval!nan - allocate(this%soilresis_col (begc:endc)) ; this%soilresis_col (:) = spval!nan - ! [PORTED by Hui Tang: allocate NVP surface resistance unconditionally — small array, safe defaults] - allocate(this%rnvp_col (begc:endc)) ; this%rnvp_col (:) = spval + allocate(this%soilresis_col (begc:endc)) ; this%soilresis_col (:) = spval!nan allocate(this%soilbeta_col (begc:endc)) ; this%soilbeta_col (:) = nan allocate(this%soilalpha_col (begc:endc)) ; this%soilalpha_col (:) = nan allocate(this%soilalpha_u_col (begc:endc)) ; this%soilalpha_u_col (:) = nan @@ -312,12 +308,6 @@ subroutine InitHistory(this, bounds) avgflag='A', long_name='soil resistance to evaporation', & ptr_col=this%soilresis_col) - ! [PORTED by Hui Tang: NVP surface resistance history field, analogous to SOILRESIS] - this%rnvp_col(begc:endc) = spval - call hist_addfld1d (fname='RNVP', units='s/m', & - avgflag='A', long_name='NVP (moss/lichen) surface resistance to evaporation', & - ptr_col=this%rnvp_col, default='inactive') - this%dsl_col(begc:endc) = spval call hist_addfld1d (fname='DSL', units='mm', & avgflag='A', long_name='dry surface layer thickness', & diff --git a/src/biogeophys/SurfaceResistanceMod.F90 b/src/biogeophys/SurfaceResistanceMod.F90 index 38775c7f59..9b04327252 100644 --- a/src/biogeophys/SurfaceResistanceMod.F90 +++ b/src/biogeophys/SurfaceResistanceMod.F90 @@ -26,8 +26,6 @@ module SurfaceResistanceMod ! ! !PUBLIC MEMBER FUNCTIONS: public :: calc_soilevap_resis - ! [PORTED by Hui Tang: NVP surface resistance calculation, analogous to calc_soilevap_resis] - public :: calc_nvp_resis public :: do_soilevap_beta, do_soil_resistance_sl14 ! public :: init_soil_resistance public :: soil_resistance_readNL @@ -238,67 +236,8 @@ subroutine calc_soilevap_resis(bounds, num_nolakec, filter_nolakec, & end associate end subroutine calc_soilevap_resis - - !------------------------------------------------------------------------------ - ! [PORTED by Hui Tang: compute NVP (moss/lichen) surface resistance, parallel to - ! calc_soilevap_resis. Uses fwet_nvp_col (NVP wet fraction = effective saturation) - ! as satfrac in the van de Griend & Owe / Daamen & Simmonds formula. When the NVP - ! layer is frozen, the empirical formula is replaced by the runtime-tunable rnvp_ice - ! to represent ice-surface resistance. When NVP is inactive (frac_nvp == 0 or - ! layer not active), rnvp_col is set to spval — callers must guard with use_nvp.] - subroutine calc_nvp_resis(bounds, num_nolakec, filter_nolakec, & - soilstate_inst, waterdiagnosticbulk_inst, temperature_inst) - ! - ! !USES: - use clm_varcon , only : tfrz, spval - use clm_varctl , only : use_nvp - use ColumnType , only : col - use NVPParamsMod , only : rnvp_min, rnvp_amp, rnvp_exp, rnvp_ice - use decompMod , only : bounds_type - ! - ! !ARGUMENTS: - implicit none - type(bounds_type) , intent(in) :: bounds - integer , intent(in) :: num_nolakec - integer , intent(in) :: filter_nolakec(:) - type(soilstate_type) , intent(inout) :: soilstate_inst - type(waterdiagnosticbulk_type) , intent(in) :: waterdiagnosticbulk_inst - type(temperature_type) , intent(in) :: temperature_inst - ! - ! !LOCAL VARIABLES: - integer :: c, fc - real(r8) :: satfrac - - if (.not. use_nvp) return - - associate( & - fwet_nvp_col => waterdiagnosticbulk_inst%fwet_nvp_col , & ! Input: NVP wet fraction [-] - t_soisno => temperature_inst%t_soisno_col , & ! Input: layer-0 temperature [K] - rnvp_col => soilstate_inst%rnvp_col & ! Output: NVP surface resistance [s/m] - ) - - do fc = 1, num_nolakec - c = filter_nolakec(fc) - - if (col%frac_nvp(c) > 0._r8 .and. col%nvp_layer_active(c)) then - if (t_soisno(c,0) >= tfrz) then - ! Unfrozen: van de Griend & Owe / Daamen & Simmonds dryness curve - satfrac = max(0._r8, min(1._r8, fwet_nvp_col(c))) - rnvp_col(c) = rnvp_min + rnvp_amp * (1._r8 - satfrac)**rnvp_exp - else - ! Frozen: literature ice/snow resistance, suppresses but doesn't zero sublimation - rnvp_col(c) = rnvp_ice - end if - else - rnvp_col(c) = spval - end if - end do - - end associate - - end subroutine calc_nvp_resis - - !------------------------------------------------------------------------------ + + !------------------------------------------------------------------------------ subroutine calc_beta_leepielke1992(bounds, num_nolakec, filter_nolakec, & soilstate_inst, waterstatebulk_inst, waterdiagnosticbulk_inst, soilbeta) ! From 01641f45256d3ae23fa94d5fa827da3ace933ca8 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Thu, 30 Apr 2026 23:51:31 +0300 Subject: [PATCH 039/113] NVP bug fix: always set a valid value for t_nvp_col. --- src/biogeophys/SoilTemperatureMod.F90 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/biogeophys/SoilTemperatureMod.F90 b/src/biogeophys/SoilTemperatureMod.F90 index 61fda6ad35..7c0054175c 100644 --- a/src/biogeophys/SoilTemperatureMod.F90 +++ b/src/biogeophys/SoilTemperatureMod.F90 @@ -583,17 +583,20 @@ subroutine SoilTemperature(bounds, num_urbanl, filter_urbanl, num_urbanc, filter ! [PORTED by Hui Tang: update t_nvp_col BEFORE t_grnd so t_grnd can use it] ! Default: inactive-NVP columns track soil layer 1. Active-NVP columns track ! layer 0. Use the nvpc filter so the active override is O(NVP columns) only. - if (use_nvp) then + do fc = 1, num_nolakec c = filter_nolakec(fc) t_nvp_col(c) = t_soisno(c,1) ! default: no NVP → layer 1 end do + + if (use_nvp) then do fc = 1, num_nvpc ! [PORTED: override for NVP-active columns] c = filter_nvpc(fc) t_nvp_col(c) = t_soisno(c,0) end do end if + do fc = 1,num_nolakec c = filter_nolakec(fc) ! [PORTED by Hui Tang: NVP fractional area for t_grnd blend (excludes snow and h2osfc)] From 5f4fd3b3a64df4552694c28cd1d8e5d5b9a43513 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Thu, 30 Apr 2026 23:58:29 +0300 Subject: [PATCH 040/113] Add debugging output to log files. --- src/biogeophys/SnowHydrologyMod.F90 | 47 +++++++++++++++++++++++++-- src/biogeophys/SoilFluxesMod.F90 | 16 +++++++++ src/biogeophys/SoilTemperatureMod.F90 | 15 ++++++--- 3 files changed, 71 insertions(+), 7 deletions(-) diff --git a/src/biogeophys/SnowHydrologyMod.F90 b/src/biogeophys/SnowHydrologyMod.F90 index 578769d9ea..cbd92eb8f0 100644 --- a/src/biogeophys/SnowHydrologyMod.F90 +++ b/src/biogeophys/SnowHydrologyMod.F90 @@ -1228,6 +1228,19 @@ subroutine UpdateState_TopLayerFluxes(bounds, num_snowc, filter_snowc, & h2osoi_liq(c,lev_top(c)) = h2osoi_liq(c,lev_top(c)) + & frac_sno_eff(c) * (qflx_liq_grnd(c) + qflx_liqdew_to_top_layer(c) & - qflx_liqevap_from_top_layer(c)) * dtime + + ! [PORTED by Hui Tang: NaN diagnostic — catch NaN from flux inputs before it propagates] + if (h2osoi_liq(c,lev_top(c)) /= h2osoi_liq(c,lev_top(c))) then + write(iulog,*) "NaN DIAGNOSTIC: h2osoi_liq is NaN after UpdateState_TopLayerFluxes" + write(iulog,*) " c, lev_top(c) = ", c, lev_top(c) + write(iulog,*) " h2osoi_liq_top_orig = ", h2osoi_liq_top_orig(c) + write(iulog,*) " frac_sno_eff = ", frac_sno_eff(c) + write(iulog,*) " qflx_liq_grnd*dtime = ", qflx_liq_grnd(c)*dtime + write(iulog,*) " qflx_liqdew_to_top_layer*dtime = ", qflx_liqdew_to_top_layer(c)*dtime + write(iulog,*) " qflx_liqevap_from_top_layer*dtime=", qflx_liqevap_from_top_layer(c)*dtime + call endrun(subgrid_index=c, subgrid_level=subgrid_level_column, & + msg="NaN in h2osoi_liq after UpdateState_TopLayerFluxes — check flux inputs") + end if end do ! If states were supposed to go to 0 but instead ended up near-0 (positive or @@ -1320,6 +1333,7 @@ subroutine BulkFlux_SnowPercolation(bounds, num_snowc, filter_snowc, & real(r8) :: vol_liq(bounds%begc:bounds%endc,-nlevsno+1:0) ! partial volume of liquid water in layer real(r8) :: vol_ice(bounds%begc:bounds%endc,-nlevsno+1:0) ! partial volume of ice lens in layer real(r8) :: eff_porosity(bounds%begc:bounds%endc,-nlevsno+1:0) ! effective porosity = porosity - vol_ice + real(r8) :: denom_j ! dz*frac_sno_eff denominator for vol calculations character(len=*), parameter :: subname = 'BulkFlux_SnowPercolation' !----------------------------------------------------------------------- @@ -1338,9 +1352,17 @@ subroutine BulkFlux_SnowPercolation(bounds, num_snowc, filter_snowc, & c = filter_snowc(fc) if (j >= snl(c)+1) then ! need to scale dz by frac_sno to convert to grid cell average depth - vol_ice(c,j) = min(1._r8, h2osoi_ice(c,j)/(dz(c,j)*frac_sno_eff(c)*denice)) - eff_porosity(c,j) = 1._r8 - vol_ice(c,j) - vol_liq(c,j) = min(eff_porosity(c,j),h2osoi_liq(c,j)/(dz(c,j)*frac_sno_eff(c)*denh2o)) + ! [PORTED by Hui Tang: guard zero denominator; dz*frac_sno_eff=0 gives 0/0=NaN without this] + denom_j = dz(c,j) * frac_sno_eff(c) + if (denom_j > 0._r8) then + vol_ice(c,j) = min(1._r8, h2osoi_ice(c,j)/(denom_j*denice)) + eff_porosity(c,j) = max(0._r8, 1._r8 - vol_ice(c,j)) + vol_liq(c,j) = min(eff_porosity(c,j), h2osoi_liq(c,j)/(denom_j*denh2o)) + else + vol_ice(c,j) = 0._r8 + eff_porosity(c,j) = 1._r8 + vol_liq(c,j) = 0._r8 + end if end if end do end do @@ -2749,6 +2771,14 @@ subroutine DivideSnowLayers(bounds, num_snowc, filter_snowc, & zwice(wi) = propor*swice(wi,c,k) zwliq(wi) = propor*swliq(wi,c,k) end do + + write(iulog,*) 'msno=',msno, & + 'dzsno=',dzsno(c,k), & + 'swliq=',swliq(:,c,k), & + 'swice=',swice(:,c,k), & + 'zwliq=',zwliq, & + 'zwice=',zwice + zmbc_phi = propor*mbc_phi(c,k) zmbc_pho = propor*mbc_pho(c,k) zmoc_phi = propor*moc_phi(c,k) @@ -2785,6 +2815,12 @@ subroutine DivideSnowLayers(bounds, num_snowc, filter_snowc, & mdst4(c,k+1) = mdst4(c,k+1)+zmdst4 ! (combo) ! Mass-weighted combination of radius + write(iulog,*) 'c=',c, & + 'k=',k, & + 'dzmax_u=', dzmax_u(k), & + 'swliq(k+1)=', swliq(:,c,:), & + 'swice(k+1)=', swice(:,c,:) + rds(c,k+1) = MassWeightedSnowRadius( rds(c,k), rds(c,k+1), & (swliq(i_bulk,c,k+1)+swice(i_bulk,c,k+1)), (zwliq(i_bulk)+zwice(i_bulk)) ) @@ -3961,6 +3997,11 @@ function MassWeightedSnowRadius( rds1, rds2, swtot, zwtot ) result(mass_weighted real(r8), intent(IN) :: zwtot ! snow water total layer 1 real(r8) :: mass_weighted_snowradius ! resulting bounded mass weighted snow radius + if (.not. (swtot + zwtot > 0._r8)) then + write(iulog,*) 'MassWeightedSnowRadius about to abort: rds1=',rds1, & + ' rds2=',rds2,' swtot=',swtot,' zwtot=',zwtot + end if + SHR_ASSERT_FL( (swtot+zwtot > 0.0_r8), sourcefile, __LINE__) mass_weighted_snowradius = (rds2*swtot + rds1*zwtot)/(swtot+zwtot) diff --git a/src/biogeophys/SoilFluxesMod.F90 b/src/biogeophys/SoilFluxesMod.F90 index 7278c2779f..e8a3c0d6de 100644 --- a/src/biogeophys/SoilFluxesMod.F90 +++ b/src/biogeophys/SoilFluxesMod.F90 @@ -202,6 +202,22 @@ subroutine SoilFluxes (bounds, num_urbanl, filter_urbanl, & qflx_ev_nvp(p) = 0._r8 qflx_ev_snow(p) = qflx_evap_soi(p) else + ! [PORTED by Hui Tang: NaN diagnostic — identify which term makes qflx_ev_snow NaN] + if ((qflx_ev_snow(p) /= qflx_ev_snow(p)) .or. & + (tinc(c)*cgrndl(p) /= tinc(c)*cgrndl(p))) then + write(iulog,*) "NaN DIAGNOSTIC SoilFluxesMod: before qflx_ev_snow linearization" + write(iulog,*) " p, c = ", p, c + write(iulog,*) " qflx_ev_snow(p) = ", qflx_ev_snow(p) + write(iulog,*) " tinc(c) = ", tinc(c) + write(iulog,*) " cgrndl(p) = ", cgrndl(p) + write(iulog,*) " tinc*cgrndl = ", tinc(c)*cgrndl(p) + write(iulog,*) " t_grnd(c) = ", t_grnd(c) + write(iulog,*) " t_grnd0(c) = ", t_grnd0(c) + write(iulog,*) " tssbef(c,snl+1) = ", tssbef(c,col%snl(c)+1) + write(iulog,*) " frac_sno_eff(c) = ", frac_sno_eff(c) + !call endrun(subgrid_index=p, subgrid_level=subgrid_level_patch, & + ! msg="NaN in qflx_ev_snow or tinc*cgrndl in SoilFluxesMod") + end if qflx_ev_snow(p) = qflx_ev_snow(p) + tinc(c)*cgrndl(p) qflx_ev_soil(p) = qflx_ev_soil(p) + tinc(c)*cgrndl(p) qflx_ev_h2osfc(p) = qflx_ev_h2osfc(p) + tinc(c)*cgrndl(p) diff --git a/src/biogeophys/SoilTemperatureMod.F90 b/src/biogeophys/SoilTemperatureMod.F90 index 7c0054175c..200eb2645d 100644 --- a/src/biogeophys/SoilTemperatureMod.F90 +++ b/src/biogeophys/SoilTemperatureMod.F90 @@ -439,12 +439,16 @@ subroutine SoilTemperature(bounds, num_urbanl, filter_urbanl, num_urbanc, filter call t_startf( 'SoilTempBandDiag') ! Solve the system + print*, "tvector_before=", tvector + print*, "nvp_layer_active=", col%nvp_layer_active, snl call BandDiagonal(bounds, -nlevsno, nlevmaxurbgrnd, jtop(begc:endc), jbot(begc:endc), & num_nolakec, filter_nolakec, nband, bmatrix(begc:endc, :, :), & rvector(begc:endc, :), tvector(begc:endc, :)) call t_stopf( 'SoilTempBandDiag') + print*, "tvector_after=", tvector + ! return temperatures to original array do fc = 1,num_nolakec @@ -584,10 +588,10 @@ subroutine SoilTemperature(bounds, num_urbanl, filter_urbanl, num_urbanc, filter ! Default: inactive-NVP columns track soil layer 1. Active-NVP columns track ! layer 0. Use the nvpc filter so the active override is O(NVP columns) only. - do fc = 1, num_nolakec - c = filter_nolakec(fc) - t_nvp_col(c) = t_soisno(c,1) ! default: no NVP → layer 1 - end do + do fc = 1, num_nolakec + c = filter_nolakec(fc) + t_nvp_col(c) = t_soisno(c,1) ! default: no NVP → layer 1 + end do if (use_nvp) then do fc = 1, num_nvpc ! [PORTED: override for NVP-active columns] @@ -596,6 +600,7 @@ subroutine SoilTemperature(bounds, num_urbanl, filter_urbanl, num_urbanc, filter end do end if + print *, "t_nvp_col=", t_nvp_col, t_soisno(c,:) do fc = 1,num_nolakec c = filter_nolakec(fc) @@ -817,6 +822,8 @@ subroutine SoilThermProp (bounds, num_urbanc, filter_urbanc, num_nolakec, filter if (snl(c)+1 < 1 .AND. (j >= snl(c)+1) .AND. (j <= 0) .AND. & .NOT. (use_nvp .AND. jbot_sno(c) == -1 .AND. j == 0)) then bw(c,j) = (h2osoi_ice(c,j)+h2osoi_liq(c,j))/(frac_sno(c)*dz(c,j)) + print *, 'bw=', bw(c,j), h2osoi_ice(c,j), h2osoi_liq(c,j),dz(c,j) + l = col%landunit(c) ! Select method over glacier land unit From 683a86a2d89f873a3ab64252f1813e006deba54b Mon Sep 17 00:00:00 2001 From: Sam Rabin Date: Fri, 1 May 2026 09:38:24 -0600 Subject: [PATCH 041/113] Point FATES at Hui Tang's branch, commit 49c247b39. Add alternative parameter file for moss, assuming arctic grass (12) of the host model to be nvp. --- .gitmodules | 4 ++-- src/fates | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitmodules b/.gitmodules index df9b5d91a9..08e6fb2104 100644 --- a/.gitmodules +++ b/.gitmodules @@ -27,8 +27,8 @@ # [submodule "fates"] path = src/fates -url = https://github.com/NGEET/fates -fxtag = sci.1.91.1_api.43.1.0 +url = https://github.com/huitang-earth/fates # "Add alternative parameter file for moss..." +fxtag = 49c247b396751aad117d8d03d290edc84ba222af fxrequired = AlwaysRequired # Standard Fork to compare to with "git fleximod test" to ensure personal forks aren't committed fxDONOTUSEurl = https://github.com/NGEET/fates diff --git a/src/fates b/src/fates index e027a4030d..49c247b396 160000 --- a/src/fates +++ b/src/fates @@ -1 +1 @@ -Subproject commit e027a4030d2a0f09039fb337ad67ced7461dd4f0 +Subproject commit 49c247b396751aad117d8d03d290edc84ba222af From cda744e3ded52a15cd65fa7b16dbb36197c76eb7 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Mon, 4 May 2026 00:08:13 +0300 Subject: [PATCH 042/113] NVP bug fix: add 'use_nvp' guard for calculating top soil layer infiltration flux. --- src/biogeophys/SoilHydrologyMod.F90 | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/biogeophys/SoilHydrologyMod.F90 b/src/biogeophys/SoilHydrologyMod.F90 index 92eeafbca6..401c6f5169 100644 --- a/src/biogeophys/SoilHydrologyMod.F90 +++ b/src/biogeophys/SoilHydrologyMod.F90 @@ -465,8 +465,13 @@ subroutine Infiltration(bounds, num_hydrologyc, filter_hydrologyc, & do fc = 1, num_hydrologyc c = filter_hydrologyc(fc) - ! [PORTED by Hui Tang: add NVP drainage to total infiltration into soil layer 1] - qflx_infl(c) = qflx_in_soil_limited(c) + qflx_h2osfc_drain(c) + qflx_nvp_drain_col(c) + ! [PORTED by Hui Tang: add NVP drainage to total infiltration into soil layer 1; + ! guard with use_nvp to avoid NaN when qflx_nvp_drain_col is uninitialised] + if (use_nvp) then + qflx_infl(c) = qflx_in_soil_limited(c) + qflx_h2osfc_drain(c) + qflx_nvp_drain_col(c) + else + qflx_infl(c) = qflx_in_soil_limited(c) + qflx_h2osfc_drain(c) + end if end do end associate From 2513aafb04b921b28cb13d5ebbfc0703a1c2864a Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Thu, 7 May 2026 00:11:19 +0300 Subject: [PATCH 043/113] bug fix: avoid too large hs values for h2osfc, when it is absent. --- src/biogeophys/SoilTemperatureMod.F90 | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/biogeophys/SoilTemperatureMod.F90 b/src/biogeophys/SoilTemperatureMod.F90 index 200eb2645d..ff87358cbd 100644 --- a/src/biogeophys/SoilTemperatureMod.F90 +++ b/src/biogeophys/SoilTemperatureMod.F90 @@ -2380,12 +2380,21 @@ subroutine SetRHSVec_StandingSurfaceWater(bounds, num_nolakec, filter_nolakec, d do fc = 1,num_nolakec c = filter_nolakec(fc) + ! When there is no standing water (c_h2osfc == thin_sfclayer), dtime/c_h2osfc = 1.8e9 + ! which amplifies any non-zero hs_h2osfc by ~10^9, producing an unphysically huge RHS + ! that corrupts the banded solver and cascades to NaN in subsequent timesteps. + ! Use a trivial identity row (rt = t_h2osfc, no coupling) instead. + if (c_h2osfc(c) <= thin_sfclayer) then + fn_h2osfc(c) = 0.0_r8 + rt(c,1) = t_h2osfc(c) + else ! surface water layer has two coefficients dzm=(0.5*dz_h2osfc(c)+col%z(c,1)) fn_h2osfc(c)=tk_h2osfc(c)*(t_soisno(c,1)-t_h2osfc(c))/dzm rt(c,1)= t_h2osfc(c) + (dtime/c_h2osfc(c)) & *( hs_h2osfc(c) - dhsdT(c)*t_h2osfc(c) + cnfac*fn_h2osfc(c) )!rhs for h2osfc + end if enddo @@ -3094,6 +3103,12 @@ subroutine SetMatrix_StandingSurfaceWater(bounds, num_nolakec, filter_nolakec, d do fc = 1,num_nolakec c = filter_nolakec(fc) + ! When there is no standing water (c_h2osfc == thin_sfclayer), dtime/c_h2osfc = 1.8e9 + ! which inflates diagonal and off-diagonal coefficients to ~10^11, corrupting the solver. + ! Use a trivial identity row (diagonal=1, off-diagonals=0) instead. + if (c_h2osfc(c) <= thin_sfclayer) then + bmatrix_ssw(c,3,0) = 1.0_r8 ! diagonal = 1; off-diagonals already 0 from init + else ! surface water layer has two coefficients dzm=(0.5*dz_h2osfc(c)+col%z(c,1)) @@ -3106,6 +3121,7 @@ subroutine SetMatrix_StandingSurfaceWater(bounds, num_nolakec, filter_nolakec, d if ( frac_h2osfc(c) /= 0.0_r8 )then bmatrix_soil_ssw(c,4,1)= - frac_h2osfc(c) * (1._r8-cnfac) * fact(c,1) & * tk_h2osfc(c)/dzm !flux from h2osfc + end if end if enddo From 4156a30fbe7881bf2997993b1f4984fbf322e7c3 Mon Sep 17 00:00:00 2001 From: Sam Rabin Date: Sun, 10 May 2026 10:08:12 -0600 Subject: [PATCH 044/113] Fix git-fleximod FATES --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index 08e6fb2104..8c57d557c2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -27,7 +27,7 @@ # [submodule "fates"] path = src/fates -url = https://github.com/huitang-earth/fates # "Add alternative parameter file for moss..." +url = https://github.com/huitang-earth/fates fxtag = 49c247b396751aad117d8d03d290edc84ba222af fxrequired = AlwaysRequired # Standard Fork to compare to with "git fleximod test" to ensure personal forks aren't committed From f183b464846c935efd87cadf869ac870b4aaff22 Mon Sep 17 00:00:00 2001 From: Sam Rabin Date: Mon, 11 May 2026 14:21:09 -0600 Subject: [PATCH 045/113] use_nvp now actually default false. --- src/main/clm_varctl.F90 | 2 +- src/main/controlMod.F90 | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/clm_varctl.F90 b/src/main/clm_varctl.F90 index 509c38374c..a3e6503bec 100644 --- a/src/main/clm_varctl.F90 +++ b/src/main/clm_varctl.F90 @@ -384,7 +384,7 @@ module clm_varctl !---------------------------------------------------------- ! true => activate nvp model - logical, public :: use_nvp = .true. + logical, public :: use_nvp = .false. ! true => nvp can photosynthesize under snow logical, public :: use_nvp_undersnow = .true. diff --git a/src/main/controlMod.F90 b/src/main/controlMod.F90 index 68cd7aad22..8429ac2f2f 100644 --- a/src/main/controlMod.F90 +++ b/src/main/controlMod.F90 @@ -1322,6 +1322,8 @@ subroutine control_print () write(iulog, *) ' fates_seeddisp_cadence = ', fates_seeddisp_cadence write(iulog, *) ' fates_seeddisp_cadence: 0, 1, 2, 3 => off, daily, monthly, or yearly dispersal' write(iulog, *) ' fates_inventory_ctrl_filename = ', trim(fates_inventory_ctrl_filename) + write(iulog, *) ' use_nvp= ', use_nvp + write(iulog, *) ' use_nvp_undersnow= ', use_nvp_undersnow write(iulog, *) ' use_fates_managed_fire= ', use_fates_managed_fire end if end subroutine control_print From dae7fe48d08e3664d472ab6fe5019bac503d3149 Mon Sep 17 00:00:00 2001 From: Sam Rabin Date: Mon, 11 May 2026 14:22:09 -0600 Subject: [PATCH 046/113] NVP namelist vars now handled in CLMBuildNamelist.pm. --- bld/CLMBuildNamelist.pm | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bld/CLMBuildNamelist.pm b/bld/CLMBuildNamelist.pm index a083bae6ae..798fbacdff 100755 --- a/bld/CLMBuildNamelist.pm +++ b/bld/CLMBuildNamelist.pm @@ -823,7 +823,8 @@ sub setup_cmdl_fates_mode { "use_fates_daylength_factor", "fates_photosynth_acclimation", "fates_stomatal_model", "fates_stomatal_assimilation", "fates_leafresp_model", "fates_cstarvation_model", "fates_regeneration_model", "fates_hydro_solver", "fates_radiation_model", "fates_electron_transport_model", - "use_fates_managed_fire" + "use_fates_managed_fire", + "use_nvp", "use_nvp_undersnow", "nvp_rad_model_ground" ); # dis-allow fates specific namelist items with non-fates runs @@ -4887,7 +4888,8 @@ sub setup_logic_fates { "use_fates_daylength_factor", "fates_photosynth_acclimation", "fates_stomatal_model", "fates_stomatal_assimilation", "fates_leafresp_model", "fates_cstarvation_model", "fates_regeneration_model", "fates_hydro_solver", "fates_radiation_model", "fates_electron_transport_model", - "use_fates_managed_fire" + "use_fates_managed_fire", + "use_nvp", "use_nvp_undersnow", "nvp_rad_model_ground" ); foreach my $var ( @list ) { @@ -4910,6 +4912,7 @@ sub setup_logic_fates { add_default($opts, $nl_flags->{'inputdata_rootdir'}, $definition, $defaults, $nl, 'fates_spitfire_mode', 'use_fates'=>$nl_flags->{'use_fates'}, 'use_fates_managed_fire'=>$nl->get_value('use_fates_managed_fire'), 'use_fates_sp'=>$nl_flags->{'use_fates_sp'} ); + add_default($opts, $nl_flags->{'inputdata_rootdir'}, $definition, $defaults, $nl, 'use_nvp'); my $suplnitro = $nl->get_value('suplnitro'); my $parteh_mode = $nl->get_value('fates_parteh_mode'); From d39516d9ea8e424dc97fb63eb71733f3482841b1 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Tue, 12 May 2026 22:59:19 +0300 Subject: [PATCH 047/113] Disable the use of canopy_fraction_pa for calculating frac_nvp(c) for the moment --- src/utils/clmfates_interfaceMod.F90 | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/utils/clmfates_interfaceMod.F90 b/src/utils/clmfates_interfaceMod.F90 index a89b0bdd52..597524f043 100644 --- a/src/utils/clmfates_interfaceMod.F90 +++ b/src/utils/clmfates_interfaceMod.F90 @@ -1814,8 +1814,13 @@ subroutine wrap_update_hlmfates_dyn(this, nc, bounds_clump, & this%fates(nc)%bc_out(s)%nvp_frac_pa(ifp) * & this%fates(nc)%bc_out(s)%canopy_fraction_pa(ifp) col%frac_nvp(c) = col%frac_nvp(c) + & - this%fates(nc)%bc_out(s)%nvp_frac_pa(ifp) * & - this%fates(nc)%bc_out(s)%canopy_fraction_pa(ifp) + this%fates(nc)%bc_out(s)%nvp_frac_pa(ifp) !* & + !this%fates(nc)%bc_out(s)%canopy_fraction_pa(ifp) + print*, 'frac_nvp=', col%frac_nvp(c), & + this%fates(nc)%bc_out(s)%nvp_frac_pa(ifp), & + this%fates(nc)%bc_out(s)%canopy_fraction_pa(ifp),& + c,ifp + end do ! [PORTED by Hui Tang: pass thermo instances only when present (normal timestep)] if (present(temperature_inst) .and. present(waterstatebulk_inst)) then From c33254d3c9953f97d654559a3effa704df8abe9d Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Tue, 12 May 2026 23:11:02 +0300 Subject: [PATCH 048/113] Use jbot_sno(c)==-1 directly (not filter_nvpc) so this can run on the first time step --- src/biogeophys/SoilTemperatureMod.F90 | 36 ++++++++++++++++----------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/biogeophys/SoilTemperatureMod.F90 b/src/biogeophys/SoilTemperatureMod.F90 index ff87358cbd..476585db32 100644 --- a/src/biogeophys/SoilTemperatureMod.F90 +++ b/src/biogeophys/SoilTemperatureMod.F90 @@ -937,14 +937,16 @@ subroutine SoilThermProp (bounds, num_urbanc, filter_urbanc, num_nolakec, filter end do end do - ! [PORTED by Hui Tang: NVP-soil interface conductivity when NVP at layer 0 — nvpc filter] - ! The standard loop above uses j >= snl(c)+1; when snl=0 this starts at j=1 and - ! skips j=0. Compute tk(c,0) explicitly here for NVP cases with or without snow. + ! [PORTED by Hui Tang: NVP-soil interface conductivity when NVP at layer 0] + ! Use jbot_sno(c)==-1 directly (not filter_nvpc) so this runs on the first + ! timestep before filter_nvpc has been built by setNVPcFilter. if (use_nvp) then - do fc = 1, num_nvpc - c = filter_nvpc(fc) - tk(c,0) = thk(c,0)*thk(c,1)*(z(c,1)-z(c,0)) & - /(thk(c,0)*(z(c,1)-zi(c,0))+thk(c,1)*(zi(c,0)-z(c,0))) + do fc = 1, num_nolakec + c = filter_nolakec(fc) + if (col%jbot_sno(c) == -1) then + tk(c,0) = thk(c,0)*thk(c,1)*(z(c,1)-z(c,0)) & + /(thk(c,0)*(z(c,1)-zi(c,0))+thk(c,1)*(zi(c,0)-z(c,0))) + end if end do end if @@ -1016,18 +1018,22 @@ subroutine SoilThermProp (bounds, num_urbanc, filter_urbanc, num_nolakec, filter end do end do - ! [PORTED by Hui Tang: NVP layer heat capacity at j=0 — nvpc filter] - ! Soil-style formula: dry matrix + pore water/ice contributions. - ! cv = csol_nvp*(1-watsat_nvp)*dz + cpliq*h2osoi_liq + cpice*h2osoi_ice + ! [PORTED by Hui Tang: NVP layer heat capacity at j=0] + ! Use jbot_sno(c)==-1 directly (not filter_nvpc) so this runs on the first + ! timestep before filter_nvpc has been built by setNVPcFilter. if (use_nvp) then - do fc = 1, num_nvpc - c = filter_nvpc(fc) - cv(c,0) = max(thin_sfclayer, & - csol_nvp*(1._r8 - watsat_nvp)*dz(c,0) & - + cpliq*h2osoi_liq(c,0) + cpice*h2osoi_ice(c,0)) + do fc = 1, num_nolakec + c = filter_nolakec(fc) + if (col%jbot_sno(c) == -1) then + cv(c,0) = max(thin_sfclayer, & + csol_nvp*(1._r8 - watsat_nvp)*dz(c,0) & + + cpliq*h2osoi_liq(c,0) + cpice*h2osoi_ice(c,0)) + end if end do end if + print *, "cv(:,:)=", cv + call t_stopf( 'SoilThermProp' ) end associate From d61e55f16b0db68d05b3e0e16286cee74238d4ae Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Tue, 12 May 2026 23:17:56 +0300 Subject: [PATCH 049/113] Set qflx_ev_nvp(p) = 0._r8 when NVP is buried under snow --- src/biogeophys/SoilFluxesMod.F90 | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/biogeophys/SoilFluxesMod.F90 b/src/biogeophys/SoilFluxesMod.F90 index e8a3c0d6de..d219ca4e35 100644 --- a/src/biogeophys/SoilFluxesMod.F90 +++ b/src/biogeophys/SoilFluxesMod.F90 @@ -10,7 +10,7 @@ module SoilFluxesMod use decompMod , only : bounds_type use abortutils , only : endrun use perf_mod , only : t_startf, t_stopf - use clm_varctl , only : iulog + use clm_varctl , only : iulog, use_nvp ! [PORTED by Hui Tang: use_nvp for NVP snow-burial guard] use clm_varpar , only : nlevsno, nlevgrnd, nlevurb use atm2lndType , only : atm2lnd_type use CanopyStateType , only : canopystate_type @@ -222,7 +222,14 @@ subroutine SoilFluxes (bounds, num_urbanl, filter_urbanl, & qflx_ev_soil(p) = qflx_ev_soil(p) + tinc(c)*cgrndl(p) qflx_ev_h2osfc(p) = qflx_ev_h2osfc(p) + tinc(c)*cgrndl(p) ! [PORTED by Hui Tang: apply linearization correction to NVP evaporation diagnostic] - qflx_ev_nvp(p) = qflx_ev_nvp(p) + tinc(c)*cgrndl(p) + ! Skip when NVP is buried under snow (snl < -1): qflx_ev_nvp was zeroed in + ! BareGroundFluxesMod/CanopyFluxesMod and must remain zero to avoid a water + ! balance error (non-zero qflx_ev_nvp_col with no corresponding water removal). + if (use_nvp .and. col%snl(c) < -1) then + qflx_ev_nvp(p) = 0._r8 + else + qflx_ev_nvp(p) = qflx_ev_nvp(p) + tinc(c)*cgrndl(p) + end if endif end do From 4423d8eec5b16e763d7e367339ff703c5d0e7f5f Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Tue, 12 May 2026 23:28:37 +0300 Subject: [PATCH 050/113] Exclude the NVP slot from snow hydrological calculations. --- src/biogeophys/SnowHydrologyMod.F90 | 37 +++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/src/biogeophys/SnowHydrologyMod.F90 b/src/biogeophys/SnowHydrologyMod.F90 index cbd92eb8f0..65ee11d511 100644 --- a/src/biogeophys/SnowHydrologyMod.F90 +++ b/src/biogeophys/SnowHydrologyMod.F90 @@ -22,7 +22,7 @@ module SnowHydrologyMod use abortutils , only : endrun use column_varcon , only : icol_roof, icol_sunwall, icol_shadewall use clm_varpar , only : nlevsno, nlevsoi, nlevgrnd, nlevmaxurbgrnd - use clm_varctl , only : iulog, use_subgrid_fluxes + use clm_varctl , only : iulog, use_subgrid_fluxes, use_nvp ! [PORTED by Hui Tang: use_nvp for NVP layer guard] use clm_varcon , only : h2osno_max, hfus, denh2o, denice, rpi, spval, tfrz use clm_varcon , only : cpice, cpliq use atm2lndType , only : atm2lnd_type @@ -2218,7 +2218,8 @@ subroutine CombineSnowLayers(bounds, num_snowc, filter_snowc, & do fc = 1, num_snowc c = filter_snowc(fc) l = col%landunit(c) - do j = msn_old(c)+1,0 + ! [PORTED by Hui Tang: stop at j=-1 when NVP at j=0, so snow is never merged into NVP] + do j = msn_old(c)+1, merge(-1, 0, use_nvp .and. col%jbot_sno(c) == -1) ! use 0.01 to avoid runaway ice buildup if (h2osoi_ice_bulk(c,j) <= .01_r8) then if (j < 0 .or. (ltype(l) == istsoil .or. urbpoi(l) .or. ltype(l) == istcrop)) then @@ -2321,6 +2322,8 @@ subroutine CombineSnowLayers(bounds, num_snowc, filter_snowc, & do j = -nlevsno+1,0 do fc = 1, num_snowc c = filter_snowc(fc) + ! [PORTED by Hui Tang: skip NVP layer j=0 — NVP water is not snow water] + if (use_nvp .and. col%jbot_sno(c) == -1 .and. j == 0) cycle if (j >= snl(c)+1) then do wi = water_inst%bulk_and_tracers_beg, water_inst%bulk_and_tracers_end associate(w => water_inst%bulk_and_tracers(wi)) @@ -2372,7 +2375,12 @@ subroutine CombineSnowLayers(bounds, num_snowc, filter_snowc, & end associate end do - snl(c) = 0 + ! [PORTED by Hui Tang: preserve NVP slot in snl when all snow is gone] + if (use_nvp .and. col%jbot_sno(c) == -1) then + snl(c) = -1 + else + snl(c) = 0 + end if h2osno_total(c) = h2osno_no_layers_bulk(c) mss_bcphi(c,:) = 0._r8 @@ -2411,7 +2419,8 @@ subroutine CombineSnowLayers(bounds, num_snowc, filter_snowc, & msn_old(c) = snl(c) mssi(c) = 1 - do i = msn_old(c)+1,0 + ! [PORTED by Hui Tang: stop at i=-1 when NVP at j=0, so NVP is never combined with snow] + do i = msn_old(c)+1, merge(-1, 0, use_nvp .and. col%jbot_sno(c) == -1) if ((frac_sno_eff(c)*dz(c,i) < dzminloc(mssi(c))) .or. & ((h2osoi_ice_bulk(c,i) + h2osoi_liq_bulk(c,i))/(frac_sno_eff(c)*dz(c,i)) < 50._r8)) then if (i == snl(c)+1) then @@ -2685,6 +2694,9 @@ subroutine DivideSnowLayers(bounds, num_snowc, filter_snowc, & c = filter_snowc(fc) msno = abs(snl(c)) + ! [PORTED by Hui Tang: when NVP occupies j=0, snl encodes -(N_snow+1). + ! Exclude the NVP slot from msno so snow compaction stops at j=-1.] + if (use_nvp .and. col%jbot_sno(c) == -1) msno = msno - 1 ! Now traverse layers from top to bottom in a dynamic way, as the total ! number of layers (msno) may increase during the loop. @@ -2838,13 +2850,28 @@ subroutine DivideSnowLayers(bounds, num_snowc, filter_snowc, & k = k+1 end do loop_layers - snl(c) = -msno + ! [PORTED by Hui Tang: restore NVP layer slot in snl after compaction] + ! snl encodes -(N_snow+1) when NVP is active (j=0 reserved for NVP). + if (use_nvp .and. col%jbot_sno(c) == -1) then + snl(c) = -(msno + 1) + else + snl(c) = -msno + end if + + ! [PORTED by Hui Tang: NVP debug print — j=0 water after compaction] + if (use_nvp .and. col%jbot_sno(c) == -1 .and. c == 1) & + write(iulog,'(A,I4,A,I4,A,2ES14.6)') '[NVP DBG] DivSnow END c=',c, & + ' snl=',snl(c),' ice0/liq0=', & + water_inst%bulk_and_tracers(i_bulk)%waterstate_inst%h2osoi_ice_col(c,0), & + water_inst%bulk_and_tracers(i_bulk)%waterstate_inst%h2osoi_liq_col(c,0) end do loop_snowcolumns do j = -nlevsno+1,0 do fc = 1, num_snowc c = filter_snowc(fc) + ! [PORTED by Hui Tang: skip NVP layer j=0 — it is not a snow layer] + if (use_nvp .and. col%jbot_sno(c) == -1 .and. j == 0) cycle if (j >= snl(c)+1) then if (is_lake) then dz(c,j) = dzsno(c,j-snl(c)) From a142abcff5ce4d43558462a6ef5fa224a8834173 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Tue, 12 May 2026 23:35:27 +0300 Subject: [PATCH 051/113] Set nvp evaporation to 0 for its water balance calculation when it is undersnow. --- src/biogeophys/NVPLayerDynamicsMod.F90 | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/biogeophys/NVPLayerDynamicsMod.F90 b/src/biogeophys/NVPLayerDynamicsMod.F90 index e361f9d8e5..6d4dba5906 100644 --- a/src/biogeophys/NVPLayerDynamicsMod.F90 +++ b/src/biogeophys/NVPLayerDynamicsMod.F90 @@ -429,6 +429,8 @@ subroutine NVPWaterBalance_Column(bounds, dtime, waterfluxbulk_inst, waterstate_ real(r8) :: q01 ! Darcy flux NVP→soil (+down, -up) [mm s-1] real(r8) :: h2osoi_net ! h2osoi_liq(c,0) after infl and evap [kg m-2] real(r8) :: satfrac ! NVP effective saturation fraction [-] + ! [PORTED by Hui Tang: effective NVP evap flux — zeroed when NVP is buried under snow] + real(r8) :: ev_nvp_eff ! evap flux applied to NVP water [mm/s]; 0 when buried associate( & qflx_rain_plus_snomelt => waterfluxbulk_inst%qflx_rain_plus_snomelt_col, & @@ -508,9 +510,21 @@ subroutine NVPWaterBalance_Column(bounds, dtime, waterfluxbulk_inst, waterstate_ ! Darcy flux: q = K * (grad_psi + gravity), positive = downward q01 = K_interface * ((psi_nvp - smp_soil1) / dz_iface_mm + 1.0_r8) + ! --- Atmosphere evap/condensation: only applies when NVP is exposed --- + ! [PORTED by Hui Tang: qflx_ev_nvp_col is computed from qg_nvp=qg_soil even when + ! NVP is buried under snow (frac_sno_eff=1 → frac_nvp_eff=0 → qg unchanged). + ! This flux is NOT included in qflx_evap_tot_col (which tracks only the blended + ! qg-based flux), so applying it to h2osoi_liq(c,0) creates an untracked water + ! source that breaks errh2o. When snow covers NVP (snl < -1), zero the flux.] + if (col%snl(c) < -1) then + ev_nvp_eff = 0._r8 + else + ev_nvp_eff = qflx_ev_nvp_col(c) + end if + ! --- Update h2osoi_liq(c,0): add infl, subtract evap; cannot go negative --- h2osoi_net = h2osoi_liq(c,0) & - + (qflx_nvp_infl_col(c) - qflx_ev_nvp_col(c)) * dtime ! [kg m-2] + + (qflx_nvp_infl_col(c) - ev_nvp_eff) * dtime ! [kg m-2] h2osoi_net = max(0._r8, h2osoi_net) if (q01 >= 0._r8) then From 4ec436d14704673793db4f39559e534667aba515 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Tue, 12 May 2026 23:42:19 +0300 Subject: [PATCH 052/113] Avoid NVP evap flux calculation when buried undersnow. --- src/biogeophys/CanopyFluxesMod.F90 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/biogeophys/CanopyFluxesMod.F90 b/src/biogeophys/CanopyFluxesMod.F90 index 3a328d23f1..7c146b3475 100644 --- a/src/biogeophys/CanopyFluxesMod.F90 +++ b/src/biogeophys/CanopyFluxesMod.F90 @@ -1563,7 +1563,8 @@ subroutine CanopyFluxes(bounds, num_exposedvegp, filter_exposedvegp, ! [PORTED by Hui Tang: NVP individual latent heat flux, analogous to snow/h2osfc] ! qflx_evap_soi already includes NVP because qg(c) blends NVP in SurfaceHumidityMod. ! This is the diagnostic breakdown of the NVP contribution. - if (use_nvp .and. col%frac_nvp(c) > 0._r8) then + ! Zero when NVP is buried under snow (snl < -1) — same guard as BareGroundFluxesMod. + if (use_nvp .and. col%frac_nvp(c) > 0._r8 .and. col%snl(c) >= -1) then delq_nvp = wtalq(p)*qg_nvp(c)-wtlq0(p)*qsatl(p)-wtaq0(p)*forc_q(c) qflx_ev_nvp(p) = forc_rho(c)*wtgq(p)*delq_nvp else From 9670a662d665ed6ae84c3bea3d521ddfef799483 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Tue, 12 May 2026 23:47:53 +0300 Subject: [PATCH 053/113] Avoid NVP evap and heat flux calculation when buried undersnow (bareground case). --- src/biogeophys/BareGroundFluxesMod.F90 | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/biogeophys/BareGroundFluxesMod.F90 b/src/biogeophys/BareGroundFluxesMod.F90 index 5d0e8cb5ab..6e8c5964bc 100644 --- a/src/biogeophys/BareGroundFluxesMod.F90 +++ b/src/biogeophys/BareGroundFluxesMod.F90 @@ -83,7 +83,7 @@ subroutine BareGroundFluxes(bounds, num_noexposedvegp, filter_noexposedvegp, & use clm_varpar , only : nlevgrnd use clm_varcon , only : cpair, vkc, grav, denice, denh2o, tfrz use clm_varcon , only : beta_param, nu_param, meier_param3 - use clm_varctl , only : use_lch4, z0param_method, use_nvp + use clm_varctl , only : use_lch4, z0param_method, use_nvp, iulog use landunit_varcon , only : istsoil, istcrop use QSatMod , only : QSat use SurfaceResistanceMod , only : do_soilevap_beta,do_soil_resistance_sl14 @@ -487,7 +487,9 @@ subroutine BareGroundFluxes(bounds, num_noexposedvegp, filter_noexposedvegp, & eflx_sh_soil(p) = -raih*(thm(p)-t_soisno(c,1)) eflx_sh_h2osfc(p) = -raih*(thm(p)-t_h2osfc(c)) ! [PORTED by Hui Tang: NVP sensible heat flux for bare ground, analogous to snow/h2osfc] - if (use_nvp .and. col%frac_nvp(c) > 0._r8) then + ! Zero when NVP is buried under snow (snl < -1): the snow surface controls the energy balance. + ! Only compute for the NVP veg patch (patch%is_veg), not the bareground gap patch. + if (use_nvp .and. patch%is_veg(p) .and. col%frac_nvp(c) > 0._r8 .and. col%snl(c) >= -1) then eflx_sh_nvp(p) = -raih*(thm(p)-t_nvp_col(c)) else eflx_sh_nvp(p) = 0._r8 @@ -499,12 +501,18 @@ subroutine BareGroundFluxes(bounds, num_noexposedvegp, filter_noexposedvegp, & qflx_evap_soi(p) = -raiw*dqh(p) qflx_evap_tot(p) = qflx_evap_soi(p) + print *, "qflx_evap_tot=", qflx_evap_soi(p), raiw, dqh(p) + ! compute latent heat fluxes individually qflx_ev_snow(p) = -raiw*(forc_q(c) - qg_snow(c)) qflx_ev_soil(p) = -raiw*(forc_q(c) - qg_soil(c)) qflx_ev_h2osfc(p) = -raiw*(forc_q(c) - qg_h2osfc(c)) ! [PORTED by Hui Tang: NVP evaporation flux for bare ground, analogous to snow/h2osfc] - if (use_nvp .and. col%frac_nvp(c) > 0._r8) then + ! Zero when NVP is buried under snow (snl < -1): qflx_ev_nvp_col drives ev_nvp_eff in + ! NVPWaterBalance_Column; a non-zero value here when NVP is covered would add water to + ! qflx_evap_tot_col without removing it from any tracked water store, causing errh2o. + ! Only compute for the NVP veg patch (patch%is_veg), not the bareground gap patch. + if (use_nvp .and. patch%is_veg(p) .and. col%frac_nvp(c) > 0._r8 .and. col%snl(c) >= -1) then qflx_ev_nvp(p) = -raiw*(forc_q(c) - qg_nvp(c)) else qflx_ev_nvp(p) = 0._r8 From 5c42e3ec57997b80bb0b066bdd323360bc388869 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Tue, 12 May 2026 23:55:33 +0300 Subject: [PATCH 054/113] =?UTF-8?q?Key=20NVP=20bug=20fix:=20Set=20NVP=20ar?= =?UTF-8?q?ea=20fraction=20=E2=80=94=20zero=20when=20NVP=20buried=20under?= =?UTF-8?q?=20snow=20for=20soil=20hydrology.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/biogeophys/SoilHydrologyMod.F90 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/biogeophys/SoilHydrologyMod.F90 b/src/biogeophys/SoilHydrologyMod.F90 index 401c6f5169..0cb311c363 100644 --- a/src/biogeophys/SoilHydrologyMod.F90 +++ b/src/biogeophys/SoilHydrologyMod.F90 @@ -356,9 +356,9 @@ subroutine SetQflxInputs(bounds, num_hydrologyc, filter_hydrologyc, & qflx_evap=qflx_ev_soil(c) endif - ! [PORTED by Hui Tang: compute effective NVP area (not blocked by surface water, - ! but snow does not block — water percolates through to NVP layer beneath snow)] - if (use_nvp) then + ! [PORTED by Hui Tang: NVP area fraction — zero when NVP buried under snow (snl < -1); + ! buried NVP receives no direct surface water and NVPWaterBalance returns zero fluxes] + if (use_nvp .and. snl(c) >= -1) then frac_nvp_eff = min(col%frac_nvp(c), max(0._r8, 1._r8 - frac_h2osfc(c))) else frac_nvp_eff = 0._r8 From ad9c5fd253966c3a1a386249dc1938f0481bad17 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Tue, 12 May 2026 23:59:09 +0300 Subject: [PATCH 055/113] Write NVP debugging output to log files. --- src/biogeophys/BalanceCheckMod.F90 | 11 +++- src/biogeophys/BareGroundFluxesMod.F90 | 4 ++ src/biogeophys/HydrologyNoDrainageMod.F90 | 62 +++++++++++++++++++++-- src/biogeophys/NVPLayerDynamicsMod.F90 | 2 + src/biogeophys/SnowHydrologyMod.F90 | 7 +++ src/biogeophys/SoilFluxesMod.F90 | 2 + src/biogeophys/SoilHydrologyMod.F90 | 22 ++++++-- src/biogeophys/SoilTemperatureMod.F90 | 53 +++++++++++++------ src/biogeophys/SoilWaterMovementMod.F90 | 33 +++++++++++- src/biogeophys/TotalWaterAndHeatMod.F90 | 40 +++++++++++++-- src/dyn_subgrid/dynEDMod.F90 | 5 +- src/main/clm_driver.F90 | 15 ++++++ src/utils/clmfates_interfaceMod.F90 | 7 +++ 13 files changed, 233 insertions(+), 30 deletions(-) diff --git a/src/biogeophys/BalanceCheckMod.F90 b/src/biogeophys/BalanceCheckMod.F90 index b79fcee46e..cf9e6740ad 100644 --- a/src/biogeophys/BalanceCheckMod.F90 +++ b/src/biogeophys/BalanceCheckMod.F90 @@ -461,7 +461,7 @@ subroutine BalanceCheck( bounds, & ! ! !USES: use clm_varcon , only : spval - use clm_varctl , only : use_soil_moisture_streams + use clm_varctl , only : use_soil_moisture_streams, use_nvp ! [PORTED by Hui Tang: use_nvp for NVP debug print] use clm_time_manager , only : get_step_size_real, get_nstep use clm_time_manager , only : get_nstep_since_startup_or_lastDA_restart_or_pause use CanopyStateType , only : canopystate_type @@ -589,6 +589,15 @@ subroutine BalanceCheck( bounds, & ! add qflx_drain_perched and qflx_flood if (col%active(c)) then + ! [PORTED by Hui Tang: NVP debug — print j=0 water and all wb flux terms for c==1] + if (use_nvp .and. col%jbot_sno(c) == -1 .and. c == 1) then + write(iulog,*) '[NVP DBG] WBal c=',c,' snl=',col%snl(c), & + ' ice0=',waterstate_inst%h2osoi_ice_col(c,0), & + ' liq0=',waterstate_inst%h2osoi_liq_col(c,0), & + ' snwcp_liq=', qflx_snwcp_liq(c)*dtime, & + ' snwcp_ice=', qflx_snwcp_ice(c)*dtime + end if + errh2o_col(c) = endwb_col(c) - begwb_col(c) & - (forc_rain_col(c) & + forc_snow_col(c) & diff --git a/src/biogeophys/BareGroundFluxesMod.F90 b/src/biogeophys/BareGroundFluxesMod.F90 index 6e8c5964bc..bce0b9a8b6 100644 --- a/src/biogeophys/BareGroundFluxesMod.F90 +++ b/src/biogeophys/BareGroundFluxesMod.F90 @@ -302,6 +302,10 @@ subroutine BareGroundFluxes(bounds, num_noexposedvegp, filter_noexposedvegp, & do f = 1, num_noexposedvegp p = filter_noexposedvegp(f) c = patch%column(p) + if (use_nvp .and. col%nvp_layer_active(c)) then + write(iulog,'(a,2i6,2l2,2f10.5)') '[DBG noexposedvegp] p, c, is_bg, is_veg, wtcol, frac_nvp:', & + p, c, patch%is_bareground(p), patch%is_veg(p), patch%wtcol(p), col%frac_nvp(c) + end if btran(p) = 0._r8 t_veg(p) = forc_t(c) cf_bare = forc_pbot(c)/(SHR_CONST_RGAS*0.001_r8*thm(p))*1.e06_r8 diff --git a/src/biogeophys/HydrologyNoDrainageMod.F90 b/src/biogeophys/HydrologyNoDrainageMod.F90 index 48c3fba9e1..ed9d6dc832 100644 --- a/src/biogeophys/HydrologyNoDrainageMod.F90 +++ b/src/biogeophys/HydrologyNoDrainageMod.F90 @@ -283,9 +283,19 @@ subroutine HydrologyNoDrainage(bounds, & ! Determine the change of snow mass and the snow water onto soil + ! [PORTED by Hui Tang: NVP debug — j=0 state entering HydrologyNoDrainage] + if (use_nvp .and. col%jbot_sno(bounds%begc) == -1) & + write(iulog,*) '[NVP DBG] HydroNoDrain BEG c=1 snl=', col%snl(bounds%begc), & + ' ice0=', h2osoi_ice(bounds%begc,0), ' liq0=', h2osoi_liq(bounds%begc,0) + call SnowWater(bounds, num_snowc, filter_snowc, num_nosnowc, filter_nosnowc, & atm2lnd_inst, aerosol_inst, water_inst) + ! [PORTED by Hui Tang: NVP debug — j=0 state after SnowWater] + if (use_nvp .and. col%jbot_sno(bounds%begc) == -1) & + write(iulog,*) '[NVP DBG] after SnowWater c=1 snl=', col%snl(bounds%begc), & + ' ice0=', h2osoi_ice(bounds%begc,0), ' liq0=', h2osoi_liq(bounds%begc,0) + ! TODO(wjs, 2019-08-30) Eventually move this down, merging this with later tracer ! consistency checks. If/when we remove calls to TracerConsistencyCheck from this ! module, remember to also remove 'use perf_mod' at the top. @@ -320,6 +330,14 @@ subroutine HydrologyNoDrainage(bounds, & b_waterstate_inst, b_waterdiagnostic_inst, soilstate_inst, temperature_inst) end if + ! [PORTED by Hui Tang: NVP debug — j=0 state and NVP fluxes after NVPWaterBalance_Column] + if (use_nvp .and. col%jbot_sno(bounds%begc) == -1) & + write(iulog,*) '[NVP DBG] after NVPWaterBal c=1 snl=', col%snl(bounds%begc), & + ' ice0=', h2osoi_ice(bounds%begc,0), ' liq0=', h2osoi_liq(bounds%begc,0), & + ' ev_nvp=', b_waterflux_inst%qflx_ev_nvp_col(bounds%begc), & + ' nvp_drain=', b_waterflux_inst%qflx_nvp_drain_col(bounds%begc), & + ' nvp_infl=', b_waterflux_inst%qflx_nvp_infl_col(bounds%begc) + call SetQflxInputs(bounds, num_hydrologyc, filter_hydrologyc, & b_waterflux_inst, b_waterdiagnostic_inst) @@ -353,10 +371,22 @@ subroutine HydrologyNoDrainage(bounds, & if ( use_fates ) then call clm_fates%ComputeRootSoilFlux(bounds, num_hydrologyc, filter_hydrologyc, soilstate_inst, b_waterflux_inst) end if - + + ! [NVP DBG: print soil liq/ice/T for j=1..6 before SoilWater; nstep<=3 to avoid log flood] + if (use_nvp .and. get_nstep() <= 3) then + write(iulog,'(a,i0,6(1x,es11.4))') '[NVP DBG] before SoilWater nstep=', get_nstep(), & + (h2osoi_liq(bounds%begc,j), j=1,6) + write(iulog,'(a,6(1x,f7.2))') '[NVP DBG] before SoilWater t_soisno(1:6)=', & + (t_soisno(bounds%begc,j), j=1,6) + end if call SoilWater(bounds, num_hydrologyc, filter_hydrologyc, num_urbanc, filter_urbanc, & soilhydrology_inst, soilstate_inst, b_waterflux_inst, b_waterstate_inst, temperature_inst, & canopystate_inst, energyflux_inst, soil_water_retention_curve) + ! [NVP DBG: print soil liq after SoilWater to see if it introduces the liquid] + if (use_nvp .and. get_nstep() <= 3) then + write(iulog,'(a,i0,6(1x,es11.4))') '[NVP DBG] after SoilWater nstep=', get_nstep(), & + (h2osoi_liq(bounds%begc,j), j=1,6) + end if if (use_vichydro) then ! mapping soilmoist from CLM to VIC layers for runoff calculations @@ -368,15 +398,26 @@ subroutine HydrologyNoDrainage(bounds, & call WaterTable(bounds, num_hydrologyc, filter_hydrologyc, & soilhydrology_inst, soilstate_inst, temperature_inst, b_waterstate_inst, & b_waterflux_inst) + ! [NVP DBG: print soil liq after WaterTable] + if (use_nvp .and. get_nstep() <= 3) then + write(iulog,'(a,i0,6(1x,es11.4))') '[NVP DBG] after WaterTable nstep=', get_nstep(), & + (h2osoi_liq(bounds%begc,j), j=1,6) + end if else call PerchedWaterTable(bounds, num_hydrologyc, filter_hydrologyc, & num_urbanc, filter_urbanc, soilhydrology_inst, soilstate_inst, & - temperature_inst, b_waterstate_inst, b_waterflux_inst) + temperature_inst, b_waterstate_inst, b_waterflux_inst) call ThetaBasedWaterTable(bounds, num_hydrologyc, filter_hydrologyc, & num_urbanc, filter_urbanc, soilhydrology_inst, soilstate_inst, & - b_waterstate_inst, b_waterflux_inst) + b_waterstate_inst, b_waterflux_inst) + + ! [NVP DBG: print soil liq after PerchedWaterTable+ThetaBasedWaterTable] + if (use_nvp .and. get_nstep() <= 3) then + write(iulog,'(a,i0,6(1x,es11.4))') '[NVP DBG] after WaterTable(perched) nstep=', get_nstep(), & + (h2osoi_liq(bounds%begc,j), j=1,6) + end if end if @@ -385,6 +426,11 @@ subroutine HydrologyNoDrainage(bounds, & soilhydrology_inst, soilstate_inst, & b_waterstate_inst, b_waterdiagnostic_inst, b_waterflux_inst) + ! [PORTED by Hui Tang: NVP debug — j=0 state after RenewCondensation] + if (use_nvp .and. col%jbot_sno(bounds%begc) == -1) & + write(iulog,*) '[NVP DBG] after RenewCond c=1 snl=', col%snl(bounds%begc), & + ' ice0=', h2osoi_ice(bounds%begc,0), ' liq0=', h2osoi_liq(bounds%begc,0) + ! BUG(wjs, 2019-09-16, ESCOMP/ctsm#762) This is needed so that we can test the ! tracerization of the following snow stuff without having tracerized everything ! before this point. Remove this block once code before this point is fully @@ -403,10 +449,20 @@ subroutine HydrologyNoDrainage(bounds, & scf_method, & temperature_inst, b_waterstate_inst, b_waterdiagnostic_inst, atm2lnd_inst) + ! [PORTED by Hui Tang: NVP debug — j=0 state after SnowCompaction] + if (use_nvp .and. col%jbot_sno(bounds%begc) == -1) & + write(iulog,*) '[NVP DBG] after SnowCompact c=1 snl=', col%snl(bounds%begc), & + ' ice0=', h2osoi_ice(bounds%begc,0), ' liq0=', h2osoi_liq(bounds%begc,0) + ! Combine thin snow elements call CombineSnowLayers(bounds, num_snowc, filter_snowc, & aerosol_inst, temperature_inst, water_inst) + ! [PORTED by Hui Tang: NVP debug — j=0 state after CombineSnowLayers] + if (use_nvp .and. col%jbot_sno(bounds%begc) == -1) & + write(iulog,*) '[NVP DBG] after CombineSnow c=1 snl=', col%snl(bounds%begc), & + ' ice0=', h2osoi_ice(bounds%begc,0), ' liq0=', h2osoi_liq(bounds%begc,0) + ! Divide thick snow elements call DivideSnowLayers(bounds, num_snowc, filter_snowc, & aerosol_inst, temperature_inst, water_inst, is_lake=.false.) diff --git a/src/biogeophys/NVPLayerDynamicsMod.F90 b/src/biogeophys/NVPLayerDynamicsMod.F90 index 6d4dba5906..0379473d66 100644 --- a/src/biogeophys/NVPLayerDynamicsMod.F90 +++ b/src/biogeophys/NVPLayerDynamicsMod.F90 @@ -462,6 +462,8 @@ subroutine NVPWaterBalance_Column(bounds, dtime, waterfluxbulk_inst, waterstate_ ! --- Water input to NVP from precipitation / snowmelt --- qflx_nvp_infl_col(c) = frac_nvp_eff * qflx_rain_plus_snomelt(c) ! [mm/s] + print *, "qflx_nvp_infl_col=", frac_nvp_eff, frac_h2osfc, qflx_rain_plus_snomelt(c),qflx_nvp_infl_col + ! --- NVP volumetric water content (clamped to valid range) --- ! [PORTED by Hui Tang: initialise eff_porosity and vol_ice on every branch — they are diff --git a/src/biogeophys/SnowHydrologyMod.F90 b/src/biogeophys/SnowHydrologyMod.F90 index 65ee11d511..18784c0307 100644 --- a/src/biogeophys/SnowHydrologyMod.F90 +++ b/src/biogeophys/SnowHydrologyMod.F90 @@ -2693,6 +2693,13 @@ subroutine DivideSnowLayers(bounds, num_snowc, filter_snowc, & loop_snowcolumns: do fc = 1, num_snowc c = filter_snowc(fc) + ! [PORTED by Hui Tang: NVP debug print — j=0 water before compaction] + if (use_nvp .and. col%jbot_sno(c) == -1 .and. c == 1) & + write(iulog,'(A,I4,A,I4,A,2ES14.6)') '[NVP DBG] DivSnow BEG c=',c, & + ' snl=',snl(c),' ice0/liq0=', & + water_inst%bulk_and_tracers(i_bulk)%waterstate_inst%h2osoi_ice_col(c,0), & + water_inst%bulk_and_tracers(i_bulk)%waterstate_inst%h2osoi_liq_col(c,0) + msno = abs(snl(c)) ! [PORTED by Hui Tang: when NVP occupies j=0, snl encodes -(N_snow+1). ! Exclude the NVP slot from msno so snow compaction stops at j=-1.] diff --git a/src/biogeophys/SoilFluxesMod.F90 b/src/biogeophys/SoilFluxesMod.F90 index d219ca4e35..a1ba7a6f73 100644 --- a/src/biogeophys/SoilFluxesMod.F90 +++ b/src/biogeophys/SoilFluxesMod.F90 @@ -402,6 +402,8 @@ subroutine SoilFluxes (bounds, num_urbanl, filter_urbanl, & eflx_sh_tot(p) = eflx_sh_veg(p) + eflx_sh_grnd(p) if (.not. lun%urbpoi(l)) eflx_sh_tot(p) = eflx_sh_tot(p) + eflx_sh_stem(p) + + print *, "qflx_evap_veg=", qflx_evap_veg(p), qflx_evap_soi(p) qflx_evap_tot(p) = qflx_evap_veg(p) + qflx_evap_soi(p) eflx_lh_tot(p)= hvap*qflx_evap_veg(p) + htvp(c)*qflx_evap_soi(p) diff --git a/src/biogeophys/SoilHydrologyMod.F90 b/src/biogeophys/SoilHydrologyMod.F90 index 0cb311c363..f9d58c707a 100644 --- a/src/biogeophys/SoilHydrologyMod.F90 +++ b/src/biogeophys/SoilHydrologyMod.F90 @@ -2057,13 +2057,13 @@ subroutine SubsurfaceLateralFlow(bounds, & ! Calculate subsurface drainage ! ! !USES: - use clm_time_manager , only : get_step_size + use clm_time_manager , only : get_step_size, get_nstep ! [NVP DBG] use clm_varpar , only : nlevsoi, nlevgrnd, nlayer, nlayert - use clm_varctl , only : nhillslope + use clm_varctl , only : nhillslope, iulog, use_nvp ! [NVP DBG] use clm_varcon , only : pondmx, watmin,rpi, secspday use column_varcon , only : icol_road_perv use abortutils , only : endrun - use GridcellType , only : grc + use GridcellType , only : grc use landunit_varcon , only : istsoil, istcrop use clm_varctl , only : use_hillslope_routing @@ -2505,6 +2505,12 @@ subroutine SubsurfaceLateralFlow(bounds, & qflx_ice_runoff_xs(c) = xs1(c) / dtime end do + ! [NVP DBG: print soil liq j=1..6 before watmin floor in SubsurfaceLateralFlow] + if (use_nvp .and. get_nstep() <= 3) then + write(iulog,'(a,i0,6(1x,es11.4))') '[NVP DBG] SubLatFlow before watmin nstep=', get_nstep(), & + (h2osoi_liq(bounds%begc,j), j=1,6) + end if + ! Limit h2osoi_liq to be greater than or equal to watmin. ! Get water needed to bring h2osoi_liq equal watmin from lower layer. ! If insufficient water in soil layers, get from aquifer water @@ -2515,7 +2521,7 @@ subroutine SubsurfaceLateralFlow(bounds, & if (h2osoi_liq(c,j) < watmin) then xs(c) = watmin - h2osoi_liq(c,j) ! deepen water table if water is passed from below zwt layer - if(j == jwt(c)) then + if(j == jwt(c)) then zwt(c) = zwt(c) + xs(c)/eff_porosity(c,j)/1000._r8 endif else @@ -2551,12 +2557,18 @@ subroutine SubsurfaceLateralFlow(bounds, & ! Needed in case there is no water to be found h2osoi_liq(c,j) = h2osoi_liq(c,j) + xs(c) ! Instead of removing water from aquifer where it eventually - ! shows up as excess drainage to the ocean, take it back out of + ! shows up as excess drainage to the ocean, take it back out of ! drainage qflx_rsub_sat(c) = qflx_rsub_sat(c) - xs(c)/dtime end do + ! [NVP DBG: print soil liq j=1..6 after watmin floor — confirms this is the 0.01/layer source] + if (use_nvp .and. get_nstep() <= 3) then + write(iulog,'(a,i0,6(1x,es11.4))') '[NVP DBG] SubLatFlow after watmin nstep=', get_nstep(), & + (h2osoi_liq(bounds%begc,j), j=1,6) + end if + do fc = 1, num_hydrologyc c = filter_hydrologyc(fc) diff --git a/src/biogeophys/SoilTemperatureMod.F90 b/src/biogeophys/SoilTemperatureMod.F90 index 476585db32..b002426dd5 100644 --- a/src/biogeophys/SoilTemperatureMod.F90 +++ b/src/biogeophys/SoilTemperatureMod.F90 @@ -380,6 +380,13 @@ subroutine SoilTemperature(bounds, num_urbanl, filter_urbanl, num_urbanc, filter ! Set up right-hand side vecor (vector r). + print *, "hs_h2osfc=", hs_h2osfc + print *, "c_h2osfc=", c_h2osfc + print *, "h2osfc=", h2osfc + print *, "frac_h2osfc=", frac_h2osfc + print *, "thin_sfclayer=", thin_sfclayer + + call SetRHSVec(bounds, num_nolakec, filter_nolakec, & dtime, & hs_h2osfc( begc:endc ), & @@ -439,7 +446,9 @@ subroutine SoilTemperature(bounds, num_urbanl, filter_urbanl, num_urbanc, filter call t_startf( 'SoilTempBandDiag') ! Solve the system - print*, "tvector_before=", tvector + print*, "bmatrix=", bmatrix(begc:endc, :, :) + print*, "rvector=", rvector(begc:endc, :) + print*, "tvector=", tvector(begc:endc, :) print*, "nvp_layer_active=", col%nvp_layer_active, snl call BandDiagonal(bounds, -nlevsno, nlevmaxurbgrnd, jtop(begc:endc), jbot(begc:endc), & @@ -771,6 +780,20 @@ subroutine SoilThermProp (bounds, num_urbanc, filter_urbanc, num_nolakec, filter thk => soilstate_inst%thk_col & ! Output: [real(r8) (:,:) ] thermal conductivity of each layer [W/m-K] ) + ! [NVP DBG: NaN check for NVP layer j=0 entering SoilThermProp — only fires on actual NaN] + if (use_nvp) then + do fc = 1, num_nolakec + c = filter_nolakec(fc) + if (snl(c) < 0) then + if (h2osoi_liq(c,0) /= h2osoi_liq(c,0) .or. h2osoi_ice(c,0) /= h2osoi_ice(c,0)) then + write(iulog,*) '[NVP DBG] SoilThermProp NaN in h2osoi at j=0 c=', c, ' snl=', snl(c) + write(iulog,*) ' h2osoi_liq(c,:) = ', h2osoi_liq(c,:) + write(iulog,*) ' h2osoi_ice(c,:) = ', h2osoi_ice(c,:) + end if + end if + end do + end if + ! Thermal conductivity of soil from Farouki (1981) do j = -nlevsno+1,nlevgrnd @@ -2394,12 +2417,12 @@ subroutine SetRHSVec_StandingSurfaceWater(bounds, num_nolakec, filter_nolakec, d fn_h2osfc(c) = 0.0_r8 rt(c,1) = t_h2osfc(c) else - ! surface water layer has two coefficients - dzm=(0.5*dz_h2osfc(c)+col%z(c,1)) + ! surface water layer has two coefficients + dzm=(0.5*dz_h2osfc(c)+col%z(c,1)) - fn_h2osfc(c)=tk_h2osfc(c)*(t_soisno(c,1)-t_h2osfc(c))/dzm - rt(c,1)= t_h2osfc(c) + (dtime/c_h2osfc(c)) & - *( hs_h2osfc(c) - dhsdT(c)*t_h2osfc(c) + cnfac*fn_h2osfc(c) )!rhs for h2osfc + fn_h2osfc(c)=tk_h2osfc(c)*(t_soisno(c,1)-t_h2osfc(c))/dzm + rt(c,1)= t_h2osfc(c) + (dtime/c_h2osfc(c)) & + *( hs_h2osfc(c) - dhsdT(c)*t_h2osfc(c) + cnfac*fn_h2osfc(c) )!rhs for h2osfc end if enddo @@ -3115,18 +3138,18 @@ subroutine SetMatrix_StandingSurfaceWater(bounds, num_nolakec, filter_nolakec, d if (c_h2osfc(c) <= thin_sfclayer) then bmatrix_ssw(c,3,0) = 1.0_r8 ! diagonal = 1; off-diagonals already 0 from init else - ! surface water layer has two coefficients - dzm=(0.5*dz_h2osfc(c)+col%z(c,1)) + ! surface water layer has two coefficients + dzm=(0.5*dz_h2osfc(c)+col%z(c,1)) - bmatrix_ssw(c,3,0)= 1._r8+(1._r8-cnfac)*(dtime/c_h2osfc(c)) & - *tk_h2osfc(c)/dzm -(dtime/c_h2osfc(c))*dhsdT(c) !interaction from atm + bmatrix_ssw(c,3,0)= 1._r8+(1._r8-cnfac)*(dtime/c_h2osfc(c)) & + *tk_h2osfc(c)/dzm -(dtime/c_h2osfc(c))*dhsdT(c) !interaction from atm - bmatrix_ssw_soil(c,2,0)= -(1._r8-cnfac)*(dtime/c_h2osfc(c))*tk_h2osfc(c)/dzm !flux to top soil layer + bmatrix_ssw_soil(c,2,0)= -(1._r8-cnfac)*(dtime/c_h2osfc(c))*tk_h2osfc(c)/dzm !flux to top soil layer - ! top soil layer has sub coef shifted to 2nd super diagonal - if ( frac_h2osfc(c) /= 0.0_r8 )then - bmatrix_soil_ssw(c,4,1)= - frac_h2osfc(c) * (1._r8-cnfac) * fact(c,1) & - * tk_h2osfc(c)/dzm !flux from h2osfc + ! top soil layer has sub coef shifted to 2nd super diagonal + if ( frac_h2osfc(c) /= 0.0_r8 )then + bmatrix_soil_ssw(c,4,1)= - frac_h2osfc(c) * (1._r8-cnfac) * fact(c,1) & + * tk_h2osfc(c)/dzm !flux from h2osfc end if end if enddo diff --git a/src/biogeophys/SoilWaterMovementMod.F90 b/src/biogeophys/SoilWaterMovementMod.F90 index c2909d0475..9daeb57f24 100644 --- a/src/biogeophys/SoilWaterMovementMod.F90 +++ b/src/biogeophys/SoilWaterMovementMod.F90 @@ -261,7 +261,8 @@ subroutine SoilWater(bounds, num_hydrologyc, filter_hydrologyc, & use ColumnType , only : col use SoilWaterRetentionCurveMod, only : soil_water_retention_curve_type use clm_varcon , only : denh2o, denice - use clm_varctl, only : use_flexibleCN + use clm_varctl, only : use_flexibleCN, iulog + use clm_time_manager, only : get_nstep ! [NVP DBG] ! ! !ARGUMENTS: type(bounds_type) , intent(in) :: bounds ! bounds @@ -294,6 +295,15 @@ subroutine SoilWater(bounds, num_hydrologyc, filter_hydrologyc, & h2osoi_liq => waterstatebulk_inst%h2osoi_liq_col & ! Output: [real(r8) (:,:) ] liquid water (kg/m2) ) + + ! [NVP DBG: print qflx_infl and first 6 soil liq layers entering solver, nstep<=3 only] + if (get_nstep() <= 3) then + write(iulog,'(a,i0,a,es11.4)') '[NVP DBG] SoilWater entry nstep=', get_nstep(), & + ' qflx_infl=', waterfluxbulk_inst%qflx_infl_col(bounds%begc) + write(iulog,'(a,i0,6(1x,es11.4))') '[NVP DBG] SoilWater entry liq(1:6) nstep=', get_nstep(), & + (h2osoi_liq(bounds%begc,j), j=1,6) + end if + select case(soilwater_movement_method) case (zengdecker_2009) @@ -327,6 +337,11 @@ subroutine SoilWater(bounds, num_hydrologyc, filter_hydrologyc, & call endrun(subname // ':: a SoilWater implementation must be specified!') end select + + ! [NVP DBG: print first 6 soil liq layers after solver] + if (get_nstep() <= 3) & + write(iulog,'(a,i0,6(1x,es11.4))') '[NVP DBG] SoilWater exit nstep=', get_nstep(), & + (h2osoi_liq(bounds%begc,j), j=1,6) if (use_flexibleCN) then !a work around of the negative liquid water. Jinyun Tang, Jan 14, 2015 @@ -1232,6 +1247,12 @@ subroutine soilwater_moisture_form(bounds, num_hydrologyc, & dqodw2(c,1:nlayers)) ! RHS of system of equations + print *, "qflx_rootsoi_col=", qflx_rootsoi_col + print *, "vwc_liq=", vwc_liq + print *, "qin=", qin + print *, "qout=", qout + print *, "dt_dz=", dt_dz + call compute_RHS_moisture_form(c, nlayers, & qflx_rootsoi_col(c,1:nlayers), & vwc_liq(c,1:nlayers), & @@ -1282,7 +1303,12 @@ subroutine soilwater_moisture_form(bounds, num_hydrologyc, & ! get a copy of the residual vector rhs(1:nlayers) = rmx(filter_hydrologyc(fc),1:nlayers) - + + print *, "rhs0=", rhs(1:nlayers) + print *, "dlow0=", dlow + print *, "diag0=", diag + print *, "dUpp0=", dUpp + ! call the lapack tri-diagonal solver call dgtsv(nlayers, & ! intent(in): [integer] number of state variables 1, & ! intent(in): [integer] number of columns of the matrix B @@ -1296,6 +1322,8 @@ subroutine soilwater_moisture_form(bounds, num_hydrologyc, & msg = subname // ':: problem with the lapack solver') ! save the iteration increment + print *, "rhs1=", rhs(1:nlayers) + print *, "dwat=", dwat dwat(filter_hydrologyc(fc),1:nlayers) = rhs(1:nlayers) endif ! solution method for the tridiagonal solution @@ -1357,6 +1385,7 @@ subroutine soilwater_moisture_form(bounds, num_hydrologyc, & ! ********** ! Renew the mass of liquid water + do j = 1, nlayers h2osoi_liq(c,j) = h2osoi_liq(c,j) + dwat(c,j) * (m_to_mm * dz(c,j)) end do diff --git a/src/biogeophys/TotalWaterAndHeatMod.F90 b/src/biogeophys/TotalWaterAndHeatMod.F90 index 885222f33b..9bcee5b33a 100644 --- a/src/biogeophys/TotalWaterAndHeatMod.F90 +++ b/src/biogeophys/TotalWaterAndHeatMod.F90 @@ -25,6 +25,7 @@ module TotalWaterAndHeatMod use column_varcon , only : icol_roof, icol_sunwall, icol_shadewall use column_varcon , only : icol_road_perv, icol_road_imperv use landunit_varcon , only : istdlak, istsoil,istcrop,istwet,istice + use clm_varctl , only : iulog, use_nvp ! [PORTED by Hui Tang: use_nvp for NVP debug prints] ! ! !PUBLIC TYPES: implicit none @@ -266,6 +267,8 @@ subroutine ComputeLiqIceMassNonLake(bounds, num_nolakec, filter_nolakec, & snocan_patch(bounds%begp:bounds%endp), & snocan_col(bounds%begc:bounds%endc)) + write(iulog,*) '[NVP DBG] snocan_patch=', snocan_patch + do fc = 1, num_nolakec c = filter_nolakec(fc) @@ -275,14 +278,33 @@ subroutine ComputeLiqIceMassNonLake(bounds, num_nolakec, filter_nolakec, & ! where FATES hydraulics is not turned on, this total_plant_stored_h2o is ! non-changing, and is set to 0 for a trivial solution. + write(iulog,*) '[NVP DBG] ComputeLiqIceMass c=',c,' j=',j, & + ' cum_liq=',liquid_mass(c),' cum_ice=',ice_mass(c), & + ' total_plant_stored_h2o=', total_plant_stored_h2o(c) + liquid_mass(c) = liquid_mass(c) + liqcan_col(c) + total_plant_stored_h2o(c) ice_mass(c) = ice_mass(c) + snocan_col(c) - ice_mass(c) = ice_mass(c) + h2osno_no_layers(c) - do j = snl(c)+1,0 + + + + ! [PORTED by Hui Tang: when NVP occupies j=0, stop at j=-1 so NVP water is not + ! counted as snow mass. The loop snl(c)+1..0 with snl=-4 would otherwise include j=0.] + !do j = snl(c)+1, merge(-1, 0, use_nvp .and. col%jbot_sno(c) == -1) + do j = snl(c)+1, 0 liquid_mass(c) = liquid_mass(c) + h2osoi_liq(c,j) ice_mass(c) = ice_mass(c) + h2osoi_ice(c,j) + ! [PORTED by Hui Tang: NVP debug — print each layer's water contribution to water mass] + if (use_nvp .and. col%jbot_sno(c) == -1 .and. c == bounds%begc) & + write(iulog,*) '[NVP DBG] ComputeLiqIceMass c=',c,' j=',j, & + ' liq=',h2osoi_liq(c,j),' ice=',h2osoi_ice(c,j), & + ' cum_liq=',liquid_mass(c),' cum_ice=',ice_mass(c) end do + ! [PORTED by Hui Tang: NVP debug — print h2osno_no_layers and h2osfc after snow loop] + if (use_nvp .and. col%jbot_sno(c) == -1 .and. c == bounds%begc) & + write(iulog,*) '[NVP DBG] ComputeLiqIceMass c=',c,' snl=',snl(c), & + ' h2osno_no_layers=',h2osno_no_layers(c),' h2osfc=',h2osfc(c), & + ' liqcan=',liqcan_col(c),' snocan=',snocan_col(c) if (col%hydrologically_active(c)) then ! It's important to exclude non-hydrologically-active points, because some of @@ -312,6 +334,12 @@ subroutine ComputeLiqIceMassNonLake(bounds, num_nolakec, filter_nolakec, & liquid_mass = liquid_mass(bounds%begc:bounds%endc), & ice_mass = ice_mass(bounds%begc:bounds%endc)) + ! [PORTED by Hui Tang: NVP debug — print total liquid_mass and ice_mass after all contributions] + if (use_nvp .and. col%jbot_sno(bounds%begc) == -1) & + write(iulog,*) '[NVP DBG] ComputeLiqIceMass TOTAL c=',bounds%begc, & + ' liquid_mass=',liquid_mass(bounds%begc),' ice_mass=',ice_mass(bounds%begc), & + ' total=',liquid_mass(bounds%begc)+ice_mass(bounds%begc) + if (subtract_dynbal_baselines) then ! Subtract baselines set in initialization do fc = 1, num_nolakec @@ -384,6 +412,9 @@ subroutine AccumulateSoilLiqIceMassNonLake(bounds, num_c, filter_c, & if (has_h2o) then liquid_mass(c) = liquid_mass(c) + h2osoi_liq(c,j) ice_mass(c) = ice_mass(c) + h2osoi_ice(c,j) + excess_ice(c,j) + write(iulog,*) '[NVP DBG] ComputeLiqIceMass c=',c,' j=',j, & + ' cum_liq=',liquid_mass(c),' cum_ice=',ice_mass(c), h2osoi_ice(c,j), excess_ice(c,j) + end if end do end do @@ -688,7 +719,10 @@ subroutine ComputeHeatNonLake(bounds, num_nolakec, filter_nolakec, & j = 1 heat_ice(c) = heat_ice(c) + & TempToHeat(temp = t_soisno(c,j), cv = (h2osno_no_layers(c)*cpice)) - do j = snl(c)+1,0 + + ! [PORTED by Hui Tang: stop at j=-1 when NVP at j=0 — NVP heat tracked separately] + !do j = snl(c)+1, merge(-1, 0, use_nvp .and. col%jbot_sno(c) == -1) + do j = snl(c)+1, 0 call AccumulateLiquidWaterHeat( & temp = t_soisno(c,j), & h2o = h2osoi_liq(c,j), & diff --git a/src/dyn_subgrid/dynEDMod.F90 b/src/dyn_subgrid/dynEDMod.F90 index e1cb1ccb42..f9d3807645 100644 --- a/src/dyn_subgrid/dynEDMod.F90 +++ b/src/dyn_subgrid/dynEDMod.F90 @@ -5,6 +5,7 @@ module dynEDMod use shr_kind_mod , only : r8 => shr_kind_r8 use decompMod , only : bounds_type use landunit_varcon, only : istsoil + use clm_varctl , only : iulog use PatchType , only : patch use ColumnType , only : col ! @@ -32,8 +33,10 @@ subroutine dyn_ED( bounds ) if (col%itype(c) == istsoil) then if (patch%is_veg(p) .or. patch%is_bareground(p)) then patch%wtcol(p) = patch%wt_ed(p) + write(iulog,'(a,2i6,2l2,f10.5)') '[DBG dynED] p, c, is_bg, is_veg, wtcol:', & + p, c, patch%is_bareground(p), patch%is_veg(p), patch%wtcol(p) else - patch%wtcol(p) = 0.0_r8 + patch%wtcol(p) = 0.0_r8 end if end if end do diff --git a/src/main/clm_driver.F90 b/src/main/clm_driver.F90 index 0851d7cfb1..e20fce5df7 100644 --- a/src/main/clm_driver.F90 +++ b/src/main/clm_driver.F90 @@ -1716,6 +1716,21 @@ subroutine clm_drv_patch2col (bounds, & waterfluxbulk_inst%qflx_ev_nvp_patch(bounds%begp:bounds%endp), & waterfluxbulk_inst%qflx_ev_nvp_col(bounds%begc:bounds%endc)) + if (use_nvp) then + do fc = 1, num_nolakec + c = filter_nolakec(fc) + if (col%nvp_layer_active(c)) then + associate(p_bg => col%patchi(c), p_vf => col%patchf(c)) + write(iulog,'(a,i6,5f12.6)') '[DBG NVP flux] c, frac_nvp, wtcol_bg, qflx_ev_nvp_bg, qflx_ev_nvp_veg(wtd sum), qflx_ev_nvp_col:', & + c, col%frac_nvp(c), patch%wtcol(p_bg), & + waterfluxbulk_inst%qflx_ev_nvp_patch(p_bg), & + sum(waterfluxbulk_inst%qflx_ev_nvp_patch(p_bg+1:p_vf) * patch%wtcol(p_bg+1:p_vf)), & + waterfluxbulk_inst%qflx_ev_nvp_col(c) + end associate + end if + end do + end if + ! Averaging for patch water flux variables call p2c (bounds, num_nolakec, filter_nolakec, & diff --git a/src/utils/clmfates_interfaceMod.F90 b/src/utils/clmfates_interfaceMod.F90 index 597524f043..f9d499145e 100644 --- a/src/utils/clmfates_interfaceMod.F90 +++ b/src/utils/clmfates_interfaceMod.F90 @@ -1798,6 +1798,10 @@ subroutine wrap_update_hlmfates_dyn(this, nc, bounds_clump, & dleaf_patch(p) = this%fates(nc)%bc_out(s)%dleaf_pa(ifp) end do ! veg patch + write(iulog,'(a,2i6,3f10.5)') '[DBG NVP patch] c, npatch, wt_ed(bg), sum(wt_ed_veg), areacheck:', & + c, npatch, patch%wt_ed(col%patchi(c)), & + sum(patch%wt_ed(col%patchi(c)+1:col%patchi(c)+npatch)), areacheck + ! [PORTED by Hui Tang: aggregate NVP patch geometry to column, then update layer state] ! nvp_dz_pa(ifp) = mean NVP thickness where NVP is present within patch [m] ! nvp_frac_pa(ifp) = fraction of patch covered by NVP [0-1] @@ -1822,6 +1826,9 @@ subroutine wrap_update_hlmfates_dyn(this, nc, bounds_clump, & c,ifp end do + write(iulog,'(a,i6,3f10.5)') '[DBG NVP wtcol] c, frac_nvp, wt_ed(bg), sum(wt_ed_veg):', & + c, col%frac_nvp(c), patch%wt_ed(col%patchi(c)), & + sum(patch%wt_ed(col%patchi(c)+1:col%patchi(c)+npatch)) ! [PORTED by Hui Tang: pass thermo instances only when present (normal timestep)] if (present(temperature_inst) .and. present(waterstatebulk_inst)) then call UpdateNVPLayer(c, temperature_inst, waterstatebulk_inst) From c3a977ae1cff5719c63beb868a5ead7b44617eb1 Mon Sep 17 00:00:00 2001 From: Sam Rabin Date: Wed, 13 May 2026 09:33:10 -0600 Subject: [PATCH 056/113] Update FATES: Avoid unconditional minimum elai and esai for NVP. --- .gitmodules | 2 +- src/fates | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index 8c57d557c2..1824a8f05f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -28,7 +28,7 @@ [submodule "fates"] path = src/fates url = https://github.com/huitang-earth/fates -fxtag = 49c247b396751aad117d8d03d290edc84ba222af +fxtag = b267a73b621b2156d3d6e3ae9289f1f43ca6fb0e fxrequired = AlwaysRequired # Standard Fork to compare to with "git fleximod test" to ensure personal forks aren't committed fxDONOTUSEurl = https://github.com/NGEET/fates diff --git a/src/fates b/src/fates index 49c247b396..b267a73b62 160000 --- a/src/fates +++ b/src/fates @@ -1 +1 @@ -Subproject commit 49c247b396751aad117d8d03d290edc84ba222af +Subproject commit b267a73b621b2156d3d6e3ae9289f1f43ca6fb0e From 1c72fe7d9330ea63c94b3039c1c5110dded35de1 Mon Sep 17 00:00:00 2001 From: Sam Rabin Date: Fri, 22 May 2026 17:26:46 -0600 Subject: [PATCH 057/113] BareGroundFluxesMod: Try wrapping patch%is_veg indexing in "if (use_fates)". Not sure why this would be necessary, but I sure did get a lot of "Reference to undefined POINTER PATCH%IS_VEG" errors at what was previously line 497 there. That array is only allocated if use_fates is true, But then again, use_nvp should always be false if use_fates is false. So I dunno. --- src/biogeophys/BareGroundFluxesMod.F90 | 30 ++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/biogeophys/BareGroundFluxesMod.F90 b/src/biogeophys/BareGroundFluxesMod.F90 index bce0b9a8b6..08cb2df1cc 100644 --- a/src/biogeophys/BareGroundFluxesMod.F90 +++ b/src/biogeophys/BareGroundFluxesMod.F90 @@ -24,6 +24,7 @@ module BareGroundFluxesMod use LandunitType , only : lun use ColumnType , only : col use PatchType , only : patch + use clm_varctl , only : use_fates ! ! !PUBLIC TYPES: implicit none @@ -303,8 +304,13 @@ subroutine BareGroundFluxes(bounds, num_noexposedvegp, filter_noexposedvegp, & p = filter_noexposedvegp(f) c = patch%column(p) if (use_nvp .and. col%nvp_layer_active(c)) then - write(iulog,'(a,2i6,2l2,2f10.5)') '[DBG noexposedvegp] p, c, is_bg, is_veg, wtcol, frac_nvp:', & - p, c, patch%is_bareground(p), patch%is_veg(p), patch%wtcol(p), col%frac_nvp(c) + ! SSR debug: I'm adding the "if use_fates" wrapper because of previous "Reference to undefined + ! POINTER PATCH%IS_VEG" errors below. Not sure if this ever might have happened here. + write(iulog,'(a,2i6,l2,2f10.5)') '[DBG noexposedvegp] p, c, is_bg, wtcol, frac_nvp:', & + p, c, patch%is_bareground(p), patch%wtcol(p), col%frac_nvp(c) + if (use_fates) then + write(iulog,'(a,l2)') '[DBG noexposedvegp] is_veg ', patch%is_veg(p) + end if end if btran(p) = 0._r8 t_veg(p) = forc_t(c) @@ -493,8 +499,14 @@ subroutine BareGroundFluxes(bounds, num_noexposedvegp, filter_noexposedvegp, & ! [PORTED by Hui Tang: NVP sensible heat flux for bare ground, analogous to snow/h2osfc] ! Zero when NVP is buried under snow (snl < -1): the snow surface controls the energy balance. ! Only compute for the NVP veg patch (patch%is_veg), not the bareground gap patch. - if (use_nvp .and. patch%is_veg(p) .and. col%frac_nvp(c) > 0._r8 .and. col%snl(c) >= -1) then - eflx_sh_nvp(p) = -raih*(thm(p)-t_nvp_col(c)) + ! SSR debug: I'm adding the "if use_fates" wrapper because of previous "Reference to undefined + ! POINTER PATCH%IS_VEG" errors here. Not sure if this is going to help. It might be because + ! patch%is_veg is only allocated when using FATES, which is why I'm trying this, but use_nvp + ! should not ever be true if not using FATES. + if (use_fates) then + if (use_nvp .and. patch%is_veg(p) .and. col%frac_nvp(c) > 0._r8 .and. col%snl(c) >= -1) then + eflx_sh_nvp(p) = -raih*(thm(p)-t_nvp_col(c)) + end if else eflx_sh_nvp(p) = 0._r8 end if @@ -516,8 +528,14 @@ subroutine BareGroundFluxes(bounds, num_noexposedvegp, filter_noexposedvegp, & ! NVPWaterBalance_Column; a non-zero value here when NVP is covered would add water to ! qflx_evap_tot_col without removing it from any tracked water store, causing errh2o. ! Only compute for the NVP veg patch (patch%is_veg), not the bareground gap patch. - if (use_nvp .and. patch%is_veg(p) .and. col%frac_nvp(c) > 0._r8 .and. col%snl(c) >= -1) then - qflx_ev_nvp(p) = -raiw*(forc_q(c) - qg_nvp(c)) + ! SSR debug: I'm adding the "if use_fates" wrapper because of previous "Reference to undefined + ! POINTER PATCH%IS_VEG" errors above. Not sure if this is going to help. It might be because + ! patch%is_veg is only allocated when using FATES, which is why I'm trying this, but use_nvp + ! should not ever be true if not using FATES. + if (use_fates) then + if (use_nvp .and. patch%is_veg(p) .and. col%frac_nvp(c) > 0._r8 .and. col%snl(c) >= -1) then + qflx_ev_nvp(p) = -raiw*(forc_q(c) - qg_nvp(c)) + end if else qflx_ev_nvp(p) = 0._r8 end if From 10d3502c645b3994cab906d7fdf06885973f99d3 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Mon, 25 May 2026 23:47:56 +0300 Subject: [PATCH 058/113] Bug fixes: avoid floating-point exception in SNICAR module --- src/biogeophys/SnowSnicarMod.F90 | 53 ++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/src/biogeophys/SnowSnicarMod.F90 b/src/biogeophys/SnowSnicarMod.F90 index a124210e80..1ca9befb0c 100644 --- a/src/biogeophys/SnowSnicarMod.F90 +++ b/src/biogeophys/SnowSnicarMod.F90 @@ -422,6 +422,9 @@ subroutine SNICAR_RT (bounds, num_nourbanc, filter_nourbanc, & real(r8):: smr ! accumulator for rdif gaussian integration real(r8):: smt ! accumulator for tdif gaussian integration real(r8):: exp_min ! minimum exponential value + ! [PORTED by Hui Tang: resonance guard for Delta-Eddington denominator (lm*mu=1 singularity)] + real(r8):: denom_dir ! denominator (1 - lm^2*mu_not^2) for direct-beam alp/gam + real(r8):: denom_dif ! denominator (1 - lm^2*mu^2) for Gaussian-loop alp/gam integer :: ng ! gaussian integration index integer, parameter :: ngmax = 8 ! max gaussian integration index @@ -1145,6 +1148,21 @@ subroutine SNICAR_RT (bounds, num_nourbanc, filter_nourbanc, & ws = omega_star(i) gs = g_star(i) + ! [DEBUG by Hui Tang: diagnose SIGFPE at line 1151] + ! Fires when ws~1 (lm->0, div-by-zero) or lm*mu_not~1 (resonance). + if (ws > 0.9999_r8 .or. & + c3*(c1-ws)*(c1-ws*gs)*mu_not*mu_not > 0.9_r8) then + write(iulog,'(A,3(1X,I6),4(1X,A,ES14.6))') & + 'SNICAR_SIGFPE_DIAG col/layer/band:', c_idx, i, bnd_idx, & + ' ws=', ws, ' gs=', gs, ' ts=', ts, ' mu_not=', mu_not + write(iulog,'(A,2(1X,I6),3(1X,A,ES14.6),A,I8,2(1X,A,ES14.6))') & + 'SNICAR_SIGFPE_INPUTS snl/flg:', snl_lcl, flg_slr_in, & + ' h2osno_ice(-1)=', h2osno_ice_lcl(-1), & + ' h2osno_ice(0)=', h2osno_ice_lcl(0), & + ' tau(i)=', tau(i), ' snw_rds(i)=', snw_rds_lcl(i), & + ' omega(i)=', omega(i), ' g(i)=', g(i) + call flush(iulog) + end if ! Delta-Eddington solution expressions, Eq. 50: Briegleb and Light 2007 lm = sqrt(c3*(c1-ws)*(c1 - ws*gs)) ue = c1p5*(c1 - ws*gs)/lm @@ -1161,12 +1179,20 @@ subroutine SNICAR_RT (bounds, num_nourbanc, filter_nourbanc, & ! Delta-Eddington solution expressions ! Eq. 50: Briegleb and Light 2007; alpha and gamma for direct radiation - alp = cp75*ws*mu_not*((c1 + gs*(c1-ws))/(c1 - lm*lm*mu_not*mu_not)) - gam = cp5*ws*((c1 + c3*gs*(c1-ws)*mu_not*mu_not)/(c1-lm*lm*mu_not*mu_not)) - apg = alp + gam - amg = alp - gam - rdir(i) = apg*rdif_a(i) + amg*(tdif_a(i)*trnlay(i) - c1) - tdir(i) = apg*tdif_a(i) + (amg* rdif_a(i)-apg+c1)*trnlay(i) + ! [PORTED by Hui Tang: skip direct-beam alp/gam at lm*mu_not~1 resonance; + ! fall back to rdif_a/tdif_a as limiting approximation to keep values physical] + denom_dir = c1 - lm*lm*mu_not*mu_not + if (abs(denom_dir) >= 1.e-4_r8) then + alp = cp75*ws*mu_not*((c1 + gs*(c1-ws))/denom_dir) + gam = cp5*ws*((c1 + c3*gs*(c1-ws)*mu_not*mu_not)/denom_dir) + apg = alp + gam + amg = alp - gam + rdir(i) = apg*rdif_a(i) + amg*(tdif_a(i)*trnlay(i) - c1) + tdir(i) = apg*tdif_a(i) + (amg*rdif_a(i)-apg+c1)*trnlay(i) + else + rdir(i) = rdif_a(i) + tdir(i) = tdif_a(i)*trnlay(i) + end if ! recalculate rdif,tdif using direct angular integration over rdir,tdir, ! since Delta-Eddington rdif formula is not well-behaved (it is usually @@ -1178,13 +1204,17 @@ subroutine SNICAR_RT (bounds, num_nourbanc, filter_nourbanc, & smr = c0 smt = c0 ! gaussian angles for the AD integral + ! [PORTED by Hui Tang: skip any Gaussian angle where lm*mu~1 (resonance); + ! clamping the denominator instead would make rdr/tdr huge and cause downstream NaN] do ng=1,ngmax mu = difgauspt(ng) gwt = difgauswt(ng) + denom_dif = c1 - lm*lm*mu*mu + if (abs(denom_dif) < 1.e-4_r8) cycle swt = swt + mu*gwt trn = max(exp_min, exp(-ts/mu)) - alp = cp75*ws*mu*((c1 + gs*(c1-ws))/(c1 - lm*lm*mu*mu)) - gam = cp5*ws*((c1 + c3*gs*(c1-ws)*mu*mu)/(c1-lm*lm*mu*mu)) + alp = cp75*ws*mu*((c1 + gs*(c1-ws))/denom_dif) + gam = cp5*ws*((c1 + c3*gs*(c1-ws)*mu*mu)/denom_dif) apg = alp + gam amg = alp - gam rdr = apg*R1 + amg*T1*trn - amg @@ -1192,8 +1222,11 @@ subroutine SNICAR_RT (bounds, num_nourbanc, filter_nourbanc, & smr = smr + mu*rdr*gwt smt = smt + mu*tdr*gwt enddo ! ng - rdif_a(i) = smr/swt - tdif_a(i) = smt/swt + if (swt > c0) then + rdif_a(i) = smr/swt + tdif_a(i) = smt/swt + end if + ! if swt==0 (all angles resonant — pathological): keep initial R1/T1 values ! homogeneous layer rdif_b(i) = rdif_a(i) From 6ba9a9d85fbf9bb9e98773e95ec44858ba221876 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Mon, 25 May 2026 23:50:36 +0300 Subject: [PATCH 059/113] Key bug fix: solving soil balance error for both NVP and non-NVP run. --- src/biogeophys/SnowSnicarMod.F90 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/biogeophys/SnowSnicarMod.F90 b/src/biogeophys/SnowSnicarMod.F90 index 1ca9befb0c..5f71b67e54 100644 --- a/src/biogeophys/SnowSnicarMod.F90 +++ b/src/biogeophys/SnowSnicarMod.F90 @@ -735,8 +735,11 @@ subroutine SNICAR_RT (bounds, num_nourbanc, filter_nourbanc, & ! Set local aerosol array (snow layers only; NVP layer 0 already zeroed above) + ! [PORTED by Hui Tang: use snl_btm (not min(snl_btm,0)-1) so that with NVP active + ! (snl_btm=-1) layer -1 (bottom snow) is included; min()-1 gave -2, skipping layer -1, + ! leaving mss_cnc_aer_lcl(-1,:) stale and corrupting g(-1) past 1 -> tau_star < 0 -> SIGFPE] do j=1,sno_nbr_aer - mss_cnc_aer_lcl(snl_top:min(snl_btm,0)-1,j) = mss_cnc_aer_in(c_idx,snl_top:min(snl_btm,0)-1,j) + mss_cnc_aer_lcl(snl_top:snl_btm,j) = mss_cnc_aer_in(c_idx,snl_top:snl_btm,j) if (.not. nvp_active) then mss_cnc_aer_lcl(0,j) = mss_cnc_aer_in(c_idx,0,j) end if From 03989b15db32f8cfa22e62b31cdd13b1ea349880 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Mon, 25 May 2026 23:52:37 +0300 Subject: [PATCH 060/113] Add debug output for soil balance error. --- src/biogeophys/SoilFluxesMod.F90 | 67 ++++++++++++++++++++++++++- src/biogeophys/SoilTemperatureMod.F90 | 5 +- 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/src/biogeophys/SoilFluxesMod.F90 b/src/biogeophys/SoilFluxesMod.F90 index a1ba7a6f73..740d53e26f 100644 --- a/src/biogeophys/SoilFluxesMod.F90 +++ b/src/biogeophys/SoilFluxesMod.F90 @@ -79,7 +79,8 @@ subroutine SoilFluxes (bounds, num_urbanl, filter_urbanl, & real(r8) :: t_grnd0(bounds%begc:bounds%endc) ! t_grnd of previous time step real(r8) :: lw_grnd real(r8) :: evaporation_limit ! top layer moisture available for evaporation - real(r8) :: evaporation_demand ! evaporative demand + real(r8) :: evaporation_demand ! evaporative demand + real(r8) :: heat_store_diag ! [PORTED by Hui Tang: errsoi diagnostic - heat storage sum] !----------------------------------------------------------------------- associate( & @@ -97,6 +98,9 @@ subroutine SoilFluxes (bounds, num_urbanl, filter_urbanl, & sabg_soil => solarabs_inst%sabg_soil_patch , & ! Input: [real(r8) (:) ] solar radiation absorbed by soil (W/m**2) sabg_snow => solarabs_inst%sabg_snow_patch , & ! Input: [real(r8) (:) ] solar radiation absorbed by snow (W/m**2) sabg => solarabs_inst%sabg_patch , & ! Input: [real(r8) (:) ] solar radiation absorbed by ground (W/m**2) + ! [PORTED by Hui Tang: NVP errsoi fix - solar by layer and NVP sensible heat] + sabg_lyr => solarabs_inst%sabg_lyr_patch , & ! Input: [real(r8) (:,:) ] solar radiation absorbed per snow/soil layer (W/m**2) + eflx_sh_nvp => energyflux_inst%eflx_sh_nvp_patch , & ! Input: [real(r8) (:) ] sensible heat flux from NVP (W/m**2) [+ to atm] emg => temperature_inst%emg_col , & ! Input: [real(r8) (:) ] ground emissivity ! emv => temperature_inst%emv_patch , & ! Input: [real(r8) (:) ] vegetation emissivity @@ -403,7 +407,6 @@ subroutine SoilFluxes (bounds, num_urbanl, filter_urbanl, & eflx_sh_tot(p) = eflx_sh_veg(p) + eflx_sh_grnd(p) if (.not. lun%urbpoi(l)) eflx_sh_tot(p) = eflx_sh_tot(p) + eflx_sh_stem(p) - print *, "qflx_evap_veg=", qflx_evap_veg(p), qflx_evap_soi(p) qflx_evap_tot(p) = qflx_evap_veg(p) + qflx_evap_soi(p) eflx_lh_tot(p)= hvap*qflx_evap_veg(p) + htvp(c)*qflx_evap_soi(p) @@ -475,6 +478,66 @@ subroutine SoilFluxes (bounds, num_urbanl, filter_urbanl, & end do end do + ! [PORTED by Hui Tang: NVP errsoi fix] + ! When NVP is active (snl=0, jbot_sno=-1), layer j=0 is the atmospheric boundary of + ! the soil column but is skipped by the loops above (condition j>=snl+1=1 fails for j=0). + ! Two corrections are needed: + ! (1) Add j=0 heat storage (the NVP layer temperature change). + ! (2) Replace the LHS boundary flux: eflx_soil_grnd uses the soil-surface (j=1) + ! terms, but the actual top-of-column boundary is the NVP surface (j=0). + ! We add (eflx_gnet_nvp - eflx_soil_grnd) to swap in the NVP surface budget. + ! eflx_gnet_nvp = sabg_lyr(p,0) + dlrad + (1-frac_veg_nosno)*emg*forc_lwrad + ! - emg*sb*tssbef(c,0)^4 - (eflx_sh_nvp + qflx_ev_nvp*htvp) + ! tssbef(c,0) is the pre-update NVP temperature, matching what SoilTemperatureMod + ! used in the BC when it computed lwrad_emit_nvp. + if (use_nvp) then + do fp = 1, num_nolakep + p = filter_nolakep(fp) + c = patch%column(p) + if (col%nvp_layer_active(c) .and. col%snl(c) == 0) then + errsoi_patch(p) = errsoi_patch(p) & + - (t_soisno(c,0) - tssbef(c,0)) / fact(c,0) + errsoi_patch(p) = errsoi_patch(p) & + + (sabg_lyr(p,0) + dlrad(p) & + + (1 - frac_veg_nosno(p))*emg(c)*forc_lwrad(c) & + - emg(c)*sb*tssbef(c,0)**4 & + - (eflx_sh_nvp(p) + qflx_ev_nvp(p)*htvp(c))) & + - eflx_soil_grnd(p) + end if + end do + end if + + ! [PORTED by Hui Tang: errsoi diagnostic - decompose terms when error is large] + do fp = 1, num_nolakep + p = filter_nolakep(fp) + c = patch%column(p) + if (abs(errsoi_patch(p)) > 0.5_r8) then + heat_store_diag = 0._r8 + do j = -nlevsno+1, nlevgrnd + if (col%itype(c) /= icol_sunwall .and. col%itype(c) /= icol_shadewall & + .and. col%itype(c) /= icol_roof) then + if (j >= col%snl(c)+1 .and. j < 1) heat_store_diag = heat_store_diag + & + frac_sno_eff(c)*(t_soisno(c,j)-tssbef(c,j))/fact(c,j) + if (j >= 1) heat_store_diag = heat_store_diag + & + (t_soisno(c,j)-tssbef(c,j))/fact(c,j) + end if + end do + write(iulog,*) '[ERRSOI DBG] p=',p,' c=',c,' snl=',col%snl(c) + write(iulog,*) ' errsoi_patch =', errsoi_patch(p) + write(iulog,*) ' eflx_soil_grnd =', eflx_soil_grnd(p) + write(iulog,*) ' xmf (phase chg) =', xmf(c) + write(iulog,*) ' xmf_h2osfc =', xmf_h2osfc(c) + write(iulog,*) ' heat_store_sum =', heat_store_diag + write(iulog,*) ' eflx_h2osfc_snow =', eflx_h2osfc_to_snow_col(c) + write(iulog,*) ' frac_h2osfc_term =', & + frac_h2osfc(c)*(t_h2osfc(c)-t_h2osfc_bef(c))*(c_h2osfc(c)/dtime) + write(iulog,*) ' expected_errsoi =', & + eflx_soil_grnd(p) - xmf(c) - xmf_h2osfc(c) - heat_store_diag & + + eflx_h2osfc_to_snow_col(c) & + - frac_h2osfc(c)*(t_h2osfc(c)-t_h2osfc_bef(c))*(c_h2osfc(c)/dtime) + end if + end do + call t_stopf('bgp2_loop_3') call t_startf('bgp2_loop_4') diff --git a/src/biogeophys/SoilTemperatureMod.F90 b/src/biogeophys/SoilTemperatureMod.F90 index b002426dd5..5d730b4276 100644 --- a/src/biogeophys/SoilTemperatureMod.F90 +++ b/src/biogeophys/SoilTemperatureMod.F90 @@ -845,8 +845,7 @@ subroutine SoilThermProp (bounds, num_urbanc, filter_urbanc, num_nolakec, filter if (snl(c)+1 < 1 .AND. (j >= snl(c)+1) .AND. (j <= 0) .AND. & .NOT. (use_nvp .AND. jbot_sno(c) == -1 .AND. j == 0)) then bw(c,j) = (h2osoi_ice(c,j)+h2osoi_liq(c,j))/(frac_sno(c)*dz(c,j)) - print *, 'bw=', bw(c,j), h2osoi_ice(c,j), h2osoi_liq(c,j),dz(c,j) - + l = col%landunit(c) ! Select method over glacier land unit @@ -1055,8 +1054,6 @@ subroutine SoilThermProp (bounds, num_urbanc, filter_urbanc, num_nolakec, filter end do end if - print *, "cv(:,:)=", cv - call t_stopf( 'SoilThermProp' ) end associate From 25340f696672b0d5d1634f4f699604178f860da2 Mon Sep 17 00:00:00 2001 From: Sam Rabin Date: Tue, 26 May 2026 10:12:07 -0600 Subject: [PATCH 061/113] Update FATES: Add initialization of optical parameter nvp_omega-pa for nvp run. --- .gitmodules | 2 +- src/fates | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index 1824a8f05f..27ab9cc700 100644 --- a/.gitmodules +++ b/.gitmodules @@ -28,7 +28,7 @@ [submodule "fates"] path = src/fates url = https://github.com/huitang-earth/fates -fxtag = b267a73b621b2156d3d6e3ae9289f1f43ca6fb0e +fxtag = 6686502eb6d06ad498efcfcf6fba2ac98c5f0ac9 fxrequired = AlwaysRequired # Standard Fork to compare to with "git fleximod test" to ensure personal forks aren't committed fxDONOTUSEurl = https://github.com/NGEET/fates diff --git a/src/fates b/src/fates index b267a73b62..6686502eb6 160000 --- a/src/fates +++ b/src/fates @@ -1 +1 @@ -Subproject commit b267a73b621b2156d3d6e3ae9289f1f43ca6fb0e +Subproject commit 6686502eb6d06ad498efcfcf6fba2ac98c5f0ac9 From bba2f74996c718fb7847146ee6e42c61f04b01a3 Mon Sep 17 00:00:00 2001 From: Sam Rabin Date: Tue, 26 May 2026 11:07:20 -0600 Subject: [PATCH 062/113] Replace a flush() with shr_sys_flush(). --- src/biogeophys/SnowSnicarMod.F90 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/biogeophys/SnowSnicarMod.F90 b/src/biogeophys/SnowSnicarMod.F90 index 5f71b67e54..937e4ce83a 100644 --- a/src/biogeophys/SnowSnicarMod.F90 +++ b/src/biogeophys/SnowSnicarMod.F90 @@ -1164,7 +1164,7 @@ subroutine SNICAR_RT (bounds, num_nourbanc, filter_nourbanc, & ' h2osno_ice(0)=', h2osno_ice_lcl(0), & ' tau(i)=', tau(i), ' snw_rds(i)=', snw_rds_lcl(i), & ' omega(i)=', omega(i), ' g(i)=', g(i) - call flush(iulog) + call shr_sys_flush(iulog) end if ! Delta-Eddington solution expressions, Eq. 50: Briegleb and Light 2007 lm = sqrt(c3*(c1-ws)*(c1 - ws*gs)) From 0061373618bc25420d92ee2b1474b76e24e12d75 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Mon, 1 Jun 2026 15:59:34 +0300 Subject: [PATCH 063/113] Exclude NVP layer for snow aerosol calculation. --- src/biogeophys/AerosolMod.F90 | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/biogeophys/AerosolMod.F90 b/src/biogeophys/AerosolMod.F90 index 39ade89fb0..57884e0d0e 100644 --- a/src/biogeophys/AerosolMod.F90 +++ b/src/biogeophys/AerosolMod.F90 @@ -570,7 +570,11 @@ subroutine AerosolMasses(bounds, num_on, filter_on, num_off, filter_off, & ! layer mass of snow: snowmass = h2osoi_ice(c,j) + h2osoi_liq(c,j) - if (j >= snl(c)+1) then + ! [PORTED by Hui Tang: exclude NVP layer j=0 from aerosol snow calculation. + ! When NVP is active jbot_sno=-1 (bottom snow = j=-1); j=0 is NVP not snow. + ! Without this guard, j=0 satisfies j>=snl+1 and snowmass(j=0)≈0 → SIGFPE + ! divide-by-zero at the concentration lines below.] + if (j >= snl(c)+1 .and. j <= col%jbot_sno(c)) then mss_bctot(c,j) = mss_bcpho(c,j) + mss_bcphi(c,j) mss_bc_col(c) = mss_bc_col(c) + mss_bctot(c,j) From 0bd6bdcdb24d91e89462306cc12d7d253cc2d2a9 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Mon, 1 Jun 2026 16:05:02 +0300 Subject: [PATCH 064/113] Add debugging output for NVP layer dynamics. --- src/biogeophys/NVPLayerDynamicsMod.F90 | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/biogeophys/NVPLayerDynamicsMod.F90 b/src/biogeophys/NVPLayerDynamicsMod.F90 index 0379473d66..73de4afccb 100644 --- a/src/biogeophys/NVPLayerDynamicsMod.F90 +++ b/src/biogeophys/NVPLayerDynamicsMod.F90 @@ -45,9 +45,9 @@ module NVPLayerDynamicsMod use WaterDiagnosticBulkType, only : waterdiagnosticbulk_type ! [PORTED by Hui Tang: soilstate for bidirectional NVP-soil Darcy flux] use SoilStateType , only : soilstate_type - use clm_varcon , only : cpliq, cpice, denh2o, roverg, tfrz, denice + use clm_varcon , only : cpliq, cpice, denh2o, roverg, tfrz, denice, hfus, spval ! [PORTED by Hui Tang: use_nvp_undersnow flag to deactivate NVP when snow present] - use clm_varctl , only : use_nvp_undersnow + use clm_varctl , only : use_nvp_undersnow, iulog use QSatMod , only : QSat ! [PORTED by Hui Tang: runtime-tunable NVP physics parameters] use NVPParamsMod @@ -107,6 +107,22 @@ subroutine UpdateNVPLayer(c, temperature_inst, waterstate_inst) dz_nvp = col%dz_nvp(c) was_active = col%nvp_layer_active(c) + ! [PORTED by Hui Tang: trace appear/disappear transitions in UpdateNVPLayer] + if (present(waterstate_inst)) then + write(iulog,'(a,i5,a,l1,2a,i8,3(a,f8.4))') & + '[DBG NVP update] c=', c, & + ' was_active=', was_active, ' pres_water=T', & + ' snl=', col%snl(c), & + ' dz_nvp=', dz_nvp, ' frac_nvp=', col%frac_nvp(c), & + ' ice0=', waterstate_inst%h2osoi_ice_col(c,0) + else + write(iulog,'(a,i5,a,l1,2a,i8,2(a,f8.4))') & + '[DBG NVP update] c=', c, & + ' was_active=', was_active, ' pres_water=F', & + ' snl=', col%snl(c), & + ' dz_nvp=', dz_nvp, ' frac_nvp=', col%frac_nvp(c) + end if + if (col%frac_nvp(c) > nvp_frac_min .and. dz_nvp > 0._r8) then ! --- Active (Appear or Grow/shrink) --- From a93dadf582f0a9b699fc89edefc7a9054da2e946 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Mon, 1 Jun 2026 16:06:34 +0300 Subject: [PATCH 065/113] Initialize volumetric water content of new NVP layer to 0.6 (similar to the initialization of soil water content). --- src/biogeophys/NVPLayerDynamicsMod.F90 | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/biogeophys/NVPLayerDynamicsMod.F90 b/src/biogeophys/NVPLayerDynamicsMod.F90 index 73de4afccb..4412ebe9f9 100644 --- a/src/biogeophys/NVPLayerDynamicsMod.F90 +++ b/src/biogeophys/NVPLayerDynamicsMod.F90 @@ -170,9 +170,14 @@ subroutine UpdateNVPLayer(c, temperature_inst, waterstate_inst) if (.not. was_active .and. now_active) then ! --- Appear: initialise layer-0 thermodynamic state from soil layer 1 --- ! Temperature inherits from layer 1 to avoid spurious heat flux. - ! h2osoi starts at zero; FATES provides moisture via its own hydrology. + ! [PORTED by Hui Tang: initialise NVP liquid to 0.6 * dz * denh2o (volumetric + ! water content = 0.6) instead of 0; mirrors FATES-soil cold-start convention. + ! NOTE: with use_nvp_undersnow=.true. (default) this branch runs only once + ! (cold start). With use_nvp_undersnow=.false. it also runs on spring + ! reactivation — in that case the water is unphysical, but that config is + ! non-default and not the current use case.] temperature_inst%t_soisno_col(c,0) = temperature_inst%t_soisno_col(c,1) - waterstate_inst%h2osoi_liq_col(c,0) = 0._r8 + waterstate_inst%h2osoi_liq_col(c,0) = 0.6_r8 * col%dz(c,0) * denh2o waterstate_inst%h2osoi_ice_col(c,0) = 0._r8 else if (was_active .and. .not. now_active) then From 5ac004e3a5b3257d2250e7ffb8704f944e923653 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Mon, 1 Jun 2026 16:09:21 +0300 Subject: [PATCH 066/113] Calculate infiltration flux of NVP only when snow is absent. --- src/biogeophys/NVPLayerDynamicsMod.F90 | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/biogeophys/NVPLayerDynamicsMod.F90 b/src/biogeophys/NVPLayerDynamicsMod.F90 index 4412ebe9f9..880f7d3d30 100644 --- a/src/biogeophys/NVPLayerDynamicsMod.F90 +++ b/src/biogeophys/NVPLayerDynamicsMod.F90 @@ -209,6 +209,10 @@ subroutine UpdateNVPLayer(c, temperature_inst, waterstate_inst) ! --- Grow/shrink (active→active): T and h2osoi are per unit ground area, ! so no adjustment is needed when dz_nvp changes. --- + ! [PORTED by Hui Tang: per-CLM-timestep phase change for j=0 is handled in + ! SoilTemperatureMod::Phasechange (runs before SoilFluxes so xmf is correct). + ! The FATES-dynamics-frequency melt block that was here is removed.] + end if end subroutine UpdateNVPLayer @@ -482,8 +486,17 @@ subroutine NVPWaterBalance_Column(bounds, dtime, waterfluxbulk_inst, waterstate_ frac_nvp_eff = min(col%frac_nvp(c), max(0._r8, 1._r8 - frac_h2osfc)) ! --- Water input to NVP from precipitation / snowmelt --- - qflx_nvp_infl_col(c) = frac_nvp_eff * qflx_rain_plus_snomelt(c) ! [mm/s] - print *, "qflx_nvp_infl_col=", frac_nvp_eff, frac_h2osfc, qflx_rain_plus_snomelt(c),qflx_nvp_infl_col + ! [PORTED by Hui Tang: when snow is present (snl < 0), snow percolation into NVP is + ! already applied to h2osoi_liq(c,0) by UpdateState_SnowPercolation (adds + ! qflx_snow_percolation_col(c,-1)*dtime to h2osoi_liq(c,0)). qflx_rain_plus_snomelt + ! equals qflx_snow_percolation_col(c,0) = NVP outflow, not inflow; using it here + ! would double-count. When no snow (snl=0), qflx_rain_plus_snomelt = rain + snowmelt + ! is the correct NVP inflow from the nosnowc branch of SumFlux_AddSnowPercolation.] + if (col%snl(c) < 0) then + qflx_nvp_infl_col(c) = 0._r8 + else + qflx_nvp_infl_col(c) = frac_nvp_eff * qflx_rain_plus_snomelt(c) ! [mm/s] + end if ! --- NVP volumetric water content (clamped to valid range) --- From adcdd9d6282d002e1d182de7706f8c40b354d366 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Mon, 1 Jun 2026 16:17:39 +0300 Subject: [PATCH 067/113] Add calculation of day length factor specifically for NVP. --- src/utils/clmfates_interfaceMod.F90 | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/utils/clmfates_interfaceMod.F90 b/src/utils/clmfates_interfaceMod.F90 index f9d499145e..ac5b0b039d 100644 --- a/src/utils/clmfates_interfaceMod.F90 +++ b/src/utils/clmfates_interfaceMod.F90 @@ -2863,7 +2863,7 @@ subroutine wrap_nvp_photosynthesis(this, nc, bounds, & type(temperature_type), intent(in) :: temperature_inst type(waterdiagnosticbulk_type), intent(in) :: waterdiagnosticbulk_inst - integer :: s, c, p, ifp + integer :: s, c, p, ifp, g real(r8) :: dtime call t_startf('fates_nvp_psn') @@ -2881,6 +2881,7 @@ subroutine wrap_nvp_photosynthesis(this, nc, bounds, & do ifp = 1, this%fates(nc)%sites(s)%youngest_patch%patchno p = ifp + col%patchi(c) + g = patch%gridcell(p) ! Re-enable processing for all patches (reset from 3 → 2) this%fates(nc)%bc_in(s)%filter_photo_pa(ifp) = 2 @@ -2893,6 +2894,12 @@ subroutine wrap_nvp_photosynthesis(this, nc, bounds, & this%fates(nc)%bc_in(s)%t_nvp_pa(ifp) = temperature_inst%t_nvp_col(c) this%fates(nc)%bc_in(s)%fwet_nvp_pa(ifp) = waterdiagnosticbulk_inst%fwet_nvp_col(c) + ! [PORTED by Hui Tang: dayl_factor_pa — CanopyFluxesMod is never called for NVP + ! columns, so dayl_factor_pa is never set there; compute it here directly from + ! gridcell daylength following the same formula as CanopyFluxesMod line 840.] + this%fates(nc)%bc_in(s)%dayl_factor_pa(ifp) = & + min(1._r8, max(0.01_r8, (grc%dayl(g)**2) / (grc%max_dayl(g)**2))) + end do end do From 2a59b2bdb9031fd8fcbeb8ca87e789bbdea15604 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Mon, 1 Jun 2026 16:38:28 +0300 Subject: [PATCH 068/113] Add debugging output for nvp filter, energy and water balances. --- src/biogeophys/HydrologyNoDrainageMod.F90 | 18 ++++++++++++++++++ src/biogeophys/SoilTemperatureMod.F90 | 17 ++++------------- src/main/clm_driver.F90 | 12 ++++++++++++ src/utils/clmfates_interfaceMod.F90 | 4 ++++ 4 files changed, 38 insertions(+), 13 deletions(-) diff --git a/src/biogeophys/HydrologyNoDrainageMod.F90 b/src/biogeophys/HydrologyNoDrainageMod.F90 index ed9d6dc832..dd756e7e9b 100644 --- a/src/biogeophys/HydrologyNoDrainageMod.F90 +++ b/src/biogeophys/HydrologyNoDrainageMod.F90 @@ -466,15 +466,33 @@ subroutine HydrologyNoDrainage(bounds, & ! Divide thick snow elements call DivideSnowLayers(bounds, num_snowc, filter_snowc, & aerosol_inst, temperature_inst, water_inst, is_lake=.false.) + + ! [PORTED by Hui Tang: NVP debug — j=0 state before ZeroEmptySnow] + if (use_nvp .and. col%jbot_sno(bounds%begc) == -1) & + write(iulog,*) '[NVP DBG] before ZeroEmptySnow c=1 snl=', col%snl(bounds%begc), & + ' ice0=', h2osoi_ice(bounds%begc,0), ' liq0=', h2osoi_liq(bounds%begc,0) ! Set empty snow layers to zero call ZeroEmptySnowLayers(bounds, num_snowc, filter_snowc, & col, water_inst, temperature_inst) + ! [PORTED by Hui Tang: NVP debug — j=0 state after ZeroEmptySnow] + if (use_nvp .and. col%jbot_sno(bounds%begc) == -1) & + write(iulog,*) '[NVP DBG] after ZeroEmptySnow c=1 snl=', col%snl(bounds%begc), & + ' ice0=', h2osoi_ice(bounds%begc,0), ' liq0=', h2osoi_liq(bounds%begc,0) + ! Build new snow filter + write(iulog,*) '[NVP DBG] before snowfilter c=1 snl=', col%snl(bounds%begc), & + ' num_snowc=', num_snowc, ' filter_snowc=', filter_snowc, & + ' num_nosnowc=', num_nosnowc, ' filter_nosnowc=', filter_nosnowc call BuildSnowFilter(bounds, num_nolakec, filter_nolakec, & num_snowc, filter_snowc, num_nosnowc, filter_nosnowc) + + write(iulog,*) '[NVP DBG] after snowfilter c=1 snl=', col%snl(bounds%begc), & + ' num_snowc=', num_snowc, ' filter_snowc=', filter_snowc, & + ' num_nosnowc=', num_nosnowc, ' filter_nosnowc=', filter_nosnowc + ! TODO(wjs, 2019-09-16) Eventually move this down, merging this with later tracer ! consistency checks. If/when we remove calls to TracerConsistencyCheck from this diff --git a/src/biogeophys/SoilTemperatureMod.F90 b/src/biogeophys/SoilTemperatureMod.F90 index 5d730b4276..a658e09482 100644 --- a/src/biogeophys/SoilTemperatureMod.F90 +++ b/src/biogeophys/SoilTemperatureMod.F90 @@ -380,12 +380,6 @@ subroutine SoilTemperature(bounds, num_urbanl, filter_urbanl, num_urbanc, filter ! Set up right-hand side vecor (vector r). - print *, "hs_h2osfc=", hs_h2osfc - print *, "c_h2osfc=", c_h2osfc - print *, "h2osfc=", h2osfc - print *, "frac_h2osfc=", frac_h2osfc - print *, "thin_sfclayer=", thin_sfclayer - call SetRHSVec(bounds, num_nolakec, filter_nolakec, & dtime, & @@ -446,17 +440,12 @@ subroutine SoilTemperature(bounds, num_urbanl, filter_urbanl, num_urbanc, filter call t_startf( 'SoilTempBandDiag') ! Solve the system - print*, "bmatrix=", bmatrix(begc:endc, :, :) - print*, "rvector=", rvector(begc:endc, :) - print*, "tvector=", tvector(begc:endc, :) - print*, "nvp_layer_active=", col%nvp_layer_active, snl call BandDiagonal(bounds, -nlevsno, nlevmaxurbgrnd, jtop(begc:endc), jbot(begc:endc), & num_nolakec, filter_nolakec, nband, bmatrix(begc:endc, :, :), & rvector(begc:endc, :), tvector(begc:endc, :)) call t_stopf( 'SoilTempBandDiag') - print*, "tvector_after=", tvector ! return temperatures to original array @@ -606,11 +595,13 @@ subroutine SoilTemperature(bounds, num_urbanl, filter_urbanl, num_urbanc, filter do fc = 1, num_nvpc ! [PORTED: override for NVP-active columns] c = filter_nvpc(fc) t_nvp_col(c) = t_soisno(c,0) + ! [DBG NVP sabg] NVP temperature after tridiagonal solver + write(iulog,*) '[DBG NVP sabg] T: c=', c, & + ' t_soisno(c,0)=', t_soisno(c,0), & + ' t_nvp_col(c)=', t_nvp_col(c) end do end if - print *, "t_nvp_col=", t_nvp_col, t_soisno(c,:) - do fc = 1,num_nolakec c = filter_nolakec(fc) ! [PORTED by Hui Tang: NVP fractional area for t_grnd blend (excludes snow and h2osfc)] diff --git a/src/main/clm_driver.F90 b/src/main/clm_driver.F90 index e20fce5df7..7f23ea7614 100644 --- a/src/main/clm_driver.F90 +++ b/src/main/clm_driver.F90 @@ -763,6 +763,12 @@ subroutine clm_drv(doalb, nextsw_cday, declinp1, declin, rstwr, nlend, rdate, ro bounds_clump, canopystate_inst%tlai_patch(bounds_clump%begp:bounds_clump%endp)) end if + ! [DBG NVP] Check exposed/noexposed veg patch filters before CanopyFluxes + write(iulog,*) '[DBG filter] num_exposedvegp=', filter(nc)%num_exposedvegp, & + ' exposedvegp=', filter(nc)%exposedvegp(1:filter(nc)%num_exposedvegp) + write(iulog,*) '[DBG filter] num_noexposedvegp=', filter(nc)%num_noexposedvegp, & + ' noexposedvegp=', filter(nc)%noexposedvegp(1:filter(nc)%num_noexposedvegp) + call CanopyFluxes(bounds_clump, & filter(nc)%num_exposedvegp, filter(nc)%exposedvegp, & clm_fates,nc, & @@ -964,6 +970,12 @@ subroutine clm_drv(doalb, nextsw_cday, declinp1, declin, rstwr, nlend, rdate, ro saturated_excess_runoff_inst, & infiltration_excess_runoff_inst, & aerosol_inst, canopystate_inst, scf_method, soil_water_retention_curve, topo_inst) + + ! [DBG NVP] Check snow/nosnow column filters before HydrologyNoDrainage + write(iulog,*) '[DBG filter] num_snowc=', filter(nc)%num_snowc, & + ' snowc=', filter(nc)%snowc(1:filter(nc)%num_snowc) + write(iulog,*) '[DBG filter] num_nosnowc=', filter(nc)%num_nosnowc, & + ' nosnowc=', filter(nc)%nosnowc(1:filter(nc)%num_nosnowc) ! The following needs to be done after HydrologyNoDrainage (because it needs ! waterfluxbulk_inst%qflx_snwcp_ice_col), but before HydrologyDrainage (because diff --git a/src/utils/clmfates_interfaceMod.F90 b/src/utils/clmfates_interfaceMod.F90 index ac5b0b039d..7238efbea4 100644 --- a/src/utils/clmfates_interfaceMod.F90 +++ b/src/utils/clmfates_interfaceMod.F90 @@ -2901,6 +2901,10 @@ subroutine wrap_nvp_photosynthesis(this, nc, bounds, & min(1._r8, max(0.01_r8, (grc%dayl(g)**2) / (grc%max_dayl(g)**2))) end do + ! [DBG NVP sabg] column-level NVP inputs to photosynthesis + write(iulog,*) '[DBG NVP sabg] psn: c=', c, & + ' t_nvp_col=', temperature_inst%t_nvp_col(c), & + ' fwet_nvp_col=', waterdiagnosticbulk_inst%fwet_nvp_col(c) end do dtime = get_step_size_real() From 99fd16a64230e9b94e2a831840dff52024297172 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Mon, 1 Jun 2026 16:42:08 +0300 Subject: [PATCH 069/113] Exclude NVP layer from snow water diagnostics. --- src/biogeophys/WaterStateType.F90 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/biogeophys/WaterStateType.F90 b/src/biogeophys/WaterStateType.F90 index 8a666baf7f..1d5224b837 100644 --- a/src/biogeophys/WaterStateType.F90 +++ b/src/biogeophys/WaterStateType.F90 @@ -377,7 +377,8 @@ subroutine InitCold(this, bounds, & associate(snl => col%snl) this%h2osfc_col(bounds%begc:bounds%endc) = 0._r8 - ! [PORTED by Hui Tang: initialize nvp (moss/lichen) water content to 0] + ! [PORTED by Hui Tang: h2onvp_col diagnostic; actual h2osoi_liq(c,0) is set in + ! NVPLayerDynamicsMod UpdateNVPLayer appear branch (vwc=0.6)] this%h2onvp_col(bounds%begc:bounds%endc) = 0._r8 this%snocan_patch(bounds%begp:bounds%endp) = 0._r8 this%liqcan_patch(bounds%begp:bounds%endp) = 0._r8 @@ -920,6 +921,8 @@ subroutine CalculateTotalH2osno(this, & h2osno_total(c) = this%h2osno_no_layers_col(c) do j = col%snl(c)+1, 0 + ! [PORTED by Hui Tang: NVP at layer 0 is not snow; exclude from SWE total] + if (use_nvp .and. col%jbot_sno(c) == -1 .and. j == 0) cycle h2osno_total(c) = & h2osno_total(c) + & this%h2osoi_ice_col(c,j) + & @@ -965,6 +968,8 @@ subroutine CheckSnowConsistency(this, num_c, filter_c, caller) end if do j = -nlevsno+1, col%snl(c) + ! [PORTED by Hui Tang: NVP layer at j=0 legitimately holds water when snl=0] + if (use_nvp .and. col%jbot_sno(c) == -1 .and. j == 0) cycle ice_bad = (this%h2osoi_ice_col(c,j) /= 0._r8 .and. this%h2osoi_ice_col(c,j) /= spval) liq_bad = (this%h2osoi_liq_col(c,j) /= 0._r8 .and. this%h2osoi_liq_col(c,j) /= spval) if (ice_bad .or. liq_bad) then From 99b3c8041fd2eb10976f47a580b7a177b7015691 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Mon, 1 Jun 2026 16:43:45 +0300 Subject: [PATCH 070/113] Initialize fwet_nvp_col and vwc_nvp_col to 0.6 (WaterDiagnosticBulkType.F90). --- src/biogeophys/WaterDiagnosticBulkType.F90 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/biogeophys/WaterDiagnosticBulkType.F90 b/src/biogeophys/WaterDiagnosticBulkType.F90 index 8413ca2280..2020efb408 100644 --- a/src/biogeophys/WaterDiagnosticBulkType.F90 +++ b/src/biogeophys/WaterDiagnosticBulkType.F90 @@ -234,9 +234,9 @@ subroutine InitBulkAllocate(this, bounds) allocate(this%wf_col (begc:endc)) ; this%wf_col (:) = nan allocate(this%wf2_col (begc:endc)) ; this%wf2_col (:) = nan allocate(this%fwet_patch (begp:endp)) ; this%fwet_patch (:) = nan - ! [PORTED by Hui Tang: allocate nvp (moss/lichen) column wet fraction and VWC, initialized to 0] - allocate(this%fwet_nvp_col (begc:endc)) ; this%fwet_nvp_col (:) = 0.0_r8 - allocate(this%vwc_nvp_col (begc:endc)) ; this%vwc_nvp_col (:) = 0.0_r8 + ! [PORTED by Hui Tang: allocate nvp (moss/lichen) column wet fraction and VWC, initialized to 0.6] + allocate(this%fwet_nvp_col (begc:endc)) ; this%fwet_nvp_col (:) = 0.6_r8 + allocate(this%vwc_nvp_col (begc:endc)) ; this%vwc_nvp_col (:) = 0.6_r8 allocate(this%fcansno_patch (begp:endp)) ; this%fcansno_patch (:) = nan allocate(this%fdry_patch (begp:endp)) ; this%fdry_patch (:) = nan allocate(this%qflx_prec_intr_patch (begp:endp)) ; this%qflx_prec_intr_patch (:) = nan @@ -1023,7 +1023,7 @@ subroutine RestartBulk(this, bounds, ncid, flag, writing_finidat_interp_dest_fil units='proportion', & interpinic_flag='interp', readvar=readvar, data=this%fwet_nvp_col) if (flag == 'read' .and. .not. readvar) then - this%fwet_nvp_col(bounds%begc:bounds%endc) = 0.0_r8 + this%fwet_nvp_col(bounds%begc:bounds%endc) = 0.6_r8 end if end if From 6167e8f653ddd4735a3cb9d083aa2e07a9dca080 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Mon, 1 Jun 2026 16:45:37 +0300 Subject: [PATCH 071/113] Include NVP layer for water mass diagnostics when snow layer is absent. --- src/biogeophys/TotalWaterAndHeatMod.F90 | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/biogeophys/TotalWaterAndHeatMod.F90 b/src/biogeophys/TotalWaterAndHeatMod.F90 index 9bcee5b33a..cc2f1fecfb 100644 --- a/src/biogeophys/TotalWaterAndHeatMod.F90 +++ b/src/biogeophys/TotalWaterAndHeatMod.F90 @@ -300,6 +300,15 @@ subroutine ComputeLiqIceMassNonLake(bounds, num_nolakec, filter_nolakec, & ' liq=',h2osoi_liq(c,j),' ice=',h2osoi_ice(c,j), & ' cum_liq=',liquid_mass(c),' cum_ice=',ice_mass(c) end do + ! [PORTED by Hui Tang: when NVP is active and snl=0, the loop above (j=snl+1..0 = 1..0) + ! is an empty range in Fortran, so j=0 (NVP layer) water is never counted. + ! AccumulateSoilLiqIceMassNonLake also starts at j=1, so NVP is fully absent + ! from the water balance when snow is gone. Explicitly include it here.] + if (col%nvp_layer_active(c) .and. snl(c) == 0) then + liquid_mass(c) = liquid_mass(c) + h2osoi_liq(c,0) + ice_mass(c) = ice_mass(c) + h2osoi_ice(c,0) + end if + ! [PORTED by Hui Tang: NVP debug — print h2osno_no_layers and h2osfc after snow loop] if (use_nvp .and. col%jbot_sno(c) == -1 .and. c == bounds%begc) & write(iulog,*) '[NVP DBG] ComputeLiqIceMass c=',c,' snl=',snl(c), & From 7cc0a28fe12c31ad8e39d8e8487383a7c2a41c38 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Mon, 1 Jun 2026 17:00:39 +0300 Subject: [PATCH 072/113] Improve the accounting of qflx_in_soil and qflx_evap when NVP is present. --- src/biogeophys/SoilHydrologyMod.F90 | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/biogeophys/SoilHydrologyMod.F90 b/src/biogeophys/SoilHydrologyMod.F90 index f9d58c707a..b1bb34171e 100644 --- a/src/biogeophys/SoilHydrologyMod.F90 +++ b/src/biogeophys/SoilHydrologyMod.F90 @@ -356,9 +356,12 @@ subroutine SetQflxInputs(bounds, num_hydrologyc, filter_hydrologyc, & qflx_evap=qflx_ev_soil(c) endif - ! [PORTED by Hui Tang: NVP area fraction — zero when NVP buried under snow (snl < -1); - ! buried NVP receives no direct surface water and NVPWaterBalance returns zero fluxes] - if (use_nvp .and. snl(c) >= -1) then + ! [PORTED by Hui Tang: NVP area fraction — use real frac_nvp whenever NVP layer is + ! active (nvp_layer_active), including when buried under multiple snow layers. + ! When snow is present, qflx_top_soil includes qflx_snow_percolation(c,-1) which + ! already enters NVP via UpdateState_SnowPercolation; applying frac_nvp here prevents + ! that flux from also reaching qflx_in_soil for the NVP-covered fraction.] + if (use_nvp .and. col%nvp_layer_active(c)) then frac_nvp_eff = min(col%frac_nvp(c), max(0._r8, 1._r8 - frac_h2osfc(c))) else frac_nvp_eff = 0._r8 @@ -369,8 +372,18 @@ subroutine SetQflxInputs(bounds, num_hydrologyc, filter_hydrologyc, & (qflx_top_soil(c) - qflx_sat_excess_surf(c)) qflx_top_soil_to_h2osfc(c) = frac_h2osfc(c) * (qflx_top_soil(c) - qflx_sat_excess_surf(c)) + if (use_nvp .and. col%nvp_layer_active(c)) then + if (snl(c) >= -1) then + frac_nvp_eff = min(col%frac_nvp(c), max(0._r8, 1._r8 - frac_h2osfc(c)- fsno)) + else + frac_nvp_eff = min(col%frac_nvp(c), max(0._r8, 1._r8 - frac_h2osfc(c))) + end if + else + frac_nvp_eff = 0._r8 + end if + ! remove evaporation from bare-soil fraction only (snow and NVP evap handled separately) - qflx_in_soil(c) = qflx_in_soil(c) - (1.0_r8 - fsno - frac_h2osfc(c) - frac_nvp_eff)*qflx_evap + qflx_in_soil(c) = qflx_in_soil(c) - max(0._r8, 1.0_r8 - fsno - frac_h2osfc(c) - frac_nvp_eff)*qflx_evap qflx_top_soil_to_h2osfc(c) = qflx_top_soil_to_h2osfc(c) - frac_h2osfc(c) * qflx_ev_h2osfc(c) end do From bc251e3e4581858c102972a17b683fe0a8603efe Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Mon, 1 Jun 2026 17:21:51 +0300 Subject: [PATCH 073/113] Improve the accounting of NVP in lw_grnd, eflx_soil_grnd, and heat_store_diag --- src/biogeophys/SoilFluxesMod.F90 | 76 ++++++++++++++++++++++++-------- 1 file changed, 57 insertions(+), 19 deletions(-) diff --git a/src/biogeophys/SoilFluxesMod.F90 b/src/biogeophys/SoilFluxesMod.F90 index 740d53e26f..32b674bafb 100644 --- a/src/biogeophys/SoilFluxesMod.F90 +++ b/src/biogeophys/SoilFluxesMod.F90 @@ -78,6 +78,7 @@ subroutine SoilFluxes (bounds, num_urbanl, filter_urbanl, & real(r8) :: eflx_lwrad_del(bounds%begp:bounds%endp) ! update due to eflx_lwrad real(r8) :: t_grnd0(bounds%begc:bounds%endc) ! t_grnd of previous time step real(r8) :: lw_grnd + real(r8) :: frac_nvp_eff ! [PORTED by Hui Tang: effective NVP fraction for LW weighting] real(r8) :: evaporation_limit ! top layer moisture available for evaporation real(r8) :: evaporation_demand ! evaporative demand real(r8) :: heat_store_diag ! [PORTED by Hui Tang: errsoi diagnostic - heat storage sum] @@ -373,14 +374,41 @@ subroutine SoilFluxes (bounds, num_urbanl, filter_urbanl, & ! Ground heat flux if (.not. lun%urbpoi(l)) then - lw_grnd=(frac_sno_eff(c)*tssbef(c,col%snl(c)+1)**4 & - +(1._r8-frac_sno_eff(c)-frac_h2osfc(c))*tssbef(c,1)**4 & - +frac_h2osfc(c)*t_h2osfc_bef(c)**4) + ! [PORTED by Hui Tang: fix lw_grnd for NVP — area-weighted LW emission including NVP] + ! Standard formula uses frac_sno_eff/bare-soil/h2osfc fractions summing to 1. + ! When NVP is active (snl=0 or snl<0), NVP occupies frac_nvp_eff of the non-snow, + ! non-water ground and emits LW from tssbef(c,0). Four fractions sum to 1: + ! snow (frac_sno_eff) + NVP (frac_nvp_eff) + bare soil + surface water (frac_h2osfc) + ! When snl=0: frac_sno_eff=0 so the snow term vanishes and the formula reduces to + ! three terms (NVP + bare soil + water). + if (use_nvp .and. col%nvp_layer_active(c)) then + frac_nvp_eff = min(col%frac_nvp(c), max(0._r8, 1._r8 - frac_sno_eff(c) - frac_h2osfc(c))) + lw_grnd = frac_sno_eff(c) * tssbef(c,col%snl(c)+1)**4 & + + frac_nvp_eff * tssbef(c,0)**4 & + + (1._r8 - frac_sno_eff(c) - frac_nvp_eff - frac_h2osfc(c)) * tssbef(c,1)**4 & + + frac_h2osfc(c) * t_h2osfc_bef(c)**4 + else + lw_grnd=(frac_sno_eff(c)*tssbef(c,col%snl(c)+1)**4 & + +(1._r8-frac_sno_eff(c)-frac_h2osfc(c))*tssbef(c,1)**4 & + +frac_h2osfc(c)*t_h2osfc_bef(c)**4) + end if eflx_soil_grnd(p) = ((1._r8- frac_sno_eff(c))*sabg_soil(p) + frac_sno_eff(c)*sabg_snow(p)) + dlrad(p) & + (1-frac_veg_nosno(p))*emg(c)*forc_lwrad(c) & - emg(c)*sb*lw_grnd - emg(c)*sb*t_grnd0(c)**3*(4._r8*tinc(c)) & - (eflx_sh_grnd(p)+qflx_evap_soi(p)*htvp(c)) + ! [PORTED by Hui Tang: NVP solar absorption in eflx_soil_grnd] + ! SurfaceRadiationMod removes sabg_lyr(p,0) from sabg_soil (snl=0: line 781; + ! snl<0: sabg_soil=sabg_lyr(p,1) only); add it back so NVP absorbed solar is + ! counted as column energy input in both the exposed (snl=0) and buried (snl<0) cases. + ! NOTE: sabg_lyr(p,0) must NOT be multiplied by frac_nvp_eff. NVPBeerLawAbsorptance + ! (FatesRadiationDriveMod.F90) already includes nvp_frac in the absorptance: + ! fabd_nvp = nvp_frac * (1 - exp(-k * lai_nvp)) + ! so sabg_lyr(p,0) = fabd_nvp * trd + fabi_nvp * tri is already per unit ground area. + ! Applying frac_nvp_eff again would double-count the NVP coverage fraction. + if (use_nvp .and. col%nvp_layer_active(c)) then + eflx_soil_grnd(p) = eflx_soil_grnd(p) + sabg_lyr(p,0) + end if if (lun%itype(l) == istsoil .or. lun%itype(l) == istcrop) then eflx_soil_grnd_r(p) = eflx_soil_grnd(p) @@ -479,30 +507,25 @@ subroutine SoilFluxes (bounds, num_urbanl, filter_urbanl, & end do ! [PORTED by Hui Tang: NVP errsoi fix] - ! When NVP is active (snl=0, jbot_sno=-1), layer j=0 is the atmospheric boundary of - ! the soil column but is skipped by the loops above (condition j>=snl+1=1 fails for j=0). - ! Two corrections are needed: - ! (1) Add j=0 heat storage (the NVP layer temperature change). - ! (2) Replace the LHS boundary flux: eflx_soil_grnd uses the soil-surface (j=1) - ! terms, but the actual top-of-column boundary is the NVP surface (j=0). - ! We add (eflx_gnet_nvp - eflx_soil_grnd) to swap in the NVP surface budget. - ! eflx_gnet_nvp = sabg_lyr(p,0) + dlrad + (1-frac_veg_nosno)*emg*forc_lwrad - ! - emg*sb*tssbef(c,0)^4 - (eflx_sh_nvp + qflx_ev_nvp*htvp) - ! tssbef(c,0) is the pre-update NVP temperature, matching what SoilTemperatureMod - ! used in the BC when it computed lwrad_emit_nvp. + ! snl=0: j=0 is the atmospheric boundary (top BC = hs_nvp) and is skipped entirely + ! by the loops above (j>=snl+1=1 fails for j=0) → subtract full heat storage. + ! snl<0: j=0 is included in the snow loop with frac_sno_eff weight, but cv(c,0) is + ! per unit column area (unlike snow layers where cv is per unit snow area) → subtract + ! the missing (1-frac_sno_eff) fraction to account for the full column-area heat storage. if (use_nvp) then do fp = 1, num_nolakep p = filter_nolakep(fp) c = patch%column(p) if (col%nvp_layer_active(c) .and. col%snl(c) == 0) then + ! snl=0: j=0 skipped entirely; subtract full heat storage (weight = 1.0). errsoi_patch(p) = errsoi_patch(p) & - (t_soisno(c,0) - tssbef(c,0)) / fact(c,0) + else if (col%nvp_layer_active(c) .and. col%snl(c) < 0) then + ! snl<0: j=0 included with frac_sno_eff; cv(c,0) is per unit column area, + ! so subtract the missing (1-frac_sno_eff) fraction. + ! [PORTED by Hui Tang: NVP under-snow errsoi correction] errsoi_patch(p) = errsoi_patch(p) & - + (sabg_lyr(p,0) + dlrad(p) & - + (1 - frac_veg_nosno(p))*emg(c)*forc_lwrad(c) & - - emg(c)*sb*tssbef(c,0)**4 & - - (eflx_sh_nvp(p) + qflx_ev_nvp(p)*htvp(c))) & - - eflx_soil_grnd(p) + - (1.0_r8 - frac_sno_eff(c)) * (t_soisno(c,0) - tssbef(c,0)) / fact(c,0) end if end do end if @@ -522,6 +545,21 @@ subroutine SoilFluxes (bounds, num_urbanl, filter_urbanl, & (t_soisno(c,j)-tssbef(c,j))/fact(c,j) end if end do + ! [PORTED by Hui Tang: NVP j=0 correction to heat_store_diag — mirrors errsoi fix] + ! Weight is 1.0 not frac_nvp_eff: cv(c,0) is per unit column area so + ! (t_0-tbef_0)/fact(0) is already W/m2 column area. + ! snl=0: j=0 skipped by loop (j>=1 and j<1 impossible) → add full term. + ! snl<0: j=0 included with frac_sno_eff → add missing (1-frac_sno_eff) fraction. + if (use_nvp .and. col%nvp_layer_active(c)) then + if (col%snl(c) == 0) then + heat_store_diag = heat_store_diag + & + (t_soisno(c,0) - tssbef(c,0)) / fact(c,0) + else if (col%snl(c) < 0) then + heat_store_diag = heat_store_diag + & + (1.0_r8 - frac_sno_eff(c)) * (t_soisno(c,0) - tssbef(c,0)) / fact(c,0) + end if + end if + write(iulog,*) '[ERRSOI DBG] p=',p,' c=',c,' snl=',col%snl(c) write(iulog,*) ' errsoi_patch =', errsoi_patch(p) write(iulog,*) ' eflx_soil_grnd =', eflx_soil_grnd(p) From 7bc31269339396ed567e4a00255cde5dac428f46 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Mon, 1 Jun 2026 17:26:16 +0300 Subject: [PATCH 074/113] Exclude NVP layer for assigning snow aerosol value. --- src/biogeophys/SnowSnicarMod.F90 | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/biogeophys/SnowSnicarMod.F90 b/src/biogeophys/SnowSnicarMod.F90 index 5f71b67e54..1518189ea9 100644 --- a/src/biogeophys/SnowSnicarMod.F90 +++ b/src/biogeophys/SnowSnicarMod.F90 @@ -716,7 +716,9 @@ subroutine SNICAR_RT (bounds, num_nourbanc, filter_nourbanc, & if (nvp_tau_col(c_idx) > 0._r8) then nvp_active = .true. nvp_tau_lcl = nvp_tau_col(c_idx) - snl_btm = -1 + ! [PORTED by Hui Tang: do NOT shift snl_btm to -1; keep snl_btm=0 so NVP layer 0 + ! is included in all RT solver loops (tau/omega/g, delta-Eddington, absorbed flux). + ! Snow-grain-radius loops already guard layer 0 via merge(-1,snl_btm,nvp_active).] ! Populate layer-0 local arrays for NVP (bypass snow grain-radius path): ! Unit effective mass so tau_snw(0) = ext_cff_mss_snw_lcl(0) = nvp_tau_lcl h2osno_ice_lcl(0) = 1._r8 @@ -735,11 +737,14 @@ subroutine SNICAR_RT (bounds, num_nourbanc, filter_nourbanc, & ! Set local aerosol array (snow layers only; NVP layer 0 already zeroed above) - ! [PORTED by Hui Tang: use snl_btm (not min(snl_btm,0)-1) so that with NVP active - ! (snl_btm=-1) layer -1 (bottom snow) is included; min()-1 gave -2, skipping layer -1, - ! leaving mss_cnc_aer_lcl(-1,:) stale and corrupting g(-1) past 1 -> tau_star < 0 -> SIGFPE] + ! [PORTED by Hui Tang: use merge(-1,snl_btm,nvp_active) to stop the range-copy at -1 + ! when NVP is active, so the zeroed NVP aerosols at layer 0 are not overwritten by + ! mss_cnc_aer_in(0,:) which holds stale snow values. Without this guard, g(0) could + ! exceed 1 -> tau_star < 0 -> SIGFPE. When NVP is inactive, snl_btm=0 so the merge + ! gives 0 and the original layer-0 snow aerosol copy is preserved.] do j=1,sno_nbr_aer - mss_cnc_aer_lcl(snl_top:snl_btm,j) = mss_cnc_aer_in(c_idx,snl_top:snl_btm,j) + mss_cnc_aer_lcl(snl_top:merge(-1,snl_btm,nvp_active),j) = & + mss_cnc_aer_in(c_idx,snl_top:merge(-1,snl_btm,nvp_active),j) if (.not. nvp_active) then mss_cnc_aer_lcl(0,j) = mss_cnc_aer_in(c_idx,0,j) end if From d83482f1d7b9b7bab6f29b32206940aa902c3347 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Mon, 1 Jun 2026 17:36:08 +0300 Subject: [PATCH 075/113] Zero NVP sh/ev flux when buried under snow (Need improvements) --- src/biogeophys/BareGroundFluxesMod.F90 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/biogeophys/BareGroundFluxesMod.F90 b/src/biogeophys/BareGroundFluxesMod.F90 index 08cb2df1cc..2768f840c4 100644 --- a/src/biogeophys/BareGroundFluxesMod.F90 +++ b/src/biogeophys/BareGroundFluxesMod.F90 @@ -506,6 +506,8 @@ subroutine BareGroundFluxes(bounds, num_noexposedvegp, filter_noexposedvegp, & if (use_fates) then if (use_nvp .and. patch%is_veg(p) .and. col%frac_nvp(c) > 0._r8 .and. col%snl(c) >= -1) then eflx_sh_nvp(p) = -raih*(thm(p)-t_nvp_col(c)) + else + eflx_sh_nvp(p) = 0._r8 ! [PORTED by Hui Tang: zero NVP sh flux when buried under snow or condition unmet] end if else eflx_sh_nvp(p) = 0._r8 @@ -535,6 +537,8 @@ subroutine BareGroundFluxes(bounds, num_noexposedvegp, filter_noexposedvegp, & if (use_fates) then if (use_nvp .and. patch%is_veg(p) .and. col%frac_nvp(c) > 0._r8 .and. col%snl(c) >= -1) then qflx_ev_nvp(p) = -raiw*(forc_q(c) - qg_nvp(c)) + else + qflx_ev_nvp(p) = 0._r8 ! [PORTED by Hui Tang: zero NVP ev flux when buried under snow or condition unmet] end if else qflx_ev_nvp(p) = 0._r8 From 9b1d0c9714d19c36102a3a89c79fb8de2552dc53 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Mon, 1 Jun 2026 18:19:45 +0300 Subject: [PATCH 076/113] Add separated treatment for the water phase changes of NVP layer (key process, need further improvements). --- src/biogeophys/SoilTemperatureMod.F90 | 115 ++++++++++++++++++++++++-- 1 file changed, 110 insertions(+), 5 deletions(-) diff --git a/src/biogeophys/SoilTemperatureMod.F90 b/src/biogeophys/SoilTemperatureMod.F90 index a658e09482..044c2bc74c 100644 --- a/src/biogeophys/SoilTemperatureMod.F90 +++ b/src/biogeophys/SoilTemperatureMod.F90 @@ -1374,7 +1374,9 @@ subroutine Phasechange (bounds, num_nolakec, filter_nolakec, dhsdT, & fact => temperature_inst%fact_col , & imelt => temperature_inst%imelt_col , & ! Output: [integer (:,:) ] flag for melting (=1), freezing (=2), Not=0 (new) - t_soisno => temperature_inst%t_soisno_col & ! Output: [real(r8) (:,:) ] soil temperature [K] + t_soisno => temperature_inst%t_soisno_col , & ! Output: [real(r8) (:,:) ] soil temperature [K] + ! [PORTED by Hui Tang: t_nvp_col synced after NVP phase change modifies t_soisno(c,0)] + t_nvp_col => temperature_inst%t_nvp_col & ! Inout: [real(r8) (:) ] NVP temperature [K] ) ! Get step size @@ -1425,6 +1427,19 @@ subroutine Phasechange (bounds, num_nolakec, filter_nolakec, dhsdT, & do fc = 1,num_nolakec c = filter_nolakec(fc) if (j >= snl(c)+1) then + ! [PORTED by Hui Tang: NVP at j=0 — phase change identification (buried case, snl<0)] + ! NVP has no supercooled water; treat like snow (plain tfrz threshold). + ! The energy loop handles hm/T-correction for j=0 as an interior layer + ! (else branch: hm = -frac_sno_eff*tinc/fact ≈ -tinc/fact when frac_sno_eff≈1). + if (use_nvp .and. col%jbot_sno(c) == -1 .and. j == 0) then + if (h2osoi_ice(c,0) > 0._r8 .and. t_soisno(c,0) > tfrz) then + imelt(c,0) = 1 ; tinc(c,0) = tfrz - t_soisno(c,0) ; t_soisno(c,0) = tfrz + end if + if (h2osoi_liq(c,0) > 0._r8 .and. t_soisno(c,0) < tfrz) then + imelt(c,0) = 2 ; tinc(c,0) = tfrz - t_soisno(c,0) ; t_soisno(c,0) = tfrz + end if + cycle + end if ! Melting identification ! If ice exists above melt point, melt some to liquid. @@ -1445,7 +1460,33 @@ subroutine Phasechange (bounds, num_nolakec, filter_nolakec, dhsdT, & end do ! end of column-loop enddo ! end of level-loop - !-- soil layers --------------------------------------------------- + ! [PORTED by Hui Tang: NVP phase change identification — active case (snl=0)] + ! When snl=0 the snow loop runs j=snl+1=1 to 0, never reaching j=0. + ! Initialise wice0/wliq0/wmass0/imelt/tinc for j=0 and identify melt/freeze here. + if (use_nvp) then + do fc = 1, num_nolakec + c = filter_nolakec(fc) + if (col%jbot_sno(c) == -1 .and. col%snl(c) == 0) then + wice0(c,0) = h2osoi_ice(c,0) + wliq0(c,0) = h2osoi_liq(c,0) + wexice0(c,0) = 0._r8 + wmass0(c,0) = wice0(c,0) + wliq0(c,0) + imelt(c,0) = 0 + hm(c,0) = 0._r8 + xm(c,0) = 0._r8 + xm2(c,0) = 0._r8 + tinc(c,0) = 0._r8 + if (wice0(c,0) > 0._r8 .and. t_soisno(c,0) > tfrz) then + imelt(c,0) = 1 ; tinc(c,0) = tfrz - t_soisno(c,0) ; t_soisno(c,0) = tfrz + end if + if (wliq0(c,0) > 0._r8 .and. t_soisno(c,0) < tfrz) then + imelt(c,0) = 2 ; tinc(c,0) = tfrz - t_soisno(c,0) ; t_soisno(c,0) = tfrz + end if + end if + end do + end if + + !-- soil layers --------------------------------------------------- do j = 1,nlevmaxurbgrnd do fc = 1,num_nolakec c = filter_nolakec(fc) @@ -1532,7 +1573,15 @@ subroutine Phasechange (bounds, num_nolakec, filter_nolakec, dhsdT, & else if (j == 1) then hm(c,j) = (1.0_r8 - frac_sno_eff(c) - frac_h2osfc(c)) & *dhsdT(c)*tinc(c,j) - tinc(c,j)/fact(c,j) - else ! non-interfacial snow/soil layers + else if (use_nvp .and. col%jbot_sno(c) == -1 .and. j == 0) then + ! [PORTED by Hui Tang: NVP j=0 phase-change energy correction] + ! cv(c,0) is per unit COLUMN area (unlike snow layers where cv is + ! per unit snow area scaled by frac_sno). Using the standard snow + ! formula hm=-frac_sno_eff*tinc/fact understates hm by frac_sno_eff, + ! causing xmf to miss (1-frac_sno_eff)*tinc/fact of latent heat and + ! leaving a residual errsoi of that magnitude during phase-change events. + hm(c,0) = -tinc(c,0) / fact(c,0) + else ! non-interfacial snow/soil layers if(j < 1) then hm(c,j) = - frac_sno_eff(c)*(tinc(c,j)/fact(c,j)) else @@ -1631,6 +1680,10 @@ subroutine Phasechange (bounds, num_nolakec, filter_nolakec, dhsdT, & else if(j > 0) then t_soisno(c,j) = t_soisno(c,j) + fact(c,j)*heatr + else if (use_nvp .and. col%jbot_sno(c) == -1 .and. j == 0) then + ! [PORTED by Hui Tang: NVP j=0 T-correction — cv(c,0) is per unit + ! column area so no frac_sno_eff division; mirrors the hm fix above] + t_soisno(c,0) = t_soisno(c,0) + fact(c,0)*heatr else if(frac_sno_eff(c) > 0._r8) t_soisno(c,j) = t_soisno(c,j) + (fact(c,j)/frac_sno_eff(c))*heatr endif @@ -1650,14 +1703,18 @@ subroutine Phasechange (bounds, num_nolakec, filter_nolakec, dhsdT, & xmf(c) = xmf(c) + hfus*(wice0(c,j)-h2osoi_ice(c,j))/dtime endif - if (imelt(c,j) == 1 .AND. j < 1) then + ! [PORTED by Hui Tang: exclude NVP (j=0, jbot_sno=-1) — NVP melt/freeze + ! is not snow melt/freeze and must not feed qflx_snomelt/qflx_snofrz] + if (imelt(c,j) == 1 .AND. j < 1 .AND. & + .NOT. (use_nvp .AND. col%jbot_sno(c) == -1 .AND. j == 0)) then qflx_snomelt_lyr(c,j) = max(0._r8,(wice0(c,j)-h2osoi_ice(c,j)))/dtime qflx_snomelt(c) = qflx_snomelt(c) + qflx_snomelt_lyr(c,j) snomelt_accum(c) = snomelt_accum(c) + qflx_snomelt_lyr(c,j) * dtime * 1.e-3_r8 endif ! layer freezing mass flux (positive): - if (imelt(c,j) == 2 .AND. j < 1) then + if (imelt(c,j) == 2 .AND. j < 1 .AND. & + .NOT. (use_nvp .AND. col%jbot_sno(c) == -1 .AND. j == 0)) then qflx_snofrz_lyr(c,j) = max(0._r8,(h2osoi_ice(c,j)-wice0(c,j)))/dtime qflx_snofrz(c) = qflx_snofrz(c) + qflx_snofrz_lyr(c,j) endif @@ -1672,6 +1729,54 @@ subroutine Phasechange (bounds, num_nolakec, filter_nolakec, dhsdT, & enddo ! end of level-loop + ! [PORTED by Hui Tang: NVP phase change energy — active case (snl=0)] + ! Active NVP (j=0, snl=0) is the top layer of the thermal domain but is not reached + ! by the energy loop (condition j >= snl+1=1 fails for j=0). Apply the top-layer + ! energy formula: hm = dhsdT*tinc - tinc/fact (no frac_sno_eff since snl=0). + ! The residual-heat T correction mirrors the top-soil-layer formula. + if (use_nvp) then + do fc = 1, num_nolakec + c = filter_nolakec(fc) + if (col%jbot_sno(c) == -1 .and. col%snl(c) == 0 .and. imelt(c,0) > 0) then + hm(c,0) = dhsdT(c)*tinc(c,0) - tinc(c,0)/fact(c,0) + ! Tridiagonal error check (mirrors standard Phasechange logic) + if (imelt(c,0) == 1 .and. hm(c,0) < 0._r8) then + hm(c,0) = 0._r8 ; imelt(c,0) = 0 + end if + if (imelt(c,0) == 2 .and. hm(c,0) > 0._r8) then + hm(c,0) = 0._r8 ; imelt(c,0) = 0 + end if + if (imelt(c,0) > 0 .and. abs(hm(c,0)) > 0._r8) then + xm(c,0) = hm(c,0) * dtime / hfus + heatr = 0._r8 + if (xm(c,0) > 0._r8) then ! melting + h2osoi_ice(c,0) = max(0._r8, wice0(c,0) - xm(c,0)) + heatr = hm(c,0) - hfus*(wice0(c,0)-h2osoi_ice(c,0))/dtime + else if (xm(c,0) < 0._r8) then ! freezing + h2osoi_ice(c,0) = min(wmass0(c,0), wice0(c,0) - xm(c,0)) + heatr = hm(c,0) - hfus*(wice0(c,0)-h2osoi_ice(c,0))/dtime + end if + h2osoi_liq(c,0) = max(0._r8, wmass0(c,0) - h2osoi_ice(c,0)) + if (abs(heatr) > 0._r8) then + ! Top-layer T correction (no frac_sno_eff or frac_h2osfc for NVP) + t_soisno(c,0) = t_soisno(c,0) + fact(c,0)*heatr & + / (1._r8 - fact(c,0)*dhsdT(c)) + end if + if (h2osoi_liq(c,0)*h2osoi_ice(c,0) > 0._r8) t_soisno(c,0) = tfrz + xmf(c) = xmf(c) + hfus*(wice0(c,0)-h2osoi_ice(c,0))/dtime + end if + end if + end do + end if + + ! [PORTED by Hui Tang: sync t_nvp_col after NVP phase change may have modified t_soisno(c,0)] + if (use_nvp) then + do fc = 1, num_nolakec + c = filter_nolakec(fc) + if (col%jbot_sno(c) == -1) t_nvp_col(c) = t_soisno(c,0) + end do + end if + ! Needed for history file output do fc = 1,num_nolakec From 9ce6316d3e72ae5160f3aca5debee2b0b6623072 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Mon, 1 Jun 2026 18:23:54 +0300 Subject: [PATCH 077/113] Add specific treatment in ComputeGroundHeatFluxAndDeriv to deal with NVP layer as the top surface layer without snow. --- src/biogeophys/SoilTemperatureMod.F90 | 41 +++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/src/biogeophys/SoilTemperatureMod.F90 b/src/biogeophys/SoilTemperatureMod.F90 index 044c2bc74c..5f31110e80 100644 --- a/src/biogeophys/SoilTemperatureMod.F90 +++ b/src/biogeophys/SoilTemperatureMod.F90 @@ -2061,12 +2061,26 @@ subroutine ComputeGroundHeatFluxAndDeriv(bounds, & ! [PORTED by Hui Tang: accumulate NVP surface BC and layer-0 solar absorption] ! NVP layer 0 is above soil layer 1 when active (jbot_sno=-1, snl=0). ! Its surface BC uses sabg_lyr(p,0) and NVP-specific turbulent/LW fluxes. + ! When snow covers NVP (snl<0), layer 0 is an internal snow layer in the + ! tridiagonal solver; the main loop (do j=lyr_top,1) already accumulates + ! sabg_lyr_col(c,0), so we must NOT add it again here or it is double-counted. if (col%nvp_layer_active(c)) then eflx_gnet_nvp = sabg_lyr(p,0) + dlrad(p) & + (1._r8-frac_veg_nosno(p))*emg(c)*forc_lwrad(c) - lwrad_emit_nvp(c) & - (eflx_sh_nvp(p) + qflx_ev_nvp(p)*htvp(c)) hs_nvp(c) = hs_nvp(c) + eflx_gnet_nvp * patch%wtcol(p) - sabg_lyr_col(c,0) = sabg_lyr_col(c,0) + sabg_lyr(p,0) * patch%wtcol(p) + ! [PORTED by Hui Tang: only accumulate sabg_lyr_col(c,0) here when there is + ! no snow (snl==0). When snow covers NVP (snl<0), the do j=lyr_top,1 loop + ! above already added sabg_lyr(p,0); adding it again would double-count it, + ! causing errsoi = -sabg_lyr(p,0).] + if (snl(c) == 0) then + sabg_lyr_col(c,0) = sabg_lyr_col(c,0) + sabg_lyr(p,0) * patch%wtcol(p) + end if + ! [DBG NVP sabg] solar absorbed by NVP and total surface energy flux + write(iulog,*) '[DBG NVP sabg] p=', p, ' c=', c, & + ' sabg_lyr(p,0)=', sabg_lyr(p,0), & + ' eflx_gnet_nvp=', eflx_gnet_nvp, & + ' hs_nvp(c)=', hs_nvp(c) end if else @@ -2992,6 +3006,17 @@ subroutine SetMatrix_Snow(bounds, num_nolakec, filter_nolakec, nband, & else !if ( j == 0) bmatrix_snow_soil(c,1,j-1) = - (1._r8-cnfac)*fact(c,j)* tk(c,j)/dzp end if + ! [PORTED by Hui Tang: NVP top-layer BC in SetMatrix_Snow] + ! With snl=0, condition j>=snl+1=j>=1 never fires for j=0, leaving + ! bmatrix_snow(c,:,-1) all zeros -> singular matrix (dgbsv info=1). + ! Explicitly set the NVP surface BC here: j=0 is the top of the system + ! with no layer above and coupled to soil j=1 below. + else if (use_nvp .and. col%jbot_sno(c) == -1 .and. j == 0) then + dzp = z(c,j+1) - z(c,j) + bmatrix_snow(c,4,j-1) = 0._r8 + bmatrix_snow(c,3,j-1) = 1._r8 + (1._r8-cnfac)*fact(c,j)*tk(c,j)/dzp & + - fact(c,j)*dhsdT(c) + bmatrix_snow_soil(c,1,j-1) = -(1._r8-cnfac)*fact(c,j)*tk(c,j)/dzp end if enddo end do @@ -3114,15 +3139,25 @@ subroutine SetMatrix_Soil(bounds, num_nolakec, filter_nolakec, nband, & (col%itype(c) == icol_road_perv) .or. & (.not. lun%urbpoi(l))) then - if (j == col%snl(c)+1) then + ! [PORTED by Hui Tang: exclude NVP — when NVP active, j=1 is interior not top] + if (j == col%snl(c)+1 .and. .not. (use_nvp .and. col%jbot_sno(c) == -1)) then dzp = z(c,j+1)-z(c,j) if (j /= 1) then bmatrix_soil(c,4,j) = 0._r8 - else + else bmatrix_soil_snow(c,5,j) = 0._r8 end if bmatrix_soil(c,3,j) = 1._r8+(1._r8-cnfac)*fact(c,j)*tk(c,j)/dzp-fact(c,j)*dhsdT(c) bmatrix_soil(c,2,j) = -(1._r8-cnfac)*fact(c,j)*tk(c,j)/dzp + ! [PORTED by Hui Tang: j=1 below NVP is interior — full conduction coupling to NVP at j=0] + ! frac_sno_eff=0 when snl=0 so the standard snow/soil branch would give zero coupling; + ! NVP is always fully present above j=1, so use full conductance (no dhsdT term). + else if (j == 1 .and. use_nvp .and. col%jbot_sno(c) == -1 .and. col%snl(c) == 0) then + dzm = (z(c,j)-z(c,j-1)) + dzp = (z(c,j+1)-z(c,j)) + bmatrix_soil(c,2,j) = -(1._r8-cnfac)*fact(c,j)*tk(c,j)/dzp + bmatrix_soil(c,3,j) = 1._r8 + (1._r8-cnfac)*fact(c,j)*(tk(c,j)/dzp + tk(c,j-1)/dzm) + bmatrix_soil_snow(c,5,j) = -(1._r8-cnfac)*fact(c,j)*tk(c,j-1)/dzm else if (j == 1) then ! this is the snow/soil interface layer dzm = (z(c,j)-z(c,j-1)) From b59064e6e7c24f6c64d86626884e1524b286ef10 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Mon, 1 Jun 2026 18:40:59 +0300 Subject: [PATCH 078/113] Place first snow layer at index -1, when NVP occupies layer 0. Apply same rule to other snow dynamics (SnowCompaction and CombineSnowLayers) (need improvements). --- src/biogeophys/SnowHydrologyMod.F90 | 78 +++++++++++++++++++++-------- 1 file changed, 56 insertions(+), 22 deletions(-) diff --git a/src/biogeophys/SnowHydrologyMod.F90 b/src/biogeophys/SnowHydrologyMod.F90 index 18784c0307..ae183fe0ee 100644 --- a/src/biogeophys/SnowHydrologyMod.F90 +++ b/src/biogeophys/SnowHydrologyMod.F90 @@ -944,8 +944,14 @@ subroutine UpdateState_InitializeSnowPack(bounds, snowpack_initialized_filterc, do fc = 1, snowpack_initialized_filterc%num c = snowpack_initialized_filterc%indices(fc) - h2osoi_ice(c,0) = h2osno_no_layers(c) - h2osoi_liq(c,0) = 0._r8 + ! [PORTED by Hui Tang: when NVP occupies layer 0, place first snow ice at layer -1] + if (use_nvp .and. col%jbot_sno(c) == -1) then + h2osoi_ice(c,-1) = h2osno_no_layers(c) + h2osoi_liq(c,-1) = 0._r8 + else + h2osoi_ice(c,0) = h2osno_no_layers(c) + h2osoi_liq(c,0) = 0._r8 + end if h2osno_no_layers(c) = 0._r8 end do @@ -994,17 +1000,28 @@ subroutine Bulk_InitializeSnowPack(bounds, snowpack_initialized_filterc, & do fc = 1, snowpack_initialized_filterc%num c = snowpack_initialized_filterc%indices(fc) - snl(c) = -1 - dz(c,0) = snow_depth(c) - z(c,0) = -0.5_r8*dz(c,0) - zi(c,-1) = -dz(c,0) - ! Currently, the water temperature for the precipitation is simply set - ! as the surface air temperature - t_soisno(c,0) = min(tfrz, forc_t(c)) - - ! This value of frac_iceold makes sense together with the state initialization: - ! h2osoi_ice is non-zero, while h2osoi_liq is zero. - frac_iceold(c,0) = 1._r8 + ! [PORTED by Hui Tang: when NVP occupies layer 0, place first snow layer at index -1 + ! so that dz/z/zi/t/frac_iceold for the NVP slot are preserved intact.] + if (use_nvp .and. col%jbot_sno(c) == -1) then + snl(c) = -2 + dz(c,-1) = snow_depth(c) + z(c,-1) = -(dz(c,0) + 0.5_r8*dz(c,-1)) + zi(c,-2) = -(dz(c,0) + dz(c,-1)) + ! zi(c,-1) = -dz(c,0) is already set by UpdateNVPLayer + t_soisno(c,-1) = min(tfrz, forc_t(c)) + frac_iceold(c,-1) = 1._r8 + else + snl(c) = -1 + dz(c,0) = snow_depth(c) + z(c,0) = -0.5_r8*dz(c,0) + zi(c,-1) = -dz(c,0) + ! Currently, the water temperature for the precipitation is simply set + ! as the surface air temperature + t_soisno(c,0) = min(tfrz, forc_t(c)) + ! This value of frac_iceold makes sense together with the state initialization: + ! h2osoi_ice is non-zero, while h2osoi_liq is zero. + frac_iceold(c,0) = 1._r8 + end if snomelt_accum(c) = 0._r8 end do @@ -1043,6 +1060,8 @@ subroutine SnowWater(bounds, & integer :: g ! gridcell loop index integer :: c, j, fc, l ! do loop/array indices real(r8) :: dtime ! land model time step (sec) + ! [PORTED by Hui Tang: per-column bottom percolation flux; uses j=-1 when NVP active, j=0 otherwise] + real(r8) :: qflx_snow_percolation_bottom_tmp(bounds%begc:bounds%endc) !----------------------------------------------------------------------- associate( & @@ -1081,6 +1100,7 @@ subroutine SnowWater(bounds, & ! Inputs dtime = dtime, & snl = col%snl(begc:endc), & + jbot_sno = col%jbot_sno(begc:endc), & dz = col%dz(begc:endc,:), & frac_sno_eff = b_waterdiagnostic_inst%frac_sno_eff_col(begc:endc), & h2osoi_ice = b_waterstate_inst%h2osoi_ice_col(begc:endc,:), & @@ -1978,6 +1998,8 @@ subroutine SnowCompaction(bounds, num_snowc, filter_snowc, & c = filter_snowc(fc) g = col%gridcell(c) if (j >= snl(c)+1) then + ! [PORTED by Hui Tang: NVP at layer 0 is not snow; skip compaction for NVP layer] + if (use_nvp .and. col%jbot_sno(c) == -1 .and. j == 0) cycle wx = (h2osoi_ice(c,j) + h2osoi_liq(c,j)) void = 1._r8 - (h2osoi_ice(c,j)/denice + h2osoi_liq(c,j)/denh2o)& @@ -2243,7 +2265,11 @@ subroutine CombineSnowLayers(bounds, num_snowc, filter_snowc, & end if if (j < 0) then - dz(c,j+1) = dz(c,j+1) + dz(c,j) + ! [PORTED by Hui Tang: do not extend NVP layer when dissolving the bottom + ! snow layer (j=-1) into it; only merge dz for true snow-on-snow cases] + if (.not. (use_nvp .and. col%jbot_sno(c) == -1 .and. j+1 == 0)) then + dz(c,j+1) = dz(c,j+1) + dz(c,j) + end if mss_bcphi(c,j+1) = mss_bcphi(c,j+1) + mss_bcphi(c,j) mss_bcpho(c,j+1) = mss_bcpho(c,j+1) + mss_bcpho(c,j) @@ -2301,6 +2327,9 @@ subroutine CombineSnowLayers(bounds, num_snowc, filter_snowc, & end do end if snl(c) = snl(c) + 1 + ! [PORTED by Hui Tang: after dissolving the last snow layer above NVP, + ! snl=-1 must be reset to 0; NVP is not a snow layer (jbot_sno=-1 tracks it)] + if (use_nvp .and. col%jbot_sno(c) == -1 .and. snl(c) == -1) snl(c) = 0 end if end do end do @@ -2375,12 +2404,9 @@ subroutine CombineSnowLayers(bounds, num_snowc, filter_snowc, & end associate end do - ! [PORTED by Hui Tang: preserve NVP slot in snl when all snow is gone] - if (use_nvp .and. col%jbot_sno(c) == -1) then - snl(c) = -1 - else - snl(c) = 0 - end if + ! [PORTED by Hui Tang: NVP occupies layer 0 but is not a snow layer; jbot_sno=-1 + ! tracks the NVP slot. snl=0 is correct for "no real snow" regardless of NVP.] + snl(c) = 0 h2osno_total(c) = h2osno_no_layers_bulk(c) mss_bcphi(c,:) = 0._r8 @@ -2426,8 +2452,10 @@ subroutine CombineSnowLayers(bounds, num_snowc, filter_snowc, & if (i == snl(c)+1) then ! If top node is removed, combine with bottom neighbor. neibor = i + 1 - else if (i == 0) then - ! If the bottom neighbor is not snow, combine with the top neighbor. + else if (i == 0 .or. & + (use_nvp .and. col%jbot_sno(c) == -1 .and. i == -1)) then + ! If the bottom neighbor is not snow (soil or NVP), combine with top. + ! [PORTED by Hui Tang: i==-1 is the bottom snow layer when NVP at 0] neibor = i - 1 else ! If none of the above special cases apply, combine with the thinnest neighbor @@ -3002,6 +3030,12 @@ subroutine ZeroEmptySnowLayers(bounds, num_snowc, filter_snowc, & do j = -nlevsno+1,0 do fc = 1, num_snowc c = filter_snowc(fc) + ! [PORTED by Hui Tang: protect NVP layer at j=0 from being zeroed] + ! filter_snowc may include a column whose snl was -1 when the filter was built + ! but has since been set to 0 (last snow layer emptied). With snl=0, the + ! condition j<=snl gives 0<=0=.true., which would incorrectly zero the NVP + ! layer at j=0. Skip j=0 when the NVP layer is active. + if (use_nvp .and. col%jbot_sno(c) == -1 .and. j == 0) cycle if (j <= snl(c) .and. snl(c) > -nlevsno) then do wi = water_inst%bulk_and_tracers_beg, water_inst%bulk_and_tracers_end associate(w => water_inst%bulk_and_tracers(wi)) From 226a5c449935046f2957fe99935b469dc0f1f7fa Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Mon, 1 Jun 2026 18:43:17 +0300 Subject: [PATCH 079/113] Re-wire snow percolation fluxes when NVP layer is present. --- src/biogeophys/SnowHydrologyMod.F90 | 45 +++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/src/biogeophys/SnowHydrologyMod.F90 b/src/biogeophys/SnowHydrologyMod.F90 index ae183fe0ee..36e73ee102 100644 --- a/src/biogeophys/SnowHydrologyMod.F90 +++ b/src/biogeophys/SnowHydrologyMod.F90 @@ -1168,13 +1168,23 @@ subroutine SnowWater(bounds, & do i = water_inst%bulk_and_tracers_beg, water_inst%bulk_and_tracers_end associate(w => water_inst%bulk_and_tracers(i)) + ! [PORTED by Hui Tang: when NVP is active jbot_sno=-1; the bottom snow percolation + ! exits at j=-1 (into NVP), not j=0. Use j=0 for non-NVP columns as before.] + do c = begc, endc + if (col%nvp_layer_active(c)) then + qflx_snow_percolation_bottom_tmp(c) = w%waterflux_inst%qflx_snow_percolation_col(c, -1) + else + qflx_snow_percolation_bottom_tmp(c) = w%waterflux_inst%qflx_snow_percolation_col(c, 0) + end if + end do call SumFlux_AddSnowPercolation(bounds, & num_snowc, filter_snowc, num_nosnowc, filter_nosnowc, & ! Inputs frac_sno_eff = b_waterdiagnostic_inst%frac_sno_eff_col(begc:endc), & - qflx_snow_percolation_bottom = w%waterflux_inst%qflx_snow_percolation_col(begc:endc, 0), & + qflx_snow_percolation_bottom = qflx_snow_percolation_bottom_tmp(begc:endc), & qflx_liq_grnd = w%waterflux_inst%qflx_liq_grnd_col(begc:endc), & qflx_snomelt = w%waterflux_inst%qflx_snomelt_col(begc:endc), & + nvp_layer_active = col%nvp_layer_active(begc:endc), & ! Outputs qflx_snow_drain = w%waterflux_inst%qflx_snow_drain_col(begc:endc), & qflx_rain_plus_snomelt = w%waterflux_inst%qflx_rain_plus_snomelt_col(begc:endc)) @@ -1324,7 +1334,7 @@ end subroutine UpdateState_TopLayerFluxes !----------------------------------------------------------------------- subroutine BulkFlux_SnowPercolation(bounds, num_snowc, filter_snowc, & - dtime, snl, dz, frac_sno_eff, h2osoi_ice, h2osoi_liq, & + dtime, snl, jbot_sno, dz, frac_sno_eff, h2osoi_ice, h2osoi_liq, & qflx_snow_percolation) ! ! !DESCRIPTION: @@ -1340,6 +1350,9 @@ subroutine BulkFlux_SnowPercolation(bounds, num_snowc, filter_snowc, & real(r8) , intent(in) :: dtime ! land model time step (sec) integer , intent(in) :: snl( bounds%begc: ) ! negative number of snow layers + ! [PORTED by Hui Tang: jbot_sno is 0 when no NVP, -1 when NVP occupies index 0; + ! used to stop percolation at the bottom snow layer and skip the NVP layer (j=0)] + integer , intent(in) :: jbot_sno( bounds%begc: ) ! bottom index of active snow layers (0 or -1) real(r8) , intent(in) :: dz( bounds%begc: , -nlevsno+1: ) ! layer depth (m) real(r8) , intent(in) :: frac_sno_eff( bounds%begc: ) ! eff. fraction of ground covered by snow (0 to 1) real(r8) , intent(in) :: h2osoi_ice( bounds%begc: , -nlevsno+1: ) ! ice lens (kg/m2) @@ -1401,7 +1414,12 @@ subroutine BulkFlux_SnowPercolation(bounds, num_snowc, filter_snowc, & do fc = 1, num_snowc c = filter_snowc(fc) if (j >= snl(c)+1) then - if (j <= -1) then + ! [PORTED by Hui Tang: use jbot_sno(c) instead of hard-coded -1 so that the + ! NVP layer at j=0 is excluded when nvp_layer_active. + ! j < jbot_sno: interior snow layer — capacity-limited (next layer is also snow). + ! j == jbot_sno: bottom snow layer — uncapped gravity drainage (next is NVP or soil). + ! j > jbot_sno: NVP layer (only j=0 when nvp_layer_active) — zero; not a snow layer.] + if (j < jbot_sno(c)) then ! No runoff over snow surface, just ponding on surface if (eff_porosity(c,j) < params_inst%wimp .OR. eff_porosity(c,j+1) < params_inst%wimp) then qflx_snow_percolation(c,j) = 0._r8 @@ -1412,9 +1430,12 @@ subroutine BulkFlux_SnowPercolation(bounds, num_snowc, filter_snowc, & qflx_snow_percolation(c,j) = min(qflx_snow_percolation(c,j),(1._r8-vol_ice(c,j+1) & - vol_liq(c,j+1))*dz(c,j+1)*frac_sno_eff(c)) end if - else + else if (j == jbot_sno(c)) then qflx_snow_percolation(c,j) = max(0._r8,(vol_liq(c,j) & - params_inst%ssi*eff_porosity(c,j))*dz(c,j)*frac_sno_eff(c)) + else + ! j > jbot_sno(c): NVP layer at j=0; not a snow layer + qflx_snow_percolation(c,j) = 0._r8 end if qflx_snow_percolation(c,j) = (qflx_snow_percolation(c,j)*1000._r8)/dtime end if @@ -1858,6 +1879,7 @@ end subroutine BulkDiag_SnowWaterAccumulatedSnow subroutine SumFlux_AddSnowPercolation(bounds, & num_snowc, filter_snowc, num_nosnowc, filter_nosnowc, & frac_sno_eff, qflx_snow_percolation_bottom, qflx_liq_grnd, qflx_snomelt, & + nvp_layer_active, & qflx_snow_drain, qflx_rain_plus_snomelt) ! ! !DESCRIPTION: @@ -1874,6 +1896,10 @@ subroutine SumFlux_AddSnowPercolation(bounds, & real(r8) , intent(in) :: qflx_snow_percolation_bottom( bounds%begc: ) ! liquid percolation out of the bottom of the snow pack (mm H2O /s) real(r8) , intent(in) :: qflx_liq_grnd( bounds%begc: ) ! liquid on ground after interception (mm H2O/s) real(r8) , intent(in) :: qflx_snomelt( bounds%begc: ) ! snow melt (mm H2O /s) + ! [PORTED by Hui Tang: when NVP occupies j=0, percolation from the bottom snow layer + ! (j=-1) goes to NVP, not the soil surface; exclude it from qflx_rain_plus_snomelt + ! to prevent bogus soil input and column water balance error] + logical , intent(in) :: nvp_layer_active( bounds%begc: ) ! .true. when NVP layer is active at j=0 real(r8) , intent(inout) :: qflx_snow_drain( bounds%begc: ) ! drainage from snow pack from previous time step (mm H2O/s) real(r8) , intent(inout) :: qflx_rain_plus_snomelt( bounds%begc: ) ! rain plus snow melt falling on the soil (mm/s) @@ -1894,9 +1920,16 @@ subroutine SumFlux_AddSnowPercolation(bounds, & do fc = 1, num_snowc c = filter_snowc(fc) + ! Always register percolation as a snow drain (fixes snow balance) qflx_snow_drain(c) = qflx_snow_drain(c) + qflx_snow_percolation_bottom(c) - qflx_rain_plus_snomelt(c) = qflx_snow_percolation_bottom(c) & - + (1.0_r8 - frac_sno_eff(c)) * qflx_liq_grnd(c) + if (nvp_layer_active(c)) then + ! Percolation exits j=-1 into NVP (j=0), not the soil surface; exclude from + ! qflx_rain_plus_snomelt so it is not double-counted as soil input / surface runoff. + qflx_rain_plus_snomelt(c) = (1.0_r8 - frac_sno_eff(c)) * qflx_liq_grnd(c) + else + qflx_rain_plus_snomelt(c) = qflx_snow_percolation_bottom(c) & + + (1.0_r8 - frac_sno_eff(c)) * qflx_liq_grnd(c) + end if end do do fc = 1, num_nosnowc From 9d40fc369464480db18ea83200d31d028dcacd71 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Mon, 1 Jun 2026 18:48:52 +0300 Subject: [PATCH 080/113] Add more debugging output in BalanceCheckMod.F90. --- src/biogeophys/BalanceCheckMod.F90 | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/biogeophys/BalanceCheckMod.F90 b/src/biogeophys/BalanceCheckMod.F90 index cf9e6740ad..765f6e66de 100644 --- a/src/biogeophys/BalanceCheckMod.F90 +++ b/src/biogeophys/BalanceCheckMod.F90 @@ -26,6 +26,7 @@ module BalanceCheckMod use Waterlnd2atmType , only : waterlnd2atm_type use WaterBalanceType , only : waterbalance_type use WaterFluxType , only : waterflux_type + use WaterFluxBulkType , only : waterfluxbulk_type ! [PORTED by Hui Tang: needed to access qflx_nvp_drain_col] use WaterType , only : water_type use TotalWaterAndHeatMod, only : ComputeWaterMassNonLake, ComputeWaterMassLake use GridcellType , only : grc @@ -502,6 +503,8 @@ subroutine BalanceCheck( bounds, & real(r8) :: errh2o_max_val ! Maximum value of error in water conservation error over all columns [mm H2O] real(r8) :: errh2osno_max_val ! Maximum value of error in h2osno conservation error over all columns [kg m-2] + ! [PORTED by Hui Tang: typed pointer to access qflx_nvp_drain_col for NVP snow balance correction] + type(waterfluxbulk_type), pointer :: waterfluxbulk_ptr real(r8), parameter :: h2o_warning_thresh = 1.e-9_r8 ! Warning threshhold for error in errh2o and errh2osnow @@ -561,6 +564,15 @@ subroutine BalanceCheck( bounds, & qflx_glcice_dyn_water_flux_col => waterflux_inst%qflx_glcice_dyn_water_flux_col & ! Input: [real(r8) (:)] column level water flux needed for balance check due to glc_dyn_runoff_routing (mm H2O/s) (positive means addition of water to the system) ) + ! [PORTED by Hui Tang: resolve the polymorphic waterflux_inst to its bulk concrete type so + ! that qflx_nvp_drain_col can be read for the NVP snow-balance correction below. + ! waterfluxbulk_ptr is null() if waterflux_inst is not a waterfluxbulk_type (safe fallback).] + waterfluxbulk_ptr => null() + select type(waterflux_inst) + type is (waterfluxbulk_type) + waterfluxbulk_ptr => waterflux_inst + end select + ! Get step size and time step dtime = get_step_size_real() nstep = get_nstep() @@ -801,7 +813,24 @@ subroutine BalanceCheck( bounds, & + qflx_snow_drain(c) + qflx_sl_top_soil(c) endif + ! [PORTED by Hui Tang: when NVP is active, h2osno includes h2osoi_liq(c,0) (NVP), + ! but qflx_nvp_drain_col (NVP→soil drainage) is not a registered snow sink. + ! Add it here so the balance closes. Signed: positive=NVP drains to soil (sink), + ! negative=soil absorbs into NVP (source, reduces snow_sinks). Both cases correct.] + !if (associated(waterfluxbulk_ptr) .and. col%nvp_layer_active(c)) then + ! snow_sinks(c) = snow_sinks(c) + waterfluxbulk_ptr%qflx_nvp_drain_col(c) + !end if + errh2osno(c) = (h2osno_total(c) - h2osno_old(c)) - (snow_sources(c) - snow_sinks(c)) * dtime + + ! [PORTED by Hui Tang: CalculateTotalH2osno excludes j=0 (NVP) from h2osno_total, + ! so liquid that percolated from the bottom snow layer into the NVP layer this + ! timestep is absent from both h2osno_total and snow_sinks. Add h2osoi_liq(c,0) + ! directly to close the balance.] + !if (col%nvp_layer_active(c)) then + ! errh2osno(c) = errh2osno(c) + waterstate_inst%h2osoi_liq_col(c,0) # Not working, as old and new status of NVP liq water is needed. + !end if + else snow_sources(c) = 0._r8 snow_sinks(c) = 0._r8 From ebd27d4bbc0faccdd2b71177590468a0e77b3699 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Mon, 1 Jun 2026 18:50:06 +0300 Subject: [PATCH 081/113] Avoid endrun when balance errors occur (Only for debugging). --- src/biogeophys/BalanceCheckMod.F90 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/biogeophys/BalanceCheckMod.F90 b/src/biogeophys/BalanceCheckMod.F90 index 765f6e66de..597c26d9c5 100644 --- a/src/biogeophys/BalanceCheckMod.F90 +++ b/src/biogeophys/BalanceCheckMod.F90 @@ -676,7 +676,7 @@ subroutine BalanceCheck( bounds, & end if write(iulog,*)'CTSM is stopping' - call endrun(subgrid_index=indexc, subgrid_level=subgrid_level_column, msg=errmsg(sourcefile, __LINE__)) + !call endrun(subgrid_index=indexc, subgrid_level=subgrid_level_column, msg=errmsg(sourcefile, __LINE__)) end if end if @@ -761,7 +761,7 @@ subroutine BalanceCheck( bounds, & write(iulog,*)'qflx_glcice_dyn_water_flux = ',qflx_glcice_dyn_water_flux_grc(indexg)*dtime write(iulog,*)'CTSM is stopping' - call endrun(subgrid_index=indexg, subgrid_level=subgrid_level_gridcell, msg=errmsg(sourcefile, __LINE__)) + !call endrun(subgrid_index=indexg, subgrid_level=subgrid_level_gridcell, msg=errmsg(sourcefile, __LINE__)) end if end if @@ -880,7 +880,7 @@ subroutine BalanceCheck( bounds, & write(iulog,*)'qflx_snwcp_discarded_liq = ',qflx_snwcp_discarded_liq_col(indexc)*dtime write(iulog,*)'qflx_sl_top_soil = ',qflx_sl_top_soil(indexc)*dtime write(iulog,*)'CTSM is stopping' - call endrun(subgrid_index=indexc, subgrid_level=subgrid_level_column, msg=errmsg(sourcefile, __LINE__)) + !call endrun(subgrid_index=indexc, subgrid_level=subgrid_level_column, msg=errmsg(sourcefile, __LINE__)) end if end if @@ -1077,7 +1077,7 @@ subroutine EnergyBalanceCheck( bounds, & write(iulog,*)'CTSM is stopping' - call endrun(subgrid_index=indexp, subgrid_level=subgrid_level_patch, msg=errmsg(sourcefile, __LINE__)) + !call endrun(subgrid_index=indexp, subgrid_level=subgrid_level_patch, msg=errmsg(sourcefile, __LINE__)) end if end if @@ -1131,7 +1131,7 @@ subroutine EnergyBalanceCheck( bounds, & write(iulog,*)'ftii ftdd ftid = ' ,ftii(indexp,:), ftdd(indexp,:),ftid(indexp,:) write(iulog,*)'elai esai = ' ,elai(indexp), esai(indexp) write(iulog,*)'CTSM is stopping' - call endrun(subgrid_index=indexp, subgrid_level=subgrid_level_patch, msg=errmsg(sourcefile, __LINE__)) + !call endrun(subgrid_index=indexp, subgrid_level=subgrid_level_patch, msg=errmsg(sourcefile, __LINE__)) end if end if @@ -1148,7 +1148,7 @@ subroutine EnergyBalanceCheck( bounds, & if ((errsoi_col_max_val > 1.e-4_r8) .and. (DAnstep > skip_steps)) then write(iulog,*)'CTSM is stopping' - call endrun(subgrid_index=indexc, subgrid_level=subgrid_level_column, msg=errmsg(sourcefile, __LINE__)) + !call endrun(subgrid_index=indexc, subgrid_level=subgrid_level_column, msg=errmsg(sourcefile, __LINE__)) end if end if From 02b611b06beada8dc8ca58b7434bcac747fbf8dd Mon Sep 17 00:00:00 2001 From: Sam Rabin Date: Tue, 2 Jun 2026 11:19:31 -0600 Subject: [PATCH 082/113] Update FATES to 54cd2c37: Set btran_eff for NVP to 1.0, no soil water stress. --- .gitmodules | 2 +- src/fates | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index 27ab9cc700..ff199aa67d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -28,7 +28,7 @@ [submodule "fates"] path = src/fates url = https://github.com/huitang-earth/fates -fxtag = 6686502eb6d06ad498efcfcf6fba2ac98c5f0ac9 +fxtag = 54cd2c375976cbea96e1637222c624f664e0870b fxrequired = AlwaysRequired # Standard Fork to compare to with "git fleximod test" to ensure personal forks aren't committed fxDONOTUSEurl = https://github.com/NGEET/fates diff --git a/src/fates b/src/fates index 6686502eb6..54cd2c3759 160000 --- a/src/fates +++ b/src/fates @@ -1 +1 @@ -Subproject commit 6686502eb6d06ad498efcfcf6fba2ac98c5f0ac9 +Subproject commit 54cd2c375976cbea96e1637222c624f664e0870b From 31f02120d0377c72df555da28af282777cae4192 Mon Sep 17 00:00:00 2001 From: Sam Rabin Date: Tue, 2 Jun 2026 13:32:13 -0600 Subject: [PATCH 083/113] Add a target to fix compile. --- src/biogeophys/BalanceCheckMod.F90 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/biogeophys/BalanceCheckMod.F90 b/src/biogeophys/BalanceCheckMod.F90 index 597c26d9c5..8b12d56dbd 100644 --- a/src/biogeophys/BalanceCheckMod.F90 +++ b/src/biogeophys/BalanceCheckMod.F90 @@ -476,7 +476,7 @@ subroutine BalanceCheck( bounds, & integer , intent(in) :: filter_allc(:) ! filter for all columns type(atm2lnd_type) , intent(in) :: atm2lnd_inst type(solarabs_type) , intent(in) :: solarabs_inst - class(waterflux_type) , intent(in) :: waterflux_inst + class(waterflux_type) , intent(in), target :: waterflux_inst class(waterstate_type), intent(in) :: waterstate_inst type(waterdiagnosticbulk_type), intent(in) :: waterdiagnosticbulk_inst class(waterbalance_type), intent(inout) :: waterbalance_inst From 7f290587953b72d231a2636796e83ae869ec19b2 Mon Sep 17 00:00:00 2001 From: Sam Rabin Date: Wed, 3 Jun 2026 09:58:49 -0600 Subject: [PATCH 084/113] CDEPS changes to get ALP2 working. --- .gitmodules | 4 ++-- components/cdeps | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitmodules b/.gitmodules index ff199aa67d..53ef2648d1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -92,10 +92,10 @@ fxDONOTUSEurl = https://github.com/ESCOMP/CMEPS.git [submodule "cdeps"] path = components/cdeps url = https://github.com/ESCOMP/CDEPS.git -fxtag = cdeps1.0.93 +fxtag = 42f9a6b064ca8d1843a7849c58cc733b3994f94e fxrequired = ToplevelRequired # Standard Fork to compare to with "git fleximod test" to ensure personal forks aren't committed -fxDONOTUSEurl = https://github.com/ESCOMP/CDEPS.git +fxDONOTUSEurl = https://github.com/samsrabin/CDEPS.git [submodule "share"] path = share diff --git a/components/cdeps b/components/cdeps index 3f7f22d042..42f9a6b064 160000 --- a/components/cdeps +++ b/components/cdeps @@ -1 +1 @@ -Subproject commit 3f7f22d0426ccc1428a1ebfd4357caf90009132a +Subproject commit 42f9a6b064ca8d1843a7849c58cc733b3994f94e From 342a90ca2486e4f4ffde846f97e30c34a419c819 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Thu, 11 Jun 2026 11:55:57 +0300 Subject: [PATCH 085/113] Add NVP-specific cold restart. --- src/main/clm_initializeMod.F90 | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/main/clm_initializeMod.F90 b/src/main/clm_initializeMod.F90 index 4530fda860..3240f36757 100644 --- a/src/main/clm_initializeMod.F90 +++ b/src/main/clm_initializeMod.F90 @@ -12,6 +12,7 @@ module clm_initializeMod use abortutils , only : endrun use clm_varctl , only : nsrest, nsrStartup, nsrContinue, nsrBranch use clm_varctl , only : use_fates_sp, use_fates_bgc, use_fates + use clm_varctl , only : use_nvp ! [PORTED by Hui Tang: NVP cold-start ice override] use clm_varctl , only : is_cold_start use clm_varctl , only : iulog use clm_varctl , only : use_lch4, use_cn, use_cndv, use_c13, use_c14, nhillslope @@ -25,6 +26,7 @@ module clm_initializeMod use LandunitType , only : lun ! instance use ColumnType , only : col ! instance use PatchType , only : patch ! instance + use NVPLayerDynamicsMod , only : NVPColdStartIce ! [PORTED by Hui Tang: NVP cold-start ice override] use reweightMod , only : reweight_wrapup use filterMod , only : allocFilters, filter, filter_inactive_and_active use CLMFatesInterfaceMod , only : CLMFatesGlobals1,CLMFatesGlobals2 @@ -760,6 +762,19 @@ subroutine initialize2(ni,nj, currtime) call clm_fates%init_coldstart(water_inst%waterstatebulk_inst, & water_inst%waterdiagnosticbulk_inst, canopystate_inst, & soilstate_inst, soilbiogeochem_carbonflux_inst) + + ! [PORTED by Hui Tang: override NVP layer-0 ice to its pore capacity at cold start. + ! Runs after init_coldstart has set the NVP geometry (nvp_layer_active, dz) and before + ! the first begwb, so the initial water balance reflects the cap (no first-step blip / + ! no discarded water). Inside the cold-start FATES guard above, so it is cold-start only.] + if (use_nvp) then + !$OMP PARALLEL DO PRIVATE (nc, bounds_clump) + do nc = 1,nclumps + call get_clump_bounds(nc, bounds_clump) + call NVPColdStartIce(bounds_clump, water_inst%waterstatebulk_inst) + end do + !$OMP END PARALLEL DO + end if end if ! topo_glc_mec was allocated in initialize1, but needed to be kept around through From 85971aff625d6146208d7631c1300e376683370a Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Thu, 11 Jun 2026 11:57:28 +0300 Subject: [PATCH 086/113] Add nvp water related variables: qflx_ev_nvp_eff_col and qflx_nvp_to_snow_col. --- src/biogeophys/WaterFluxBulkType.F90 | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/biogeophys/WaterFluxBulkType.F90 b/src/biogeophys/WaterFluxBulkType.F90 index 8b5f8730f4..daac7737e7 100644 --- a/src/biogeophys/WaterFluxBulkType.F90 +++ b/src/biogeophys/WaterFluxBulkType.F90 @@ -40,9 +40,14 @@ module WaterFluxBulkType ! [PORTED by Hui Tang: NVP (moss/lichen) ground evaporation flux] real(r8), pointer :: qflx_ev_nvp_patch (:) ! patch evaporation flux from NVP (mm H2O/s) [+ to atm] real(r8), pointer :: qflx_ev_nvp_col (:) ! col evaporation flux from NVP (mm H2O/s) [+ to atm] + ! [PORTED by Hui Tang: diagnostic — effective NVP evap = frac_nvp_eff * qflx_ev_nvp_col, + ! i.e. the column-area-weighted flux actually removed from the NVP water balance. + ! History output only; not used by any physics.] + real(r8), pointer :: qflx_ev_nvp_eff_col (:) ! col effective NVP evap (mm H2O/s) ! [PORTED by Hui Tang: NVP water infiltration and drainage fluxes] real(r8), pointer :: qflx_nvp_infl_col (:) ! col water arriving at top of NVP layer (mm H2O/s) [diagnostic] real(r8), pointer :: qflx_nvp_drain_col (:) ! col drainage from NVP layer 0 to soil layer 1 (mm H2O/s) + real(r8), pointer :: qflx_nvp_to_snow_col (:) ! [PORTED by Hui Tang] col excess NVP ice (above pore capacity) pushed up into bottom snow layer (mm H2O/s) real(r8), pointer :: qflx_adv_col (:,:) ! col advective flux across different soil layer interfaces [mm H2O/s] [+ downward] real(r8), pointer :: qflx_rootsoi_col (:,:) ! col root and soil water exchange [mm H2O/s] [+ into root] @@ -131,9 +136,11 @@ subroutine InitBulkAllocate(this, bounds) ! [PORTED by Hui Tang: allocate NVP evaporation flux arrays] allocate( this%qflx_ev_nvp_patch (begp:endp)) ; this%qflx_ev_nvp_patch (:) = nan allocate( this%qflx_ev_nvp_col (begc:endc)) ; this%qflx_ev_nvp_col (:) = nan + allocate( this%qflx_ev_nvp_eff_col (begc:endc)) ; this%qflx_ev_nvp_eff_col (:) = nan ! [PORTED by Hui Tang: allocate NVP infiltration and drainage flux arrays] allocate( this%qflx_nvp_infl_col (begc:endc)) ; this%qflx_nvp_infl_col (:) = nan allocate( this%qflx_nvp_drain_col (begc:endc)) ; this%qflx_nvp_drain_col (:) = nan + allocate( this%qflx_nvp_to_snow_col (begc:endc)) ; this%qflx_nvp_to_snow_col (:) = 0._r8 ! [PORTED by Hui Tang] allocate(this%qflx_drain_vr_col (begc:endc,1:nlevsoi)) ; this%qflx_drain_vr_col (:,:) = nan allocate(this%qflx_adv_col (begc:endc,0:nlevsoi)) ; this%qflx_adv_col (:,:) = nan @@ -272,6 +279,12 @@ subroutine InitBulkHistory(this, bounds) avgflag='A', long_name=this%info%lname('column evaporation flux from nvp (moss/lichen)'), & ptr_col=this%qflx_ev_nvp_col, c2l_scale_type='urbanf', default='inactive') + this%qflx_ev_nvp_eff_col(begc:endc) = spval + call hist_addfld1d ( & + fname=this%info%fname('QFLX_EV_NVP_EFF_COL'), units='mm/s', & + avgflag='A', long_name=this%info%lname('effective nvp evaporation = frac_nvp_eff*qflx_ev_nvp_col'), & + ptr_col=this%qflx_ev_nvp_eff_col, c2l_scale_type='urbanf', default='inactive') + this%qflx_nvp_infl_col(begc:endc) = spval call hist_addfld1d ( & fname=this%info%fname('QFLX_NVP_INFL'), units='mm/s', & @@ -283,6 +296,13 @@ subroutine InitBulkHistory(this, bounds) fname=this%info%fname('QFLX_NVP_DRAIN'), units='mm/s', & avgflag='A', long_name=this%info%lname('drainage from nvp (moss/lichen) layer to soil'), & ptr_col=this%qflx_nvp_drain_col, c2l_scale_type='urbanf', default='inactive') + + ! [PORTED by Hui Tang: history for excess NVP ice routed to bottom snow layer] + this%qflx_nvp_to_snow_col(begc:endc) = spval + call hist_addfld1d ( & + fname=this%info%fname('QFLX_NVP_TO_SNOW'), units='mm/s', & + avgflag='A', long_name=this%info%lname('excess nvp (moss/lichen) ice pushed into bottom snow layer'), & + ptr_col=this%qflx_nvp_to_snow_col, c2l_scale_type='urbanf', default='inactive') end if end subroutine InitBulkHistory From 2e398e50ec59444e34f7a6167f2f2733bf9e8292 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Thu, 11 Jun 2026 12:05:52 +0300 Subject: [PATCH 087/113] add separate NVP resistance when frozen. --- src/biogeophys/NVPParamsMod.F90 | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/biogeophys/NVPParamsMod.F90 b/src/biogeophys/NVPParamsMod.F90 index 652c12eb6d..d05c68689a 100644 --- a/src/biogeophys/NVPParamsMod.F90 +++ b/src/biogeophys/NVPParamsMod.F90 @@ -33,8 +33,12 @@ module NVPParamsMod ! Evaporation resistance (van de Griend & Owe, 1994 / Daamen & Simmonds) ! rnvp = rnvp_min + rnvp_amp * (1 - satfrac)^rnvp_exp [s m-1] real(r8) :: rnvp_min = 10.0_r8 ! minimum surface resistance when saturated [s m-1] - real(r8) :: rnvp_amp = 500.0_r8 ! amplitude of resistance increase when dry [s m-1] + real(r8) :: rnvp_amp = 1000.0_r8 ! [PORTED by Hui Tang: raised 500->1000 to retain water (slower dry-moss evaporation)] amplitude of resistance increase when dry [s m-1] real(r8) :: rnvp_exp = 3.0_r8 ! exponent of dryness function [-] + ! [PORTED by Hui Tang: surface resistance applied when NVP is frozen — typical literature + ! range for ice/snow surfaces is ~1000-3000 s/m; default 1500 s/m suppresses but does + ! not zero sublimation. Set to a very large value (e.g. 1e6) to disable evap when frozen.] + real(r8) :: rnvp_ice = 1500.0_r8 ! NVP resistance when frozen [s m-1] ! Hydraulic properties (Mualem-van Genuchten) real(r8) :: ksat_nvp = 1.0e-4_r8 ! saturated hydraulic conductivity [m s-1] From edb9448e085a933b4302044a58cc23fab8b5d823 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Thu, 11 Jun 2026 12:08:42 +0300 Subject: [PATCH 088/113] Snow percolation to NVP layer regardless of whether NVP is exposed or buried. No NVP layer depth change with snow. --- src/biogeophys/SnowHydrologyMod.F90 | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/biogeophys/SnowHydrologyMod.F90 b/src/biogeophys/SnowHydrologyMod.F90 index 36e73ee102..aaea23d05c 100644 --- a/src/biogeophys/SnowHydrologyMod.F90 +++ b/src/biogeophys/SnowHydrologyMod.F90 @@ -1168,10 +1168,16 @@ subroutine SnowWater(bounds, & do i = water_inst%bulk_and_tracers_beg, water_inst%bulk_and_tracers_end associate(w => water_inst%bulk_and_tracers(i)) - ! [PORTED by Hui Tang: when NVP is active jbot_sno=-1; the bottom snow percolation - ! exits at j=-1 (into NVP), not j=0. Use j=0 for non-NVP columns as before.] + ! [PORTED by Hui Tang: select bottom-of-snow percolation index. Whenever the NVP + ! structural layer exists at j=0 (jbot_sno == -1), the bottom snow percolation + ! exits at j=-1 into NVP — regardless of whether NVP is exposed or buried. + ! Previously gated on col%nvp_layer_active(c) which is TRUE only when NVP is + ! exposed (snl=0); the buried-NVP case (snl<0, jbot_sno=-1) then incorrectly + ! picked qflx_snow_percolation_col(c,0), which carries the NVP→soil drainage + ! flux (a different physical process), causing systematic negative errh2osno. + ! Correct condition is jbot_sno==-1.] do c = begc, endc - if (col%nvp_layer_active(c)) then + if (col%jbot_sno(c) == -1) then qflx_snow_percolation_bottom_tmp(c) = w%waterflux_inst%qflx_snow_percolation_col(c, -1) else qflx_snow_percolation_bottom_tmp(c) = w%waterflux_inst%qflx_snow_percolation_col(c, 0) @@ -1804,6 +1810,11 @@ subroutine PostPercolation_AdjustLayerThicknesses(bounds, num_snowc, filter_snow do j = -nlevsno+1, 0 do fc = 1, num_snowc c = filter_snowc(fc) + ! [PORTED by Hui Tang: skip NVP layer j=0 — its dz is a structural property set by + ! UpdateNVPLayer from FATES-derived nvp_dz, not a snow-water-content floor. During + ! melt, snow percolation fills h2osoi_liq(c,0) and the standard floor below would + ! inflate dz(c,0), corrupting layer geometry until UpdateNVPLayer resets it.] + if (use_nvp .and. col%jbot_sno(c) == -1 .and. j == 0) cycle if (j >= snl(c)+1) then dz(c,j) = max(dz(c,j),h2osoi_liq(c,j)/denh2o + h2osoi_ice(c,j)/denice) end if From 907fe490a2b435813adac935fc864d27d5ed4069 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Thu, 11 Jun 2026 12:14:24 +0300 Subject: [PATCH 089/113] re-wired frac_nvp_eff for surface humidty --- src/biogeophys/SurfaceHumidityMod.F90 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/biogeophys/SurfaceHumidityMod.F90 b/src/biogeophys/SurfaceHumidityMod.F90 index 9677474d82..d37062917f 100644 --- a/src/biogeophys/SurfaceHumidityMod.F90 +++ b/src/biogeophys/SurfaceHumidityMod.F90 @@ -181,7 +181,8 @@ subroutine CalculateSurfaceHumidity(bounds, & ! [PORTED by Hui Tang: NVP effective fraction for ground humidity blend] ! NVP occupies area not covered by snow or surface water if (use_nvp) then - frac_nvp_eff = min(col%frac_nvp(c), max(0._r8, 1._r8 - frac_sno_eff(c) - frac_h2osfc(c))) + ! [PORTED by Hui Tang: re-wired frac_nvp_eff — snow buries NVP (frac_nvp - frac_sno_eff), cap = 1 - frac_h2osfc - frac_sno_eff] + frac_nvp_eff = min(1._r8 - frac_h2osfc(c) - frac_sno_eff(c), max(0._r8, col%frac_nvp(c) - frac_sno_eff(c))) qred = (1._r8 - frac_sno_eff(c) - frac_h2osfc(c) - frac_nvp_eff)*hr & + frac_sno_eff(c) + frac_h2osfc(c) + frac_nvp_eff*hr_nvp else From 400643c1f67941bcef7e90d67dc52753672d844d Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Thu, 11 Jun 2026 12:17:16 +0300 Subject: [PATCH 090/113] Add NVPColdStartIce for NVP cold start. --- src/biogeophys/NVPLayerDynamicsMod.F90 | 32 ++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/biogeophys/NVPLayerDynamicsMod.F90 b/src/biogeophys/NVPLayerDynamicsMod.F90 index 880f7d3d30..4fc92b3fd4 100644 --- a/src/biogeophys/NVPLayerDynamicsMod.F90 +++ b/src/biogeophys/NVPLayerDynamicsMod.F90 @@ -61,6 +61,7 @@ module NVPLayerDynamicsMod public :: NVPEvaporation public :: NVPWaterBalance_Column public :: NVPLayerRestart + public :: NVPColdStartIce ! [PORTED by Hui Tang: cold-start override of NVP layer-0 ice to pore cap] character(len=*), parameter, private :: sourcefile = __FILE__ @@ -677,4 +678,35 @@ subroutine NVPLayerRestart(bounds, ncid, flag) end subroutine NVPLayerRestart + !----------------------------------------------------------------------- + subroutine NVPColdStartIce(bounds, waterstate_inst) + ! + ! [PORTED by Hui Tang: cold-start initialization of the NVP layer-0 ice content. + ! The general WaterStateType::InitCold fills the j=0 slot as if it were a snow/soil + ! layer, giving an unphysically large ice mass (e.g. 55 kg/m2). Override it to the + ! NVP pore-space capacity (watsat_nvp*denice*dz) so the layer starts physically + ! consistent and the initial begwb already reflects the cap — no first-step blip and + ! no discarded water. Called once from clm_initializeMod after init_coldstart sets the + ! NVP geometry (nvp_layer_active, dz) and before the first begwb, on a cold start only. + ! Assumes a frozen cold start (ice-saturated, liquid=0), consistent with the alpine + ! Jan-1 cold start; the per-timestep appear-branch handles unfrozen reactivation.] + ! + ! !ARGUMENTS: + type(bounds_type), intent(in) :: bounds + class(waterstate_type), intent(inout) :: waterstate_inst + ! + ! !LOCAL VARIABLES: + integer :: c + real(r8) :: max_ice_nvp ! NVP pore-space ice capacity [kg m-2] + !----------------------------------------------------------------------- + do c = bounds%begc, bounds%endc + if (col%nvp_layer_active(c) .and. col%dz(c,0) > 0._r8) then + max_ice_nvp = watsat_nvp * denice * col%dz(c,0) + waterstate_inst%h2osoi_ice_col(c,0) = 0._r8 + waterstate_inst%h2osoi_liq_col(c,0) = 0._r8 + end if + end do + + end subroutine NVPColdStartIce + end module NVPLayerDynamicsMod From 7da693051adc09b655b7c35cb0c254d692da4bf7 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Thu, 11 Jun 2026 12:22:24 +0300 Subject: [PATCH 091/113] Assign different hydraulic conductivity according to the water gradient between soil and nvp; deal with excess ice in NVP. --- src/biogeophys/NVPLayerDynamicsMod.F90 | 101 +++++++++++++++++++------ 1 file changed, 76 insertions(+), 25 deletions(-) diff --git a/src/biogeophys/NVPLayerDynamicsMod.F90 b/src/biogeophys/NVPLayerDynamicsMod.F90 index 4fc92b3fd4..e343af31e7 100644 --- a/src/biogeophys/NVPLayerDynamicsMod.F90 +++ b/src/biogeophys/NVPLayerDynamicsMod.F90 @@ -448,27 +448,32 @@ subroutine NVPWaterBalance_Column(bounds, dtime, waterfluxbulk_inst, waterstate_ real(r8) :: khydr_nvp ! NVP unsaturated hydraulic conductivity [m s-1] real(r8) :: K_nvp_mms ! khydr_nvp converted to mm/s real(r8) :: K_soil1 ! soil layer 1 hydraulic conductivity [mm/s] - real(r8) :: K_interface ! harmonic-mean interface conductivity [mm/s] + real(r8) :: K_interface ! interface conductivity (upstream-weighted) [mm/s] + real(r8) :: grad ! [PORTED by Hui Tang] NVP-soil head gradient (matric+gravity); <0 = upward real(r8) :: psi_nvp ! NVP van Genuchten matric potential [mm] real(r8) :: smp_soil1 ! soil layer 1 matric potential, prev. timestep [mm] real(r8) :: dz_iface_mm ! distance between NVP and soil layer 1 centres [mm] real(r8) :: q01 ! Darcy flux NVP→soil (+down, -up) [mm s-1] real(r8) :: h2osoi_net ! h2osoi_liq(c,0) after infl and evap [kg m-2] real(r8) :: satfrac ! NVP effective saturation fraction [-] - ! [PORTED by Hui Tang: effective NVP evap flux — zeroed when NVP is buried under snow] - real(r8) :: ev_nvp_eff ! evap flux applied to NVP water [mm/s]; 0 when buried + real(r8) :: max_ice_nvp ! [PORTED by Hui Tang] max NVP ice = pore-space capacity [kg m-2] + real(r8) :: ice_excess ! [PORTED by Hui Tang] NVP ice above pore capacity [kg m-2] associate( & qflx_rain_plus_snomelt => waterfluxbulk_inst%qflx_rain_plus_snomelt_col, & qflx_ev_nvp_col => waterfluxbulk_inst%qflx_ev_nvp_col, & + qflx_ev_nvp_eff_col => waterfluxbulk_inst%qflx_ev_nvp_eff_col, & ! [PORTED by Hui Tang: diagnostic history output] qflx_nvp_infl_col => waterfluxbulk_inst%qflx_nvp_infl_col, & qflx_nvp_drain_col => waterfluxbulk_inst%qflx_nvp_drain_col, & + qflx_nvp_to_snow_col => waterfluxbulk_inst%qflx_nvp_to_snow_col, & ! [PORTED by Hui Tang: excess NVP ice -> bottom snow layer] h2osoi_liq => waterstate_inst%h2osoi_liq_col, & h2osoi_ice => waterstate_inst%h2osoi_ice_col, & ! [PORTED by Hui Tang: ice content for porosity reduction] h2onvp_col => waterstate_inst%h2onvp_col, & ! [PORTED by Hui Tang: sync diagnostic copy] smp_l => soilstate_inst%smp_l_col, & hk_l => soilstate_inst%hk_l_col, & frac_h2osfc_col => waterdiagnosticbulk_inst%frac_h2osfc_col, & + ! [PORTED by Hui Tang: snow fraction for snow-cover-aware frac_nvp_eff] + frac_sno_eff_col => waterdiagnosticbulk_inst%frac_sno_eff_col, & fwet_nvp_col => waterdiagnosticbulk_inst%fwet_nvp_col, & vwc_nvp_col => waterdiagnosticbulk_inst%vwc_nvp_col, & ! [PORTED by Hui Tang: volumetric water content] t_nvp_col => temperature_inst%t_nvp_col & ! Input: [real(r8) (:) ] NVP (moss/lichen) temperature (Kelvin) @@ -482,9 +487,14 @@ subroutine NVPWaterBalance_Column(bounds, dtime, waterfluxbulk_inst, waterstate_ cycle end if - ! --- Effective NVP area: not covered by surface water --- + ! --- Effective NVP area: not covered by snow or surface water --- + ! [PORTED by Hui Tang: include frac_sno_eff_col so partial snow cover reduces + ! the effective NVP fraction used for both infiltration and evaporation. + ! Same convention as SoilFluxesMod, BareGroundFluxesMod, CanopyFluxesMod.] frac_h2osfc = frac_h2osfc_col(c) - frac_nvp_eff = min(col%frac_nvp(c), max(0._r8, 1._r8 - frac_h2osfc)) + ! [PORTED by Hui Tang: re-wired frac_nvp_eff — snow buries NVP (frac_nvp - frac_sno_eff), cap = 1 - frac_h2osfc - frac_sno_eff] + frac_nvp_eff = min(1._r8 - frac_h2osfc - frac_sno_eff_col(c), max(0._r8, & + col%frac_nvp(c) - frac_sno_eff_col(c))) ! --- Water input to NVP from precipitation / snowmelt --- ! [PORTED by Hui Tang: when snow is present (snl < 0), snow percolation into NVP is @@ -534,34 +544,50 @@ subroutine NVPWaterBalance_Column(bounds, dtime, waterfluxbulk_inst, waterstate_ smp_soil1 = smp_l(c,1) ! [mm] K_soil1 = hk_l(c,1) ! [mm/s] - ! Harmonic-mean interface conductivity - if (K_nvp_mms + K_soil1 > 0._r8) then - K_interface = 2._r8 * K_nvp_mms * K_soil1 / (K_nvp_mms + K_soil1) - else - K_interface = 0._r8 - end if - ! Distance between layer centres [mm] dz_iface_mm = (0.5_r8 * col%dz(c,0) + 0.5_r8 * col%dz(c,1)) * 1000._r8 - ! Darcy flux: q = K * (grad_psi + gravity), positive = downward - q01 = K_interface * ((psi_nvp - smp_soil1) / dz_iface_mm + 1.0_r8) - - ! --- Atmosphere evap/condensation: only applies when NVP is exposed --- - ! [PORTED by Hui Tang: qflx_ev_nvp_col is computed from qg_nvp=qg_soil even when - ! NVP is buried under snow (frac_sno_eff=1 → frac_nvp_eff=0 → qg unchanged). - ! This flux is NOT included in qflx_evap_tot_col (which tracks only the blended - ! qg-based flux), so applying it to h2osoi_liq(c,0) creates an untracked water - ! source that breaks errh2o. When snow covers NVP (snl < -1), zero the flux.] - if (col%snl(c) < -1) then - ev_nvp_eff = 0._r8 + ! Head gradient (matric potential + gravity); negative = upward (capillary rise into NVP) + grad = (psi_nvp - smp_soil1) / dz_iface_mm + 1.0_r8 + + ! [PORTED by Hui Tang: upstream-weighted interface conductivity. The harmonic mean collapses + ! to ~0 when the NVP layer is dry (K_nvp -> 0 in Mualem-van Genuchten), so a dry moss could + ! not draw water up despite strong suction. Use the SOURCE layer's conductivity for the flow + ! direction: upward (capillary rise) is supplied at the soil conductivity K_soil1; downward + ! drainage is limited by the NVP conductivity K_nvp_mms. Upward flux is still capped by the + ! available soil-layer-1 liquid below, so the soil is not over-drained.] + if (grad < 0._r8) then + K_interface = K_soil1 ! upward: soil supplies the water else - ev_nvp_eff = qflx_ev_nvp_col(c) + K_interface = K_nvp_mms ! downward: NVP drains at its own conductivity end if + ! Darcy flux: q = K * (grad_psi + gravity), positive = downward + q01 = K_interface * grad + + ! --- Atmosphere evap/condensation, scaled by effective NVP area --- + ! [PORTED by Hui Tang: qflx_ev_nvp_eff_col scales with frac_nvp_eff so partial + ! snow cover gracefully reduces the NVP evaporation contribution to the water + ! balance. Replaces the prior binary snl<-1 check, which is now subsumed: + ! when NVP is deeply buried, frac_sno_eff_col → 1 → frac_nvp_eff → 0 → + ! qflx_ev_nvp_eff_col → 0 automatically. Also exposed as history field + ! QFLX_EV_NVP_EFF_COL. Same convention as the infiltration line above: + ! qflx_nvp_infl_col = frac_nvp_eff * qflx_rain_plus_snomelt.] + qflx_ev_nvp_eff_col(c) = frac_nvp_eff * qflx_ev_nvp_col(c) + + ! [PORTED by Hui Tang: the NVP evaporation is now ALREADY in qflx_evap_tot_col — do NOT add + ! it here (would double-count). SoilFluxesMod builds qflx_evap_tot_patch from the per-surface + ! ground-evap total qflx_evap_grnd_eff, which contains frac_nvp_eff*qflx_ev_nvp_patch; the + ! clm_drv_patch2col p2c (driver line ~947, BEFORE HydrologyNoDrainage) then carries exactly + ! frac_nvp_eff*qflx_ev_nvp_col = qflx_ev_nvp_eff_col into qflx_evap_tot_col. That is the same + ! amount debited from h2osoi_liq(c,0) below, so the column water balance (BalanceCheckMod + ! errh2o) already conserves. (The prior explicit add was correct only while SoilFluxesMod + ! overwrote qflx_evap_tot_patch with the bulk qflx_evap_soi and dropped the moss evap; the + ! qflx_evap_grnd_eff change made that add redundant.)] + ! --- Update h2osoi_liq(c,0): add infl, subtract evap; cannot go negative --- h2osoi_net = h2osoi_liq(c,0) & - + (qflx_nvp_infl_col(c) - ev_nvp_eff) * dtime ! [kg m-2] + + (qflx_nvp_infl_col(c) - qflx_ev_nvp_eff_col(c)) * dtime ! [kg m-2] h2osoi_net = max(0._r8, h2osoi_net) if (q01 >= 0._r8) then @@ -585,6 +611,31 @@ subroutine NVPWaterBalance_Column(bounds, dtime, waterfluxbulk_inst, waterstate_ end if end if + ! --- Step 7: Ice pore-space cap — push excess frozen water up into the snow --- + ! [PORTED by Hui Tang: cap NVP ice at pore capacity (watsat_nvp*denice*dz). Frozen water + ! (refreezing meltwater / snow ice) must not accumulate beyond what the pore volume can + ! hold, which otherwise inflates cv(c,0) and breaks the soil energy balance (errsoi). + ! Excess ice is moved UP into the bottom snow layer (j=-1) as ice — energetically clean + ! (ice->ice, no phase change) — and recorded in qflx_nvp_to_snow_col so the snow balance + ! (errh2osno, BalanceCheckMod) books it as a snow source. When no snow layer exists + ! (snl=0, NVP exposed), fall back to draining the excess to soil layer 1.] + qflx_nvp_to_snow_col(c) = 0._r8 + if (col%dz(c,0) > 0._r8) then + max_ice_nvp = watsat_nvp * denice * col%dz(c,0) ! pore-space cap [kg m-2] + if (h2osoi_ice(c,0) > max_ice_nvp) then + ice_excess = h2osoi_ice(c,0) - max_ice_nvp + h2osoi_ice(c,0) = max_ice_nvp + if (col%jbot_sno(c) == -1 .and. col%snl(c) <= -2) then + ! snow present: move excess up into the bottom snow layer (j=-1), as ice + h2osoi_ice(c,-1) = h2osoi_ice(c,-1) + ice_excess + qflx_nvp_to_snow_col(c) = ice_excess / dtime + else + ! no snow layer to receive it: drain to soil layer 1 (fallback) + qflx_nvp_drain_col(c) = qflx_nvp_drain_col(c) + ice_excess / dtime + end if + end if + end if + ! --- Update fwet_nvp (saturation fraction passed to FATES) --- if (col%dz(c,0) > 0._r8) then vol_liq = h2osoi_liq(c,0) / (denh2o * col%dz(c,0)) From e1ed254fe49a2f0513f7a442eeabc1b0b5adc855 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Thu, 11 Jun 2026 12:55:31 +0300 Subject: [PATCH 092/113] Add nvp specific evaporation (qflx_ev_nvp) to total surface evaporation (qflx_evap_soi) in CanopyFluxMod (not tested). --- src/biogeophys/CanopyFluxesMod.F90 | 88 ++++++++++++++++++++++++++---- 1 file changed, 77 insertions(+), 11 deletions(-) diff --git a/src/biogeophys/CanopyFluxesMod.F90 b/src/biogeophys/CanopyFluxesMod.F90 index 7c146b3475..04a285b4ec 100644 --- a/src/biogeophys/CanopyFluxesMod.F90 +++ b/src/biogeophys/CanopyFluxesMod.F90 @@ -47,6 +47,8 @@ module CanopyFluxesMod use ColumnType , only : col use PatchType , only : patch use EDTypesMod , only : ed_site_type + ! [PORTED by Hui Tang: NVP surface resistance parameters for local rnvp computation] + use NVPParamsMod , only : rnvp_min, rnvp_amp, rnvp_exp, rnvp_ice use SoilWaterRetentionCurveMod, only : soil_water_retention_curve_type use LunaMod , only : Update_Photosynthesis_Capacity, Acc24_Climate_LUNA,Acc240_Climate_LUNA,Clear24_Climate_LUNA use NumericsMod , only : truncate_small_values @@ -320,6 +322,16 @@ subroutine CanopyFluxes(bounds, num_exposedvegp, filter_exposedvegp, real(r8) :: wtaq ! latent heat conductance for air [m/s] real(r8) :: wtlq ! latent heat conductance for leaf [m/s] real(r8) :: wtgq(bounds%begp:bounds%endp) ! latent heat conductance for ground [m/s] + ! [PORTED by Hui Tang: per-surface ground latent conductances for NVP-aware LE partitioning. + ! Used only for post-iteration diagnostic fluxes; iterative wtgq stays bulk to keep + ! canopy energy balance solver bit-identical to non-NVP behaviour.] + real(r8) :: wtgq_snow ! snow surface latent conductance [m/s] + real(r8) :: wtgq_soil ! bare soil latent conductance [m/s] + real(r8) :: wtgq_h2osfc ! surface water latent conductance [m/s] + real(r8) :: wtgq_nvp ! NVP surface latent conductance [m/s] + real(r8) :: rnvp ! NVP surface evaporative resistance [s/m] + real(r8) :: satfrac_nvp ! NVP effective saturation fraction [-] + real(r8) :: frac_soil ! bare soil fraction (1-fsno-fh2osfc-fnvp_eff) [-] real(r8) :: wtaq0(bounds%begp:bounds%endp) ! normalized latent heat conductance for air [-] real(r8) :: wtlq0(bounds%begp:bounds%endp) ! normalized latent heat conductance for leaf [-] real(r8) :: wtgq0 ! normalized heat conductance for ground [-] @@ -570,7 +582,9 @@ subroutine CanopyFluxes(bounds, num_exposedvegp, filter_exposedvegp, frac_h2osfc => waterdiagnosticbulk_inst%frac_h2osfc_col , & ! Input: [real(r8) (:) ] fraction of surface water fwet => waterdiagnosticbulk_inst%fwet_patch , & ! Input: [real(r8) (:) ] fraction of canopy that is wet (0 to 1) fdry => waterdiagnosticbulk_inst%fdry_patch , & ! Input: [real(r8) (:) ] fraction of foliage that is green and dry [-] - frac_sno => waterdiagnosticbulk_inst%frac_sno_eff_col , & ! Input: [real(r8) (:) ] fraction of ground covered by snow (0 to 1) + frac_sno => waterdiagnosticbulk_inst%frac_sno_eff_col , & ! Input: [real(r8) (:) ] fraction of ground covered by snow (0 to 1) + ! [PORTED by Hui Tang: NVP wet fraction (= effective saturation) for local rnvp] + fwet_nvp_col => waterdiagnosticbulk_inst%fwet_nvp_col , & ! Input: [real(r8) (:) ] NVP wet fraction (0 to 1) snow_depth => waterdiagnosticbulk_inst%snow_depth_col , & ! Input: [real(r8) (:) ] snow height (m) qg_snow => waterdiagnosticbulk_inst%qg_snow_col , & ! Input: [real(r8) (:) ] specific humidity at snow surface [kg/kg] qg_soil => waterdiagnosticbulk_inst%qg_soil_col , & ! Input: [real(r8) (:) ] specific humidity at soil surface [kg/kg] @@ -1313,7 +1327,8 @@ subroutine CanopyFluxes(bounds, num_exposedvegp, filter_exposedvegp, ! fractionate ground emitted longwave ! [PORTED by Hui Tang: include NVP layer in lw_grnd blend] if (use_nvp) then - frac_nvp_eff = min(col%frac_nvp(c), max(0._r8, 1._r8 - frac_sno(c) - frac_h2osfc(c))) + ! [PORTED by Hui Tang: re-wired frac_nvp_eff — snow buries NVP (frac_nvp - frac_sno), cap = 1 - frac_h2osfc - frac_sno] + frac_nvp_eff = min(1._r8 - frac_h2osfc(c) - frac_sno(c), max(0._r8, col%frac_nvp(c) - frac_sno(c))) else frac_nvp_eff = 0._r8 end if @@ -1489,7 +1504,8 @@ subroutine CanopyFluxes(bounds, num_exposedvegp, filter_exposedvegp, ! Energy balance check in canopy ! [PORTED by Hui Tang: include NVP in lw_grnd for energy balance check] if (use_nvp) then - frac_nvp_eff = min(col%frac_nvp(c), max(0._r8, 1._r8 - frac_sno(c) - frac_h2osfc(c))) + ! [PORTED by Hui Tang: re-wired frac_nvp_eff — snow buries NVP (frac_nvp - frac_sno), cap = 1 - frac_h2osfc - frac_sno] + frac_nvp_eff = min(1._r8 - frac_h2osfc(c) - frac_sno(c), max(0._r8, col%frac_nvp(c) - frac_sno(c))) else frac_nvp_eff = 0._r8 end if @@ -1552,25 +1568,75 @@ subroutine CanopyFluxes(bounds, num_exposedvegp, filter_exposedvegp, ! compute individual latent heat fluxes delq_snow = wtalq(p)*qg_snow(c)-wtlq0(p)*qsatl(p)-wtaq0(p)*forc_q(c) - qflx_ev_snow(p) = forc_rho(c)*wtgq(p)*delq_snow - delq_soil = wtalq(p)*qg_soil(c)-wtlq0(p)*qsatl(p)-wtaq0(p)*forc_q(c) - qflx_ev_soil(p) = forc_rho(c)*wtgq(p)*delq_soil - delq_h2osfc = wtalq(p)*qg_h2osfc(c)-wtlq0(p)*qsatl(p)-wtaq0(p)*forc_q(c) - qflx_ev_h2osfc(p) = forc_rho(c)*wtgq(p)*delq_h2osfc + + ! [PORTED by Hui Tang: per-surface ground latent conductances for NVP-aware LE. + ! Iterative wtgq(p) above used soilresis in series with raw(p,below_canopy); + ! applying that same throttling to snow/h2osfc/NVP suppresses sublimation by + ! ~40× in NVP runs (NVP intercepts surface infiltration → soil layer 1 dries + ! → soilresis blows up). Recompute each surface's conductance with its own + ! resistance for the post-iteration diagnostic flux assignments, then rebuild + ! qflx_evap_soi from the area-weighted sum. Iterative wtgq(p) is left + ! untouched so canopy energy balance convergence is bit-identical.] + if (use_nvp) then + ! Local NVP surface resistance (matches NVPLayerDynamicsMod::NVPEvaporation) + if (t_soisno(c,0) >= tfrz) then + satfrac_nvp = max(0._r8, min(1._r8, fwet_nvp_col(c))) + rnvp = rnvp_min + rnvp_amp * (1._r8 - satfrac_nvp)**rnvp_exp + else + rnvp = rnvp_ice + end if + wtgq_snow = frac_veg_nosno(p) / raw(p,below_canopy) + wtgq_h2osfc = frac_veg_nosno(p) / raw(p,below_canopy) + wtgq_soil = frac_veg_nosno(p) / (raw(p,below_canopy) + soilresis(c)) + wtgq_nvp = frac_veg_nosno(p) / (raw(p,below_canopy) + rnvp) + qflx_ev_snow(p) = forc_rho(c) * wtgq_snow * delq_snow + qflx_ev_soil(p) = forc_rho(c) * wtgq_soil * delq_soil + qflx_ev_h2osfc(p) = forc_rho(c) * wtgq_h2osfc * delq_h2osfc + else + qflx_ev_snow(p) = forc_rho(c) * wtgq(p) * delq_snow + qflx_ev_soil(p) = forc_rho(c) * wtgq(p) * delq_soil + qflx_ev_h2osfc(p) = forc_rho(c) * wtgq(p) * delq_h2osfc + end if ! [PORTED by Hui Tang: NVP individual latent heat flux, analogous to snow/h2osfc] ! qflx_evap_soi already includes NVP because qg(c) blends NVP in SurfaceHumidityMod. ! This is the diagnostic breakdown of the NVP contribution. - ! Zero when NVP is buried under snow (snl < -1) — same guard as BareGroundFluxesMod. - if (use_nvp .and. col%frac_nvp(c) > 0._r8 .and. col%snl(c) >= -1) then + ! [PORTED by Hui Tang: gate on exposed NVP fraction (frac_nvp_eff>0) instead of the binary + ! snow-layer count snl>=-1, so partial snow cover no longer erases NVP latent flux while + ! part of the column is snow-free. frac_nvp_eff uses frac_sno (Canopy's snow variable), + ! matching the qflx_evap_soi weighting below; computed here because it is not yet set.] + if (use_nvp) then + frac_nvp_eff = min(1._r8 - frac_h2osfc(c) - frac_sno(c), max(0._r8, & + col%frac_nvp(c) - frac_sno(c))) + else + frac_nvp_eff = 0._r8 + end if + ! Zero when NVP is fully buried (frac_nvp_eff <= 0) — same guard as BareGroundFluxesMod. + if (use_nvp .and. frac_nvp_eff > 0._r8) then delq_nvp = wtalq(p)*qg_nvp(c)-wtlq0(p)*qsatl(p)-wtaq0(p)*forc_q(c) - qflx_ev_nvp(p) = forc_rho(c)*wtgq(p)*delq_nvp + qflx_ev_nvp(p) = forc_rho(c) * wtgq_nvp * delq_nvp else qflx_ev_nvp(p) = 0._r8 end if + ! [PORTED by Hui Tang: rebuild qflx_evap_soi from per-surface fluxes weighted + ! by area. Without this, the bulk qflx_evap_soi (computed via bulk wtgq with + ! soilresis) throttles snow sublimation in NVP runs. When use_nvp=.false., + ! the original bulk value above is kept unchanged.] + if (use_nvp) then + ! [PORTED by Hui Tang: re-wired frac_nvp_eff — snow buries NVP (frac_nvp - frac_sno), cap = 1 - frac_h2osfc - frac_sno] + frac_nvp_eff = min(1._r8 - frac_h2osfc(c) - frac_sno(c), max(0._r8, & + col%frac_nvp(c) - frac_sno(c))) + frac_soil = max(0._r8, & + 1._r8 - frac_sno(c) - frac_h2osfc(c) - frac_nvp_eff) + qflx_evap_soi(p) = frac_sno(c) * qflx_ev_snow(p) & + + frac_h2osfc(c) * qflx_ev_h2osfc(p) & + + frac_nvp_eff * qflx_ev_nvp(p) & + + frac_soil * qflx_ev_soil(p) + end if + ! 2 m height air temperature t_ref2m(p) = thm(p) + temp1(p)*dth(p)*(1._r8/temp12m(p) - 1._r8/temp1(p)) From c3d2f2c2859811639ec7e86b153dcb14607d2ae0 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Thu, 11 Jun 2026 14:50:45 +0300 Subject: [PATCH 093/113] Build per-surface raiw to avoid soilresis cross-coupling in bareground evap flux calculation. --- src/biogeophys/BareGroundFluxesMod.F90 | 129 +++++++++++++++++++++---- 1 file changed, 110 insertions(+), 19 deletions(-) diff --git a/src/biogeophys/BareGroundFluxesMod.F90 b/src/biogeophys/BareGroundFluxesMod.F90 index 2768f840c4..86091f3759 100644 --- a/src/biogeophys/BareGroundFluxesMod.F90 +++ b/src/biogeophys/BareGroundFluxesMod.F90 @@ -25,6 +25,8 @@ module BareGroundFluxesMod use ColumnType , only : col use PatchType , only : patch use clm_varctl , only : use_fates + ! [PORTED by Hui Tang: NVP surface resistance parameters for local rnvp computation] + use NVPParamsMod , only : rnvp_min, rnvp_amp, rnvp_exp, rnvp_ice ! ! !PUBLIC TYPES: implicit none @@ -141,6 +143,15 @@ subroutine BareGroundFluxes(bounds, num_noexposedvegp, filter_noexposedvegp, & real(r8) :: raw ! moisture resistance [s/m] real(r8) :: raih ! temporary variable [kg/m2/s] real(r8) :: raiw ! temporary variable [kg/m2/s] + ! [PORTED by Hui Tang: per-surface moisture conductances for NVP-aware LE partitioning] + real(r8) :: raiw_snow ! moisture conductance for snow surface [kg/m2/s] + real(r8) :: raiw_soil ! moisture conductance for bare soil [kg/m2/s] + real(r8) :: raiw_h2osfc ! moisture conductance for surface water [kg/m2/s] + real(r8) :: raiw_nvp ! moisture conductance for NVP surface [kg/m2/s] + real(r8) :: frac_nvp_eff ! effective NVP fraction (capped by 1-fsno-fh2osfc) [-] + real(r8) :: frac_soil ! bare soil fraction (1-fsno-fh2osfc-fnvp) [-] + real(r8) :: rnvp ! NVP surface evaporative resistance [s/m] + real(r8) :: satfrac_nvp ! NVP effective saturation fraction [-] real(r8) :: fm(bounds%begp:bounds%endp) ! needed for BGC only to diagnose 10m wind speed real(r8) :: e_ref2m ! 2 m height surface saturated vapor pressure [Pa] real(r8) :: qsat_ref2m ! 2 m height surface saturated specific humidity [kg/kg] @@ -163,8 +174,13 @@ subroutine BareGroundFluxes(bounds, num_noexposedvegp, filter_noexposedvegp, & associate( & dhsdt_canopy => energyflux_inst%dhsdt_canopy_patch , & ! Output: [real(r8) (:) ] change in heat storage of stem (W/m**2) [+ to atm] eflx_sh_stem => energyflux_inst%eflx_sh_stem_patch , & ! Output: [real(r8) (:) ] sensible heat flux from stems (W/m**2) [+ to atm] - soilresis => soilstate_inst%soilresis_col , & ! Input: [real(r8) (:,:) ] evaporative soil resistance (s/m) - snl => col%snl , & ! Input: [integer (:) ] number of snow layers + soilresis => soilstate_inst%soilresis_col , & ! Input: [real(r8) (:,:) ] evaporative soil resistance (s/m) + ! [PORTED by Hui Tang: surface area fractions for NVP-aware LE partitioning] + frac_sno_eff => waterdiagnosticbulk_inst%frac_sno_eff_col , & ! Input: [real(r8) (:) ] eff. fraction of ground covered by snow (0 to 1) + frac_h2osfc => waterdiagnosticbulk_inst%frac_h2osfc_col , & ! Input: [real(r8) (:) ] fraction of ground covered by surface water (0 to 1) + ! [PORTED by Hui Tang: NVP wet fraction (= effective saturation) for local rnvp] + fwet_nvp_col => waterdiagnosticbulk_inst%fwet_nvp_col , & ! Input: [real(r8) (:) ] NVP wet fraction (0 to 1) + snl => col%snl , & ! Input: [integer (:) ] number of snow layers dz => col%dz , & ! Input: [real(r8) (:,:) ] layer depth (m) zii => col%zii , & ! Input: [real(r8) (:) ] convective boundary height [m] @@ -453,12 +469,38 @@ subroutine BareGroundFluxes(bounds, num_noexposedvegp, filter_noexposedvegp, & forc_dewpoint = forc_dewpoint + tfrz !changed by K.Sakaguchi. Soilbeta is used for evaporation - if (dqh(p) > 0._r8) then !dew (beta is not applied, just like rsoil used to be) + if (dqh(p) > 0._r8) then !dew (beta is not applied, just like rsoil used to be) if (t_grnd(c) > forc_dewpoint) then ! no dew - raiw = 0._r8 + raiw = 0._r8 else ! dew raiw = forc_rho(c)/(raw) - end if + end if + ! [PORTED by Hui Tang: per-surface moisture conductances for the dew branch. + ! Dew (condensation) deposits on every surface with aerodynamic resistance + ! only — no soil/NVP surface resistance is applied, consistent with the bulk + ! raiw above and the "beta is not applied" comment. Mirror the no-dew/dew + ! split: zero when no dew (t_grnd > dewpoint), aerodynamic-only otherwise. + ! Without this, raiw_nvp etc. retain stale values from the previous patch's + ! evaporation branch and corrupt the per-surface qflx_ev_* fluxes below (which + ! are consumed in SoilFluxes for the water-store partitioning and for the + ! area-weighted ground-evap total qflx_evap_grnd_eff). frac_nvp_eff is also + ! recomputed here because the qflx_ev_nvp and eflx_sh_nvp gates (frac_nvp_eff>0) + ! below depend on it.] + if (use_nvp) then + if (t_grnd(c) > forc_dewpoint) then ! no dew + raiw_snow = 0._r8 + raiw_h2osfc = 0._r8 + raiw_soil = 0._r8 + raiw_nvp = 0._r8 + else ! dew + raiw_snow = forc_rho(c) / raw + raiw_h2osfc = forc_rho(c) / raw + raiw_soil = forc_rho(c) / raw + raiw_nvp = forc_rho(c) / raw + end if + frac_nvp_eff = min(1._r8 - frac_h2osfc(c) - frac_sno_eff(c), max(0._r8, & + col%frac_nvp(c) - frac_sno_eff(c))) + end if else if(do_soilevap_beta())then if (t_grnd(c) > forc_dewpoint) then ! no dew @@ -470,7 +512,46 @@ subroutine BareGroundFluxes(bounds, num_noexposedvegp, filter_noexposedvegp, & endif if(do_soil_resistance_sl14())then ! Swenson & Lawrence 2014 soil resistance is applied - raiw = forc_rho(c)/(raw+soilresis(c)) + if (use_nvp) then + ! [PORTED by Hui Tang: soilresis applies only to bare soil patches. + ! In NVP runs, soilresis becomes very large because NVP intercepts + ! surface infiltration and dries soil layer 1; using a single bulk + ! raiw then incorrectly throttles snow sublimation by ~40× during + ! the spring melt season. Compute per-surface raiw; these feed the + ! per-surface qflx_ev_* fluxes below, which SoilFluxes consumes for the + ! water-store partitioning and the area-weighted ground-evap total + ! qflx_evap_grnd_eff.] + raiw_snow = forc_rho(c) / raw ! snow: aerodynamic only + raiw_h2osfc = forc_rho(c) / raw ! open water: aerodynamic only + raiw_soil = forc_rho(c) / (raw + soilresis(c)) ! bare soil: SL14 + ! [PORTED by Hui Tang: compute NVP surface resistance locally to avoid + ! cross-module state coupling that caused the compile error in commit + ! 16945c674 (reverted). Same formula as NVPEvaporation in NVPLayerDynamicsMod: + ! unfrozen: rnvp = rnvp_min + rnvp_amp * (1 - satfrac)^rnvp_exp + ! frozen: rnvp = rnvp_ice (literature ice/snow resistance) + ! satfrac taken from fwet_nvp_col (already a 0-1 effective saturation). + ! When NVP is inactive (frac_nvp=0), raiw_nvp is irrelevant — flux gated + ! by frac_nvp_eff weighting and the qflx_ev_nvp guard below.] + if (t_soisno(c,0) >= tfrz) then + satfrac_nvp = max(0._r8, min(1._r8, fwet_nvp_col(c))) + rnvp = rnvp_min + rnvp_amp * (1._r8 - satfrac_nvp)**rnvp_exp + else + rnvp = rnvp_ice + end if + raiw_nvp = forc_rho(c) / (raw + rnvp) ! NVP: aerodynamic + rnvp + ! [PORTED by Hui Tang: re-wired frac_nvp_eff — snow buries NVP (frac_nvp - frac_sno_eff), cap = 1 - frac_h2osfc - frac_sno_eff] + frac_nvp_eff = min(1._r8 - frac_h2osfc(c) - frac_sno_eff(c), max(0._r8, & + col%frac_nvp(c) - frac_sno_eff(c))) + frac_soil = max(0._r8, & + 1._r8 - frac_sno_eff(c) - frac_h2osfc(c) - frac_nvp_eff) + ! Area-weighted aggregate raiw — used for cgrndl linearization and the + ! qflx_evap_soi back-compat diagnostic. The per-surface ground-evap total + ! is now built in SoilFluxes (qflx_evap_grnd_eff), not here. + raiw = frac_sno_eff(c)*raiw_snow + frac_h2osfc(c)*raiw_h2osfc & + + frac_nvp_eff *raiw_nvp + frac_soil *raiw_soil + else + raiw = forc_rho(c)/(raw+soilresis(c)) + endif endif end if @@ -517,26 +598,28 @@ subroutine BareGroundFluxes(bounds, num_noexposedvegp, filter_noexposedvegp, & qflx_tran_veg(p) = 0._r8 qflx_evap_veg(p) = 0._r8 qflx_evap_soi(p) = -raiw*dqh(p) - qflx_evap_tot(p) = qflx_evap_soi(p) - - print *, "qflx_evap_tot=", qflx_evap_soi(p), raiw, dqh(p) ! compute latent heat fluxes individually - qflx_ev_snow(p) = -raiw*(forc_q(c) - qg_snow(c)) - qflx_ev_soil(p) = -raiw*(forc_q(c) - qg_soil(c)) - qflx_ev_h2osfc(p) = -raiw*(forc_q(c) - qg_h2osfc(c)) + if (use_nvp) then + ! [PORTED by Hui Tang: per-surface raiw avoids soilresis cross-coupling] + qflx_ev_snow(p) = -raiw_snow * (forc_q(c) - qg_snow(c)) + qflx_ev_soil(p) = -raiw_soil * (forc_q(c) - qg_soil(c)) + qflx_ev_h2osfc(p) = -raiw_h2osfc * (forc_q(c) - qg_h2osfc(c)) + else + qflx_ev_snow(p) = -raiw*(forc_q(c) - qg_snow(c)) + qflx_ev_soil(p) = -raiw*(forc_q(c) - qg_soil(c)) + qflx_ev_h2osfc(p) = -raiw*(forc_q(c) - qg_h2osfc(c)) + end if ! [PORTED by Hui Tang: NVP evaporation flux for bare ground, analogous to snow/h2osfc] - ! Zero when NVP is buried under snow (snl < -1): qflx_ev_nvp_col drives ev_nvp_eff in + ! [PORTED by Hui Tang: gate on exposed NVP fraction (frac_nvp_eff>0) instead of snl>=-1, so + ! partial snow cover keeps NVP evaporation wherever NVP is still exposed (frac_nvp > frac_sno_eff).] + ! Zero when NVP is fully buried (frac_nvp_eff <= 0): qflx_ev_nvp_col drives ev_nvp_eff in ! NVPWaterBalance_Column; a non-zero value here when NVP is covered would add water to ! qflx_evap_tot_col without removing it from any tracked water store, causing errh2o. ! Only compute for the NVP veg patch (patch%is_veg), not the bareground gap patch. - ! SSR debug: I'm adding the "if use_fates" wrapper because of previous "Reference to undefined - ! POINTER PATCH%IS_VEG" errors above. Not sure if this is going to help. It might be because - ! patch%is_veg is only allocated when using FATES, which is why I'm trying this, but use_nvp - ! should not ever be true if not using FATES. if (use_fates) then - if (use_nvp .and. patch%is_veg(p) .and. col%frac_nvp(c) > 0._r8 .and. col%snl(c) >= -1) then - qflx_ev_nvp(p) = -raiw*(forc_q(c) - qg_nvp(c)) + if (use_nvp .and. patch%is_veg(p) .and. frac_nvp_eff > 0._r8) then + qflx_ev_nvp(p) = -raiw_nvp*(forc_q(c) - qg_nvp(c)) else qflx_ev_nvp(p) = 0._r8 ! [PORTED by Hui Tang: zero NVP ev flux when buried under snow or condition unmet] end if @@ -544,6 +627,14 @@ subroutine BareGroundFluxes(bounds, num_noexposedvegp, filter_noexposedvegp, & qflx_ev_nvp(p) = 0._r8 end if + ! [PORTED by Hui Tang: removed the NVP per-surface qflx_evap_tot rebuild here — it was + ! dead code. SoilFluxes runs after BareGroundFluxes and unconditionally recomputes + ! qflx_evap_tot = qflx_evap_veg + qflx_evap_grnd_eff (SoilFluxesMod.F90 bgp2_loop_2), + ! and nothing reads qflx_evap_tot in between (driver consumes it only at clm_driver.F90 + ! ~line 1753, after SoilFluxes). The per-surface area-weighting now lives in + ! qflx_evap_grnd_eff in SoilFluxesMod. Restored the original unconditional CLM line.] + qflx_evap_tot(p) = qflx_evap_soi(p) + ! 2 m height air temperature t_ref2m(p) = thm(p) + temp1(p)*dth(p)*(1._r8/temp12m(p) - 1._r8/temp1(p)) From 428302ec6f102cedf5e36ea637b4aba0007ef49f Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Thu, 11 Jun 2026 15:08:46 +0300 Subject: [PATCH 094/113] Allow per-surface eflx_sh_nvp when nvp is partially covered by snow. --- src/biogeophys/BareGroundFluxesMod.F90 | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/biogeophys/BareGroundFluxesMod.F90 b/src/biogeophys/BareGroundFluxesMod.F90 index 86091f3759..d90e8221ec 100644 --- a/src/biogeophys/BareGroundFluxesMod.F90 +++ b/src/biogeophys/BareGroundFluxesMod.F90 @@ -578,14 +578,17 @@ subroutine BareGroundFluxes(bounds, num_noexposedvegp, filter_noexposedvegp, & eflx_sh_soil(p) = -raih*(thm(p)-t_soisno(c,1)) eflx_sh_h2osfc(p) = -raih*(thm(p)-t_h2osfc(c)) ! [PORTED by Hui Tang: NVP sensible heat flux for bare ground, analogous to snow/h2osfc] - ! Zero when NVP is buried under snow (snl < -1): the snow surface controls the energy balance. + ! [PORTED by Hui Tang: gate on exposed NVP fraction (frac_nvp_eff>0) instead of the binary + ! snow-layer count snl>=-1, so partial snow cover keeps NVP sensible heat wherever NVP is + ! still exposed (frac_nvp > frac_sno_eff). frac_nvp_eff set above (line ~515, frac_sno_eff based).] + ! Zero when NVP is fully buried (frac_nvp_eff <= 0): the snow surface controls the energy balance. ! Only compute for the NVP veg patch (patch%is_veg), not the bareground gap patch. ! SSR debug: I'm adding the "if use_fates" wrapper because of previous "Reference to undefined ! POINTER PATCH%IS_VEG" errors here. Not sure if this is going to help. It might be because ! patch%is_veg is only allocated when using FATES, which is why I'm trying this, but use_nvp ! should not ever be true if not using FATES. if (use_fates) then - if (use_nvp .and. patch%is_veg(p) .and. col%frac_nvp(c) > 0._r8 .and. col%snl(c) >= -1) then + if (use_nvp .and. patch%is_veg(p) .and. frac_nvp_eff > 0._r8) then eflx_sh_nvp(p) = -raih*(thm(p)-t_nvp_col(c)) else eflx_sh_nvp(p) = 0._r8 ! [PORTED by Hui Tang: zero NVP sh flux when buried under snow or condition unmet] From 12325bf31e9b9679f735c3f871b01e2d3985e2bb Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Thu, 11 Jun 2026 15:40:25 +0300 Subject: [PATCH 095/113] Cap the snow layers with nvp layer to the same number as that without nvp layer (lower bound -11). --- src/biogeophys/SnowHydrologyMod.F90 | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/biogeophys/SnowHydrologyMod.F90 b/src/biogeophys/SnowHydrologyMod.F90 index aaea23d05c..6347e1dbcb 100644 --- a/src/biogeophys/SnowHydrologyMod.F90 +++ b/src/biogeophys/SnowHydrologyMod.F90 @@ -2781,8 +2781,15 @@ subroutine DivideSnowLayers(bounds, num_snowc, filter_snowc, & ! number of layers (msno) may increase during the loop. ! Impose k < nlevsno; the special case 'k == nlevsno' is not relevant, ! as it is neither allowed to subdivide nor does it have layers below. + ! [PORTED by Hui Tang: when NVP occupies index j=0, the valid snow indices are only + ! -nlevsno+1 .. -1 (nlevsno-1 slots), because j=0 is reserved for the NVP layer. Cap the + ! subdivision at nlevsno-1 in that case so msno can reach at most nlevsno-1 and the later + ! reconstruction snl = -(msno+1) stays >= -nlevsno (i.e. snl+1 >= -nlevsno+1, in bounds). + ! Without this, a deep snowpack subdivides to msno=nlevsno -> snl=-(nlevsno+1) -> snl+1 + ! one below the t_soisno lower bound (Fortran "Index '-12' below lower bound -11" crash).] k = 1 - loop_layers: do while( k <= msno .and. k < nlevsno ) + loop_layers: do while( k <= msno .and. & + k < nlevsno - merge(1, 0, use_nvp .and. col%jbot_sno(c) == -1) ) ! Current layer is bottom layer if (k == msno) then From d8b5f5ef50d2570a343e4e1ada4f302682797394 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Thu, 11 Jun 2026 15:49:59 +0300 Subject: [PATCH 096/113] 3-way split (snow / exposed-NVP / exposed bare-soil) for t_grnd calculation when NVP layer is present. --- src/biogeophys/BiogeophysPreFluxCalcsMod.F90 | 40 +++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/src/biogeophys/BiogeophysPreFluxCalcsMod.F90 b/src/biogeophys/BiogeophysPreFluxCalcsMod.F90 index 574af6f782..32424b1fcd 100644 --- a/src/biogeophys/BiogeophysPreFluxCalcsMod.F90 +++ b/src/biogeophys/BiogeophysPreFluxCalcsMod.F90 @@ -17,7 +17,7 @@ module BiogeophysPreFluxCalcsMod use LandunitType , only : lun use clm_varcon , only : spval use clm_varpar , only : nlevgrnd, nlevsno, nlevurb, nlevmaxurbgrnd - use clm_varctl , only : use_fates, z0param_method, iulog + use clm_varctl , only : use_fates, use_nvp, z0param_method, iulog use pftconMod , only : pftcon, noveg use column_varcon , only : icol_roof, icol_sunwall, icol_shadewall use landunit_varcon , only : istsoil, istcrop, istice @@ -257,6 +257,9 @@ subroutine CalcInitialTemperatureAndEnergyVars(bounds, & integer :: fp, p integer :: j real(r8) :: avmuir ! ir inverse optical depth per unit leaf area + ! [PORTED by Hui Tang: NVP t_grnd weighting] + real(r8) :: frac_nvp_eff_loc ! exposed NVP area fraction (snow buries NVP) + real(r8) :: frac_soil_loc ! bare-soil area fraction (residual after snow/NVP/h2osfc) character(len=*), parameter :: subname = 'CalcInitialTemperatureAndEnergyVars' !----------------------------------------------------------------------- @@ -332,11 +335,38 @@ subroutine CalcInitialTemperatureAndEnergyVars(bounds, & ! ground temperature is weighted average of exposed soil, snow, and h2osfc if (snl(c) < 0) then - t_grnd(c) = frac_sno_eff(c) * t_soisno(c,snl(c)+1) & - + (1.0_r8 - frac_sno_eff(c) - frac_h2osfc(c)) * t_soisno(c,1) & - + frac_h2osfc(c) * t_h2osfc(c) + ! [PORTED by Hui Tang: Phase 1c RESTORE (2026-06-11) — NVP-weighted t_grnd for snl<0, now + ! that the thermal solve applies the exposed-NVP surface flux at j=0 for snl<0 + ! (SetRHSVec_Snow/SetMatrix_Snow). eflx_sh_grnd = -raih*(thm-t_grnd) must carry the NVP + ! component so the errsoi accounting matches the per-surface flux the solve applies: + ! fse*sh_snow + frac_nvp_eff*sh_nvp + frac_soil*sh_soil + fh2o*sh_h2osfc. INVARIANT: keep + ! consistent with the SoilTemperatureMod j=0/j=1 surface BC and the t_grnd0 snl<0 branch.] + if (use_nvp .and. col%nvp_layer_active(c)) then + frac_nvp_eff_loc = min(1._r8 - frac_h2osfc(c) - frac_sno_eff(c), & + max(0._r8, col%frac_nvp(c) - frac_sno_eff(c))) + frac_soil_loc = max(0._r8, 1._r8 - frac_sno_eff(c) - frac_h2osfc(c) - frac_nvp_eff_loc) + t_grnd(c) = frac_sno_eff(c) * t_soisno(c,snl(c)+1) & + + frac_nvp_eff_loc * t_soisno(c,0) & + + frac_soil_loc * t_soisno(c,1) & + + frac_h2osfc(c) * t_h2osfc(c) + else + t_grnd(c) = frac_sno_eff(c) * t_soisno(c,snl(c)+1) & + + (1.0_r8 - frac_sno_eff(c) - frac_h2osfc(c)) * t_soisno(c,1) & + + frac_h2osfc(c) * t_h2osfc(c) + end if else - t_grnd(c) = (1 - frac_h2osfc(c)) * t_soisno(c,1) + frac_h2osfc(c) * t_h2osfc(c) + ! [PORTED by Hui Tang: include NVP layer temperature in t_grnd for snl==0 NVP columns. + ! frac_sno_eff=0 here, so formula reduces to min(1-frac_h2osfc, frac_nvp) for frac_nvp_eff.] + if (use_nvp .and. col%nvp_layer_active(c)) then + frac_nvp_eff_loc = min(1._r8 - frac_h2osfc(c) - frac_sno_eff(c), & + max(0._r8, col%frac_nvp(c) - frac_sno_eff(c))) + frac_soil_loc = max(0._r8, 1._r8 - frac_sno_eff(c) - frac_h2osfc(c) - frac_nvp_eff_loc) + t_grnd(c) = frac_nvp_eff_loc * t_soisno(c,0) & + + frac_soil_loc * t_soisno(c,1) & + + frac_h2osfc(c) * t_h2osfc(c) + else + t_grnd(c) = (1 - frac_h2osfc(c)) * t_soisno(c,1) + frac_h2osfc(c) * t_h2osfc(c) + end if end if ! Ground emissivity - only calculate for non-urban landunits From ea99b7686c0ef4198a6cee775a4024ade28344c0 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Thu, 11 Jun 2026 17:03:20 +0300 Subject: [PATCH 097/113] 3-way split (snow / exposed-NVP / bare-soil) for t_grnd0(c). --- src/biogeophys/SoilFluxesMod.F90 | 38 ++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/src/biogeophys/SoilFluxesMod.F90 b/src/biogeophys/SoilFluxesMod.F90 index 32b674bafb..4a942e7890 100644 --- a/src/biogeophys/SoilFluxesMod.F90 +++ b/src/biogeophys/SoilFluxesMod.F90 @@ -79,9 +79,14 @@ subroutine SoilFluxes (bounds, num_urbanl, filter_urbanl, & real(r8) :: t_grnd0(bounds%begc:bounds%endc) ! t_grnd of previous time step real(r8) :: lw_grnd real(r8) :: frac_nvp_eff ! [PORTED by Hui Tang: effective NVP fraction for LW weighting] + real(r8) :: frac_soil ! [PORTED by Hui Tang: exposed bare-soil fraction (1-fsno-fh2osfc-fnvp)] + real(r8) :: qflx_evap_grnd_eff ! [PORTED by Hui Tang: per-surface ground evap total for energy/diagnostic consistency] real(r8) :: evaporation_limit ! top layer moisture available for evaporation real(r8) :: evaporation_demand ! evaporative demand real(r8) :: heat_store_diag ! [PORTED by Hui Tang: errsoi diagnostic - heat storage sum] + real(r8) :: wgt ! [PORTED by Hui Tang: errsoi per-layer diagnostic - applied frac weight] + real(r8) :: eflx_soil_grnd_nvp ! [PORTED by Hui Tang: VERIFY-ONLY candidate NVP-consistent errsoi input flux (W/m2)] + real(r8) :: errsoi_test ! [PORTED by Hui Tang: VERIFY-ONLY candidate errsoi residual using NVP-consistent input (W/m2)] !----------------------------------------------------------------------- associate( & @@ -180,11 +185,36 @@ subroutine SoilFluxes (bounds, num_urbanl, filter_urbanl, & ! flux corrections if (col%snl(c) < 0) then - t_grnd0(c) = frac_sno_eff(c) * tssbef(c,col%snl(c)+1) & - + (1 - frac_sno_eff(c) - frac_h2osfc(c)) * tssbef(c,1) & - + frac_h2osfc(c) * t_h2osfc_bef(c) + ! [PORTED by Hui Tang: Phase 1c RESTORE (2026-06-11) — NVP-weighted t_grnd0 for snl<0, + ! mirroring the BiogeophysPreFluxCalcsMod snl<0 restore. tinc = t_grnd - t_grnd0 must use + ! the NVP-weighted blend on both sides so the LW linearization emg*sb*t_grnd0^3*4*tinc in + ! eflx_soil_grnd matches the solve (which now applies the NVP surface flux at j=0).] + if (use_nvp .and. col%nvp_layer_active(c)) then + frac_nvp_eff = min(1._r8 - frac_h2osfc(c) - frac_sno_eff(c), & + max(0._r8, col%frac_nvp(c) - frac_sno_eff(c))) + frac_soil = max(0._r8, 1._r8 - frac_sno_eff(c) - frac_h2osfc(c) - frac_nvp_eff) + t_grnd0(c) = frac_sno_eff(c) * tssbef(c,col%snl(c)+1) & + + frac_nvp_eff * tssbef(c,0) & + + frac_soil * tssbef(c,1) & + + frac_h2osfc(c) * t_h2osfc_bef(c) + else + t_grnd0(c) = frac_sno_eff(c) * tssbef(c,col%snl(c)+1) & + + (1 - frac_sno_eff(c) - frac_h2osfc(c)) * tssbef(c,1) & + + frac_h2osfc(c) * t_h2osfc_bef(c) + end if else - t_grnd0(c) = (1 - frac_h2osfc(c)) * tssbef(c,1) + frac_h2osfc(c) * t_h2osfc_bef(c) + ! [PORTED by Hui Tang: include NVP layer temperature in t_grnd0 for snl==0 NVP columns. + ! Mirrors BiogeophysPreFluxCalcsMod snl==0 branch so tinc = t_grnd - t_grnd0 is consistent.] + if (use_nvp .and. col%nvp_layer_active(c)) then + frac_nvp_eff = min(1._r8 - frac_h2osfc(c) - frac_sno_eff(c), & + max(0._r8, col%frac_nvp(c) - frac_sno_eff(c))) + frac_soil = max(0._r8, 1._r8 - frac_sno_eff(c) - frac_h2osfc(c) - frac_nvp_eff) + t_grnd0(c) = frac_nvp_eff * tssbef(c,0) & + + frac_soil * tssbef(c,1) & + + frac_h2osfc(c) * t_h2osfc_bef(c) + else + t_grnd0(c) = (1 - frac_h2osfc(c)) * tssbef(c,1) + frac_h2osfc(c) * t_h2osfc_bef(c) + end if endif tinc(c) = t_grnd(c) - t_grnd0(c) From 71c57198a86e4cae270c760012b57cc8ca472adb Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Thu, 11 Jun 2026 17:06:19 +0300 Subject: [PATCH 098/113] Add 3-way split (snow / exposed-NVP / exposed bare-soil) for lw_grnd. --- src/biogeophys/SoilFluxesMod.F90 | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/biogeophys/SoilFluxesMod.F90 b/src/biogeophys/SoilFluxesMod.F90 index 4a942e7890..c975934c4b 100644 --- a/src/biogeophys/SoilFluxesMod.F90 +++ b/src/biogeophys/SoilFluxesMod.F90 @@ -623,9 +623,22 @@ subroutine SoilFluxes (bounds, num_urbanl, filter_urbanl, & j = col%snl(c)+1 if (.not. lun%urbpoi(l)) then - lw_grnd=(frac_sno_eff(c)*tssbef(c,col%snl(c)+1)**4 & - +(1._r8-frac_sno_eff(c)-frac_h2osfc(c))*tssbef(c,1)**4 & - +frac_h2osfc(c)*t_h2osfc_bef(c)**4) + ! [PORTED by Hui Tang: NVP-aware lw_grnd for eflx_lwrad_out — mirror the eflx_soil_grnd + ! lw_grnd (lines ~387-390). Without this, eflx_lwrad_out emits the NVP fraction at soil + ! temperature tssbef(c,1) while eflx_soil_grnd emits it at NVP temperature tssbef(c,0), + ! leaving errseb = -emg*sb*frac_nvp_eff*(tssbef(c,1)**4 - tssbef(c,0)**4) (~-3 W/m2 at + ! partial snow cover). At full snow frac_nvp_eff=0 and this reduces to the standard form.] + if (use_nvp .and. col%nvp_layer_active(c)) then + frac_nvp_eff = min(1._r8 - frac_h2osfc(c) - frac_sno_eff(c), max(0._r8, col%frac_nvp(c) - frac_sno_eff(c))) + lw_grnd = frac_sno_eff(c) * tssbef(c,col%snl(c)+1)**4 & + + frac_nvp_eff * tssbef(c,0)**4 & + + (1._r8 - frac_sno_eff(c) - frac_nvp_eff - frac_h2osfc(c)) * tssbef(c,1)**4 & + + frac_h2osfc(c) * t_h2osfc_bef(c)**4 + else + lw_grnd=(frac_sno_eff(c)*tssbef(c,col%snl(c)+1)**4 & + +(1._r8-frac_sno_eff(c)-frac_h2osfc(c))*tssbef(c,1)**4 & + +frac_h2osfc(c)*t_h2osfc_bef(c)**4) + end if eflx_lwrad_out(p) = ulrad(p) & + (1-frac_veg_nosno(p))*(1.-emg(c))*forc_lwrad(c) & From 787e6df3fda2117359ebd4fd40586018deeef380 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Thu, 11 Jun 2026 17:16:56 +0300 Subject: [PATCH 099/113] Linearize qflx_ev_nvp with the NVP layer's own temperature increment instead of the bulk tinc. --- src/biogeophys/SoilFluxesMod.F90 | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/biogeophys/SoilFluxesMod.F90 b/src/biogeophys/SoilFluxesMod.F90 index c975934c4b..5293bab889 100644 --- a/src/biogeophys/SoilFluxesMod.F90 +++ b/src/biogeophys/SoilFluxesMod.F90 @@ -257,13 +257,33 @@ subroutine SoilFluxes (bounds, num_urbanl, filter_urbanl, & qflx_ev_soil(p) = qflx_ev_soil(p) + tinc(c)*cgrndl(p) qflx_ev_h2osfc(p) = qflx_ev_h2osfc(p) + tinc(c)*cgrndl(p) ! [PORTED by Hui Tang: apply linearization correction to NVP evaporation diagnostic] - ! Skip when NVP is buried under snow (snl < -1): qflx_ev_nvp was zeroed in + ! Skip when NVP is fully buried (frac_nvp_eff <= 0): qflx_ev_nvp was zeroed in ! BareGroundFluxesMod/CanopyFluxesMod and must remain zero to avoid a water ! balance error (non-zero qflx_ev_nvp_col with no corresponding water removal). - if (use_nvp .and. col%snl(c) < -1) then + ! [PORTED by Hui Tang: gate on exposed NVP fraction instead of the binary snl<-1, so + ! partial snow cover keeps the correction wherever NVP is still exposed. frac_nvp_eff + ! (frac_sno_eff based, matching this module) is computed locally since it is not yet set.] + frac_nvp_eff = min(1._r8 - frac_h2osfc(c) - frac_sno_eff(c), max(0._r8, & + col%frac_nvp(c) - frac_sno_eff(c))) + if (use_nvp .and. frac_nvp_eff <= 0._r8) then qflx_ev_nvp(p) = 0._r8 else - qflx_ev_nvp(p) = qflx_ev_nvp(p) + tinc(c)*cgrndl(p) + ! [PORTED by Hui Tang: linearize qflx_ev_nvp with the NVP layer's OWN + ! temperature increment instead of the bulk tinc. tinc = t_grnd(post, NVP-weighted) + ! - t_grnd0(pre, soil-only) conflates the temporal increment with a soil-vs-NVP basis + ! difference (~frac_nvp_eff*(t_nvp-t_soil)); for the thin, thermally-decoupled moss + ! that spurious term is several K and can flip qflx_ev_nvp negative (unphysical dew in + ! summer). When NVP is active (jbot_sno=-1) layer 0 IS the NVP layer for ALL snow + ! states (snow bottoms at j=-1, not 0), so t_soisno(c,0)/tssbef(c,0) are the NVP + ! post/pre-solve temperatures (t_nvp_col = t_soisno(c,0), SoilTemperatureMod:597). + ! cgrndl (bulk raiw*dqgdT) is retained — only the increment is corrected. Non-NVP + ! columns keep the standard bulk tinc correction.] + + ! The correct correction uses the NVP layer's own temperature increment and derivative: + ! qflx_ev_nvp += (t_soisno(c,0) − tssbef(c,0)) · cgrndl_nvp, where cgrndl_nvp = raiw_nvp·hr_nvp·qsatgdT_nvp. + + qflx_ev_nvp(p) = qflx_ev_nvp(p) + (t_soisno(c,0) - tssbef(c,0))*cgrndl(p) + end if endif end do From 34bb0fd52f18621ade794d2e6275b7a1a672bc03 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Thu, 11 Jun 2026 17:24:37 +0300 Subject: [PATCH 100/113] Make consistent frac_nvp_eff calculation for lw_grnd's 3-way split (snow / exposed-NVP / exposed bare-soil). --- src/biogeophys/SoilFluxesMod.F90 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/biogeophys/SoilFluxesMod.F90 b/src/biogeophys/SoilFluxesMod.F90 index 5293bab889..78f4f0ba22 100644 --- a/src/biogeophys/SoilFluxesMod.F90 +++ b/src/biogeophys/SoilFluxesMod.F90 @@ -432,7 +432,8 @@ subroutine SoilFluxes (bounds, num_urbanl, filter_urbanl, & ! When snl=0: frac_sno_eff=0 so the snow term vanishes and the formula reduces to ! three terms (NVP + bare soil + water). if (use_nvp .and. col%nvp_layer_active(c)) then - frac_nvp_eff = min(col%frac_nvp(c), max(0._r8, 1._r8 - frac_sno_eff(c) - frac_h2osfc(c))) + ! [PORTED by Hui Tang: re-wired frac_nvp_eff — snow buries NVP (frac_nvp - frac_sno_eff), cap = 1 - frac_h2osfc - frac_sno_eff] + frac_nvp_eff = min(1._r8 - frac_h2osfc(c) - frac_sno_eff(c), max(0._r8, col%frac_nvp(c) - frac_sno_eff(c))) lw_grnd = frac_sno_eff(c) * tssbef(c,col%snl(c)+1)**4 & + frac_nvp_eff * tssbef(c,0)**4 & + (1._r8 - frac_sno_eff(c) - frac_nvp_eff - frac_h2osfc(c)) * tssbef(c,1)**4 & From 1d943c284dd4b0da0c0a25bf6ff65b5d29dc0c59 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Thu, 11 Jun 2026 18:04:49 +0300 Subject: [PATCH 101/113] Use 3-way split qflx_evap_grnd_eff for eflx_soil_grnd, qflx_evap_tot and eflx_lh_grnd calculation. --- src/biogeophys/SoilFluxesMod.F90 | 34 +++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/src/biogeophys/SoilFluxesMod.F90 b/src/biogeophys/SoilFluxesMod.F90 index 78f4f0ba22..b8b81d7641 100644 --- a/src/biogeophys/SoilFluxesMod.F90 +++ b/src/biogeophys/SoilFluxesMod.F90 @@ -421,8 +421,31 @@ subroutine SoilFluxes (bounds, num_urbanl, filter_urbanl, & g = patch%gridcell(p) j = col%snl(c)+1 + ! [PORTED by Hui Tang: per-surface ground-evaporation total for energy/diagnostic + ! consistency under NVP. The bulk qflx_evap_soi = -raiw*dqh differs from the + ! area-weighted sum of the per-surface fluxes that actually remove water from each + ! store (snow/soil/h2osfc/NVP), because the raiw and qg blends are by area, not by + ! conductance. Drive the latent ENERGY (eflx_lh_tot, eflx_lh_grnd, and the latent + ! term of eflx_soil_grnd) and the evaporation diagnostic (qflx_evap_tot) from this + ! single per-surface sum so the energy leaving the column matches the water removed + ! from the stores. qflx_evap_soi is left unchanged for the soil/snow water-store + ! partitioning and capping above. The four fractions sum to 1 by construction + ! (frac_nvp_eff capped at 1-frac_sno_eff-frac_h2osfc), so the implicit-solve + ! linearization increment tinc*cgrndl carried in each per-surface flux aggregates to + ! the same value already added to qflx_evap_soi. For non-NVP / urban columns this + ! reduces exactly to qflx_evap_soi, leaving those paths bit-for-bit unchanged.] + if (use_nvp .and. col%nvp_layer_active(c)) then + frac_nvp_eff = min(1._r8 - frac_h2osfc(c) - frac_sno_eff(c), max(0._r8, & + col%frac_nvp(c) - frac_sno_eff(c))) + frac_soil = max(0._r8, 1._r8 - frac_sno_eff(c) - frac_h2osfc(c) - frac_nvp_eff) + qflx_evap_grnd_eff = frac_sno_eff(c)*qflx_ev_snow(p) + frac_h2osfc(c)*qflx_ev_h2osfc(p) & + + frac_nvp_eff *qflx_ev_nvp(p) + frac_soil *qflx_ev_soil(p) + else + qflx_evap_grnd_eff = qflx_evap_soi(p) + end if + ! Ground heat flux - + if (.not. lun%urbpoi(l)) then ! [PORTED by Hui Tang: fix lw_grnd for NVP — area-weighted LW emission including NVP] ! Standard formula uses frac_sno_eff/bare-soil/h2osfc fractions summing to 1. @@ -447,7 +470,7 @@ subroutine SoilFluxes (bounds, num_urbanl, filter_urbanl, & eflx_soil_grnd(p) = ((1._r8- frac_sno_eff(c))*sabg_soil(p) + frac_sno_eff(c)*sabg_snow(p)) + dlrad(p) & + (1-frac_veg_nosno(p))*emg(c)*forc_lwrad(c) & - emg(c)*sb*lw_grnd - emg(c)*sb*t_grnd0(c)**3*(4._r8*tinc(c)) & - - (eflx_sh_grnd(p)+qflx_evap_soi(p)*htvp(c)) + - (eflx_sh_grnd(p)+qflx_evap_grnd_eff*htvp(c)) ! [PORTED by Hui Tang: per-surface latent term] ! [PORTED by Hui Tang: NVP solar absorption in eflx_soil_grnd] ! SurfaceRadiationMod removes sabg_lyr(p,0) from sabg_soil (snl=0: line 781; ! snl<0: sabg_soil=sabg_lyr(p,1) only); add it back so NVP absorbed solar is @@ -486,9 +509,10 @@ subroutine SoilFluxes (bounds, num_urbanl, filter_urbanl, & eflx_sh_tot(p) = eflx_sh_veg(p) + eflx_sh_grnd(p) if (.not. lun%urbpoi(l)) eflx_sh_tot(p) = eflx_sh_tot(p) + eflx_sh_stem(p) - qflx_evap_tot(p) = qflx_evap_veg(p) + qflx_evap_soi(p) + ! [PORTED by Hui Tang: use per-surface ground-evap total (= qflx_evap_soi for non-NVP)] + qflx_evap_tot(p) = qflx_evap_veg(p) + qflx_evap_grnd_eff - eflx_lh_tot(p)= hvap*qflx_evap_veg(p) + htvp(c)*qflx_evap_soi(p) + eflx_lh_tot(p)= hvap*qflx_evap_veg(p) + htvp(c)*qflx_evap_grnd_eff if (lun%itype(l) == istsoil .or. lun%itype(l) == istcrop) then eflx_lh_tot_r(p)= eflx_lh_tot(p) eflx_sh_tot_r(p)= eflx_sh_tot(p) @@ -502,7 +526,7 @@ subroutine SoilFluxes (bounds, num_urbanl, filter_urbanl, & qflx_evap_can(p) = qflx_evap_veg(p) - qflx_tran_veg(p) eflx_lh_vege(p) = (qflx_evap_veg(p) - qflx_tran_veg(p)) * hvap eflx_lh_vegt(p) = qflx_tran_veg(p) * hvap - eflx_lh_grnd(p) = qflx_evap_soi(p) * htvp(c) + eflx_lh_grnd(p) = qflx_evap_grnd_eff * htvp(c) ! [PORTED by Hui Tang: per-surface latent term] end do call t_stopf('bgp2_loop_2') From 440cdc761d8b3b6a41b0f33f69ad9c9f51fa66ba Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Thu, 11 Jun 2026 22:32:16 +0300 Subject: [PATCH 102/113] Add excess NVP ice flux to snow as snow sources in errh2osno calculation. --- src/biogeophys/BalanceCheckMod.F90 | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/biogeophys/BalanceCheckMod.F90 b/src/biogeophys/BalanceCheckMod.F90 index 597c26d9c5..6144d293e5 100644 --- a/src/biogeophys/BalanceCheckMod.F90 +++ b/src/biogeophys/BalanceCheckMod.F90 @@ -821,6 +821,14 @@ subroutine BalanceCheck( bounds, & ! snow_sinks(c) = snow_sinks(c) + waterfluxbulk_ptr%qflx_nvp_drain_col(c) !end if + ! [PORTED by Hui Tang: excess NVP ice (above pore capacity) is pushed up into the + ! bottom snow layer (j=-1) in NVPWaterBalance_Column. That mass enters h2osno_total + ! (which excludes the NVP layer j=0) with no registered snow source, so book it + ! here as a snow source to keep errh2osno closed.] + if (associated(waterfluxbulk_ptr) .and. col%nvp_layer_active(c)) then + snow_sources(c) = snow_sources(c) + waterfluxbulk_ptr%qflx_nvp_to_snow_col(c) + end if + errh2osno(c) = (h2osno_total(c) - h2osno_old(c)) - (snow_sources(c) - snow_sinks(c)) * dtime ! [PORTED by Hui Tang: CalculateTotalH2osno excludes j=0 (NVP) from h2osno_total, From 9355fa27cb427d37fe576b40bfa98f5b076cf68b Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Fri, 12 Jun 2026 21:09:16 +0300 Subject: [PATCH 103/113] Partitioning NVP radiation absorption to sabg_nvp (exposed) and sabg_lyr(p,0) (snow buried) parts. --- src/biogeophys/SurfaceRadiationMod.F90 | 85 +++++++++++++++++++++++--- 1 file changed, 76 insertions(+), 9 deletions(-) diff --git a/src/biogeophys/SurfaceRadiationMod.F90 b/src/biogeophys/SurfaceRadiationMod.F90 index ed6071aec0..6a111c2682 100644 --- a/src/biogeophys/SurfaceRadiationMod.F90 +++ b/src/biogeophys/SurfaceRadiationMod.F90 @@ -522,6 +522,11 @@ subroutine SurfaceRadiation(bounds, num_nourbanp, filter_nourbanp, & real(r8) :: cai(bounds%begp:bounds%endp,numrad) ! diffuse radiation absorbed by canopy (W/m**2) real(r8) :: dtime ! land model time step (sec) real(r8) :: sabg_snl_sum ! temporary, absorbed energy in all active snow layers [W/m2] + ! [PORTED by Hui Tang: partial-snow NVP blend locals] + real(r8) :: frac_nvp_eff_loc ! locally-computed exposed NVP area fraction + real(r8) :: f_exp_loc ! fraction of NVP area that is exposed (not snow-buried) + real(r8) :: sabg_nvp_beer ! Beer's law NVP absorption per unit column area [W/m2] + real(r8) :: sabg_sum_chk ! [PORTED by Hui Tang: sum(sabg_lyr) (+ sabg_nvp for snl==0) for the SNICAR conservation check] real(r8) :: absrad_pur ! temp: absorbed solar radiation by pure snow [W/m2] real(r8) :: absrad_bc ! temp: absorbed solar radiation without BC [W/m2] real(r8) :: absrad_oc ! temp: absorbed solar radiation without OC [W/m2] @@ -592,6 +597,8 @@ subroutine SurfaceRadiation(bounds, num_nourbanp, filter_nourbanp, & sabg => solarabs_inst%sabg_patch , & ! Output: [real(r8) (:) ] solar radiation absorbed by ground (W/m**2) sabg_pen => solarabs_inst%sabg_pen_patch , & ! Output: [real(r8) (:) ] solar (rural) radiation penetrating top soisno layer (W/m**2) sabg_soil => solarabs_inst%sabg_soil_patch , & ! Output: [real(r8) (:) ] solar radiation absorbed by soil (W/m**2) + sabg_nvp => solarabs_inst%sabg_nvp_patch , & ! [PORTED by Hui Tang: solar absorbed by exposed NVP moss surface (W/m**2)] + sabg_soil_bandloop => solarabs_inst%sabg_soil_bandloop_patch , & ! [PORTED by Hui Tang: band-loop ground absorption snapshot] sabg_snow => solarabs_inst%sabg_snow_patch , & ! Output: [real(r8) (:) ] solar radiation absorbed by snow (W/m**2) sabg_lyr => solarabs_inst%sabg_lyr_patch , & ! Output: [real(r8) (:,:) ] absorbed radiative flux (patch,lyr) [W/m2] fsr_nir_d => solarabs_inst%fsr_nir_d_patch , & ! Output: [real(r8) (:) ] reflected direct beam nir solar radiation (W/m**2) @@ -638,7 +645,8 @@ subroutine SurfaceRadiation(bounds, num_nourbanp, filter_nourbanp, & fsds_sno_nd => surfrad_inst%fsds_sno_nd_patch , & ! Output: [real(r8) (:) ] incident near-IR, direct radiation on snow (for history files) (patch) [W/m2] fsds_sno_vi => surfrad_inst%fsds_sno_vi_patch , & ! Output: [real(r8) (:) ] incident visible, diffuse radiation on snow (for history files) (patch) [W/m2] fsds_sno_ni => surfrad_inst%fsds_sno_ni_patch , & ! Output: [real(r8) (:) ] incident near-IR, diffuse radiation on snow (for history files) (patch) [W/m2] - frac_sno_eff => waterdiagnosticbulk_inst%frac_sno_eff_col & !Input: + frac_sno_eff => waterdiagnosticbulk_inst%frac_sno_eff_col , & !Input: + frac_h2osfc => waterdiagnosticbulk_inst%frac_h2osfc_col & !Input: [real(r8) (:)] fraction of ground covered by surface water (0 to 1) ) @@ -653,6 +661,8 @@ subroutine SurfaceRadiation(bounds, num_nourbanp, filter_nourbanp, & g = patch%gridcell(p) sabg_soil(p) = 0._r8 + sabg_soil_bandloop(p) = 0._r8 ! [PORTED by Hui Tang] + if (use_nvp) sabg_nvp(p) = 0._r8 ! [PORTED by Hui Tang: exposed-NVP surface solar] sabg_snow(p) = 0._r8 sabg(p) = 0._r8 sabv(p) = 0._r8 @@ -755,6 +765,13 @@ subroutine SurfaceRadiation(bounds, num_nourbanp, filter_nourbanp, & sub_surf_abs_SW(p) = 0._r8 + ! [PORTED by Hui Tang: snapshot the band-loop (albsod) ground absorption BEFORE the NVP + ! carve-out (CASE1, sabg_soil -= sabg_lyr0) and the SNICAR snow reassignment (CASE2, + ! sabg_soil = sabg_lyr(p,1)) overwrite sabg_soil. This raw value lumps NVP+soil absorption + ! for the exposed surface and is used only to build the diagnostic SABG tile in + ! SoilTemperatureMod (true exposed-surface absorption during melt). Diagnostic-only.] + sabg_soil_bandloop(p) = sabg_soil(p) + ! CASE1: No snow layers: all energy is absorbed in top soil layer if (snl(c) == 0) then sabg_lyr(p,:) = 0._r8 @@ -770,15 +787,26 @@ subroutine SurfaceRadiation(bounds, num_nourbanp, filter_nourbanp, & ! use_nvp check with array access in one .and. dereferences a null pointer.] if (use_nvp) then if (col%nvp_layer_active(patch%column(p))) then - sabg_lyr(p,0) = 0._r8 + ! [PORTED by Hui Tang: snl==0 — moss is fully exposed (no snow). The Beer's-law + ! absorption is the moss SURFACE solar -> store as sabg_nvp (analogous to sabg_soil), + ! carve it out of the soil layer/sabg_soil, and set the internal sabg_lyr(p,0)=0 + ! (there is no snow above the moss, so no buried/SNICAR internal absorption).] + sabg_nvp(p) = 0._r8 do ib = 1, nband - sabg_lyr(p,0) = sabg_lyr(p,0) + & + sabg_nvp(p) = sabg_nvp(p) + & surfalb_inst%fabd_nvp_col(c,ib) * trd(p,ib) + & surfalb_inst%fabi_nvp_col(c,ib) * tri(p,ib) end do - sabg_lyr(p,0) = max(0._r8, min(sabg_lyr(p,0), sabg_lyr(p,1))) - sabg_lyr(p,1) = sabg_lyr(p,1) - sabg_lyr(p,0) - sabg_soil(p) = sabg_soil(p) - sabg_lyr(p,0) + sabg_nvp(p) = max(0._r8, min(sabg_nvp(p), sabg_lyr(p,1))) + sabg_lyr(p,1) = sabg_lyr(p,1) - sabg_nvp(p) + sabg_soil(p) = sabg_soil(p) - sabg_nvp(p) + ! [PORTED by Hui Tang (2026-06-12): keep sabg_lyr(p,0) = sabg_nvp so the SNICAR + ! energy-conservation guard (sum(sabg_lyr)==sabg_snow) is satisfied without special- + ! casing. This does NOT re-inject internal solar: the j=0 RHS internal term is + ! frac_sno_eff*sabg_lyr_col(c,0) = 0 for snl==0 (no snow), so the moss solar still + ! flows ONLY through hs_nvp (= frac_nvp_eff*sabg_nvp, thermostatted). The accounting + ! uses frac_nvp_eff*sabg_nvp (not sabg_lyr(p,0)).] + sabg_lyr(p,0) = sabg_nvp(p) end if end if ! [PORTED by Hui Tang: close outer use_nvp guard] @@ -858,14 +886,53 @@ subroutine SurfaceRadiation(bounds, num_nourbanp, filter_nourbanp, & endif endif endif + ! [PORTED by Hui Tang: partial-snow NVP blend — CASE2 (snl<0) with partially-exposed moss] + ! When snow is partial (frac_sno_eff < frac_nvp), fraction f_exp of the NVP is exposed + ! and receives unattenuated radiation via Beer's law (per column area, same formula as CASE1 + ! weighted by f_exp). The buried fraction (1-f_exp) receives SNICAR-attenuated radiation + ! that was set by the SNICAR loop above (per unit snow area); multiplying by frac_sno_eff + ! converts it to per column area. + ! Combined: sabg_lyr(p,0) = f_exp*beer_per_col + (1-f_exp)*frac_sno_eff*snicar_per_snow + ! [PORTED by Hui Tang (2026-06-11): SPLIT the moss solar into two un-weighted quantities, + ! exactly mirroring the soil pair (sabg_soil vs sabg_lyr(p,1)): + ! sabg_nvp(p) = Beer's-law absorption = EXPOSED-moss SURFACE solar (full). The + ! frac_nvp_eff exposure weighting is applied later in the thermal solve + ! (hs_nvp, via nvp_exp*wtcol) — so NO f_exp here (it would double-count). + ! sabg_lyr(p,0) = SNICAR moss-layer absorption = BURIED-moss INTERNAL solar (left as set + ! by the SNICAR loop above). The fse weighting is applied in the solve + ! (fse*sabg_lyr_col(c,0)) — so NO fse pre-weighting here. + ! This replaces the old blend sabg_lyr(p,0)=f_exp*beer+(1-f_exp)*fse*snicar, which both + ! double-weighted the fractions AND put the exposed solar internally (no -dhsdT surface + ! thermostat) -> moss overheating.] + if (use_nvp) then + if (col%nvp_layer_active(c)) then + if (col%frac_nvp(c) > 0._r8) then + sabg_nvp_beer = 0._r8 + do ib = 1, nband + sabg_nvp_beer = sabg_nvp_beer + & + surfalb_inst%fabd_nvp_col(c,ib) * trd(p,ib) + & + surfalb_inst%fabi_nvp_col(c,ib) * tri(p,ib) + end do + sabg_nvp(p) = max(0._r8, sabg_nvp_beer) + end if + end if ! [PORTED by Hui Tang: close nvp_layer_active guard] + end if ! [PORTED by Hui Tang: close use_nvp guard] endif ! This situation should not happen: - if (abs(sum(sabg_lyr(p,:))-sabg_snow(p)) > 0.00001_r8) then + ! [PORTED by Hui Tang: skip endrun for NVP partial-snow — after the Beer's-law blend above, + ! sabg_lyr(p,0) is per column area, so sum(sabg_lyr) intentionally exceeds sabg_snow + ! (per snow area) when snl<0 and frac_sno_eff < frac_nvp] + ! [PORTED by Hui Tang (2026-06-12): sabg_lyr(p,0)=sabg_nvp for snl==0 (set above), so the + ! moss is in sum(sabg_lyr) and this check passes unchanged — no NVP special-casing needed.] + sabg_sum_chk = sum(sabg_lyr(p,:)) + if (abs(sabg_sum_chk-sabg_snow(p)) > 0.00001_r8 .and. & + .not. (use_nvp .and. col%nvp_layer_active(c) .and. & + snl(c) < 0 .and. frac_sno_eff(c) < col%frac_nvp(c))) then write(iulog,*)"SNICAR ERROR: Absorbed ground radiation not equal to summed snow layer radiation" - write(iulog,*)"Diff = ",sum(sabg_lyr(p,:))-sabg_snow(p) + write(iulog,*)"Diff = ",sabg_sum_chk-sabg_snow(p) write(iulog,*)"sabg_snow(p)= ",sabg_snow(p) - write(iulog,*)"sabg_sum(p) = ",sum(sabg_lyr(p,:)) + write(iulog,*)"sabg_sum(p) = ",sabg_sum_chk write(iulog,*)"snl(c) = ",snl(c) write(iulog,*)"flx_absdv1 = ",trd(p,1)*(1.-albgrd(c,1)) write(iulog,*)"flx_absdv2 = ",sum(flx_absdv(c,:))*trd(p,1) From cfc4773837482c0d9e7f63942abc395717fad677 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Sun, 14 Jun 2026 22:55:06 +0300 Subject: [PATCH 104/113] Send alb_nvp_gnd (per-moss) to CLM surface albedo calculation. --- src/utils/clmfates_interfaceMod.F90 | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/utils/clmfates_interfaceMod.F90 b/src/utils/clmfates_interfaceMod.F90 index 7238efbea4..de01a929c9 100644 --- a/src/utils/clmfates_interfaceMod.F90 +++ b/src/utils/clmfates_interfaceMod.F90 @@ -3044,6 +3044,7 @@ subroutine wrap_canopy_radiation(this, bounds_clump, nc, fcansno, surfalb_inst) ! [PORTED by Hui Tang: NVP absorptance patch→col aggregation] integer :: ib ! band index integer :: npatches_site ! patch count in site + real(r8) :: nvp_frac_sum ! [PORTED by Hui Tang: Σ nvp_frac_pa for coverage-weighted alb_nvp_gnd_col] ! [PORTED by Hui Tang: NVP Beer's law k now read from fates_params_default.json via nvp_extinction_coeff] call t_startf('fates_wrapcanopyradiation') @@ -3160,6 +3161,21 @@ subroutine wrap_canopy_radiation(this, bounds_clump, nc, fcansno, surfalb_inst) surfalb_inst%nvp_omega_vis_col(c) = surfalb_inst%nvp_omega_vis_col(c) / real(npatches_site, r8) surfalb_inst%nvp_omega_nir_col(c) = surfalb_inst%nvp_omega_nir_col(c) / real(npatches_site, r8) end if + ! [PORTED by Hui Tang (2026-06-13): NVP moss ground reflectance, COVERAGE-weighted mean over + ! patches. alb_nvp_gnd is an INTENSIVE reflectance (the value where moss exists), so it must + ! NOT be diluted by bare patches (which have alb_nvp_gnd_pa=0, nvp_frac_pa=0); the coverage is + ! supplied separately by nvp_frac_eff in the SurfaceAlbedoMod blend. =0 when no moss present.] + surfalb_inst%alb_nvp_gnd_col(c) = 0._r8 + nvp_frac_sum = 0._r8 + do ifp = 1, npatches_site + surfalb_inst%alb_nvp_gnd_col(c) = surfalb_inst%alb_nvp_gnd_col(c) + & + this%fates(nc)%bc_out(s)%alb_nvp_gnd_pa(ifp) * & + this%fates(nc)%bc_out(s)%nvp_frac_pa(ifp) + nvp_frac_sum = nvp_frac_sum + this%fates(nc)%bc_out(s)%nvp_frac_pa(ifp) + end do + if (nvp_frac_sum > 1.e-5_r8) then + surfalb_inst%alb_nvp_gnd_col(c) = surfalb_inst%alb_nvp_gnd_col(c) / nvp_frac_sum + end if end if end do From b14e92285424c8c27fd445a8544e240dd7e805b9 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Sun, 14 Jun 2026 23:14:30 +0300 Subject: [PATCH 105/113] Add nvp albedo Beer-effective ground albedo into the blend of ground albedo --- src/biogeophys/SurfaceAlbedoMod.F90 | 22 ++++++++++++++++++++++ src/biogeophys/SurfaceAlbedoType.F90 | 4 ++++ 2 files changed, 26 insertions(+) diff --git a/src/biogeophys/SurfaceAlbedoMod.F90 b/src/biogeophys/SurfaceAlbedoMod.F90 index 80d3d01319..02fc644207 100644 --- a/src/biogeophys/SurfaceAlbedoMod.F90 +++ b/src/biogeophys/SurfaceAlbedoMod.F90 @@ -368,6 +368,8 @@ subroutine SurfaceAlbedo(bounds,nc, & real(r8) :: albsfc (bounds%begc:bounds%endc,numrad) ! albedo of surface underneath snow (col,bnd) real(r8) :: albsnd(bounds%begc:bounds%endc,numrad) ! snow albedo (direct) real(r8) :: albsni(bounds%begc:bounds%endc,numrad) ! snow albedo (diffuse) + real(r8) :: frac_nvp_eff_alb ! [PORTED by Hui Tang: exposed-moss column fraction for the ground-albedo blend] + real(r8) :: T_nvp ! [PORTED by Hui Tang: per-moss-area transmittance exp(-k*lai) for the Beer-effective ground albedo] real(r8) :: albsnd_pur (bounds%begc:bounds%endc,numrad) ! direct pure snow albedo (radiative forcing) real(r8) :: albsni_pur (bounds%begc:bounds%endc,numrad) ! diffuse pure snow albedo (radiative forcing) real(r8) :: albsnd_bc (bounds%begc:bounds%endc,numrad) ! direct snow albedo without BC (radiative forcing) @@ -404,6 +406,7 @@ subroutine SurfaceAlbedo(bounds,nc, & esai => canopystate_inst%esai_patch , & ! Input: [real(r8) (:) ] one-sided stem area index with burying by snow frac_sno => waterdiagnosticbulk_inst%frac_sno_col , & ! Input: [real(r8) (:) ] fraction of ground covered by snow (0 to 1) + frac_sno_eff => waterdiagnosticbulk_inst%frac_sno_eff_col , & !Input: fcansno => waterdiagnosticbulk_inst%fcansno_patch , & ! Input: [real(r8) (:) ] fraction of canopy that is snow-covered (0 to 1) h2osoi_liq => waterstatebulk_inst%h2osoi_liq_col , & ! Input: [real(r8) (:,:) ] liquid water content (col,lyr) [kg/m2] h2osoi_ice => waterstatebulk_inst%h2osoi_ice_col , & ! Input: [real(r8) (:,:) ] ice lens content (col,lyr) [kg/m2] @@ -856,6 +859,25 @@ subroutine SurfaceAlbedo(bounds,nc, & albgrd(c,ib) = albsod(c,ib)*(1._r8-frac_sno(c)) + albsnd(c,ib)*frac_sno(c) albgri(c,ib) = albsoi(c,ib)*(1._r8-frac_sno(c)) + albsni(c,ib)*frac_sno(c) + ! [PORTED by Hui Tang (2026-06-13): blend the EXPOSED moss into the ground albedo so + ! sabg(p)=trd*(1-albgrd) carries it in BOTH snow regimes (replaces the snl==0-only in-FATES + ! gnd_alb blend). Perturbation form: replace frac_nvp_eff_alb of the snow-free ground with the + ! moss-area effective albedo. frac_nvp_eff_alb = max(0,frac_nvp-frac_sno) keeps the soil weight + ! (1-frac_sno)-frac_nvp_eff_alb >=0; reduces to frac_nvp at snl==0. Buried moss stays in SNICAR. + ! [PORTED by Hui Tang (2026-06-13): OPTION 2 — Beer-EFFECTIVE moss-area albedo (1st order): + ! 1-alb_eff = (1-alb_nvp)*(1-albsoil*T_nvp), T_nvp=exp(-k*lai)=exp(-nvp_tau_col/frac_nvp). + ! The moss area then absorbs moss(Beer) + soil-under-moss; the carve-out subtracts the Beer + ! sabg_nvp, so the soil remainder is the PHYSICAL (1-alb_nvp)*T*(1-albsoil) transmitted solar, + ! not the inflated opaque value. alb_nvp_gnd_col is the moss SURFACE reflectance (bc_out lag).] + if (use_nvp .and. col%nvp_layer_active(c)) then + frac_nvp_eff_alb = min(1._r8 - frac_sno(c), max(0._r8, col%frac_nvp(c) - frac_sno(c))) + T_nvp = exp(-surfalb_inst%nvp_tau_col(c)/col%frac_nvp(c)) ! exp(-k*lai), per moss area + albgrd(c,ib) = albgrd(c,ib) + frac_nvp_eff_alb* & + ( (1._r8 - (1._r8-surfalb_inst%alb_nvp_gnd_col(c))*(1._r8 - albsod(c,ib)*T_nvp)) - albsod(c,ib) ) + albgri(c,ib) = albgri(c,ib) + frac_nvp_eff_alb* & + ( (1._r8 - (1._r8-surfalb_inst%alb_nvp_gnd_col(c))*(1._r8 - albsoi(c,ib)*T_nvp)) - albsoi(c,ib) ) + end if + ! albedos for radiative forcing calculations: if (use_snicar_frc) then ! BC forcing albedo diff --git a/src/biogeophys/SurfaceAlbedoType.F90 b/src/biogeophys/SurfaceAlbedoType.F90 index 7b4a97b987..63707c9754 100644 --- a/src/biogeophys/SurfaceAlbedoType.F90 +++ b/src/biogeophys/SurfaceAlbedoType.F90 @@ -46,6 +46,9 @@ module SurfaceAlbedoType real(r8), pointer :: nvp_tau_col (:) ! col NVP optical depth (k*LAI*frac) [-] real(r8), pointer :: nvp_omega_vis_col (:) ! col NVP single-scatter albedo VIS [-] real(r8), pointer :: nvp_omega_nir_col (:) ! col NVP single-scatter albedo NIR [-] + ! [PORTED by Hui Tang (2026-06-13): NVP moss ground reflectance (band-independent), from FATES + ! bc_out%alb_nvp_gnd_pa; blended into albsod in SurfaceAlbedoMod so sabg(p) carries the exposed moss.] + real(r8), pointer :: alb_nvp_gnd_col (:) ! col NVP moss ground reflectance [-] real(r8), pointer :: albsod_col (:,:) ! col soil albedo: direct (col,bnd) [frc] real(r8), pointer :: albsoi_col (:,:) ! col soil albedo: diffuse (col,bnd) [frc] real(r8), pointer :: albsnd_hst_col (:,:) ! col snow albedo, direct , for history files (col,bnd) [frc] @@ -156,6 +159,7 @@ subroutine InitAllocate(this, bounds) allocate(this%fabi_nvp_col (begc:endc,numrad)) ; this%fabi_nvp_col (:,:) = 0._r8 ! [PORTED by Hui Tang: allocate NVP optical properties for SNICAR layer-0] allocate(this%nvp_tau_col (begc:endc)) ; this%nvp_tau_col (:) = 0._r8 + allocate(this%alb_nvp_gnd_col (begc:endc)) ; this%alb_nvp_gnd_col (:) = 0._r8 allocate(this%nvp_omega_vis_col (begc:endc)) ; this%nvp_omega_vis_col (:) = 0._r8 allocate(this%nvp_omega_nir_col (begc:endc)) ; this%nvp_omega_nir_col (:) = 0._r8 end if From 57403df0a6a8b264ffcef7f310fbb71187b8c435 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Sun, 14 Jun 2026 23:55:31 +0300 Subject: [PATCH 106/113] Add heat content of solid moss to heat(c). --- src/biogeophys/TotalWaterAndHeatMod.F90 | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/biogeophys/TotalWaterAndHeatMod.F90 b/src/biogeophys/TotalWaterAndHeatMod.F90 index cc2f1fecfb..cd22da0b2f 100644 --- a/src/biogeophys/TotalWaterAndHeatMod.F90 +++ b/src/biogeophys/TotalWaterAndHeatMod.F90 @@ -26,6 +26,7 @@ module TotalWaterAndHeatMod use column_varcon , only : icol_road_perv, icol_road_imperv use landunit_varcon , only : istdlak, istsoil,istcrop,istwet,istice use clm_varctl , only : iulog, use_nvp ! [PORTED by Hui Tang: use_nvp for NVP debug prints] + use NVPParamsMod , only : csol_nvp, watsat_nvp ! [PORTED by Hui Tang: NVP solid heat capacity for heat(c)] ! ! !PUBLIC TYPES: implicit none @@ -742,6 +743,17 @@ subroutine ComputeHeatNonLake(bounds, num_nolakec, filter_nolakec, & TempToHeat(temp = t_soisno(c,j), cv = (h2osoi_ice(c,j)*cpice)) end do + ! [PORTED by Hui Tang (2026-06-12): add the NVP-layer SOLID (dry-mass) heat content to heat(c). + ! The j=0 moss WATER is already counted in the loop above, but the moss SOLID was omitted + ! (AccumulateSoilHeatNonLake loops j>=1 only). dz(c,0)=col%dz_nvp = nvp_dz*frac_nvp*canopy_frac + ! already carries frac_nvp, so csol_nvp*(1-watsat_nvp)*dz(c,0) is the PER-COLUMN solid heat + ! content. This makes heat(c) consistent with the per-moss cv (=old_cv/frac_nvp), for which + ! frac_nvp*cv_moss = solid_percol + water — both terms now tracked. Closes the total-energy gap.] + if (use_nvp .and. col%jbot_sno(c) == -1) then + heat_dry_mass(c) = heat_dry_mass(c) + & + TempToHeat(temp = t_soisno(c,0), cv = (csol_nvp*(1._r8-watsat_nvp)*dz(c,0))) + end if + if (col%hydrologically_active(c)) then ! NOTE(wjs, 2017-03-23) Water in the unconfined aquifer currently doesn't have ! an explicit temperature; thus, we only add its latent heat of From fc9e3f620d9e6572a56ab18799548b3edc137763 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Mon, 15 Jun 2026 00:31:00 +0300 Subject: [PATCH 107/113] Modify the blend of sabg among sabg_soil, sabg_nvp, and sabg_snow. --- src/biogeophys/SurfaceRadiationMod.F90 | 65 +++++++++++++++++++------- 1 file changed, 48 insertions(+), 17 deletions(-) diff --git a/src/biogeophys/SurfaceRadiationMod.F90 b/src/biogeophys/SurfaceRadiationMod.F90 index 6a111c2682..bc3efc5b16 100644 --- a/src/biogeophys/SurfaceRadiationMod.F90 +++ b/src/biogeophys/SurfaceRadiationMod.F90 @@ -482,7 +482,7 @@ subroutine SurfaceRadiation(bounds, num_nourbanp, filter_nourbanp, & use clm_varcon , only : spval use landunit_varcon , only : istsoil, istcrop use clm_varctl , only : use_subgrid_fluxes, use_snicar_frc, iulog, use_SSRE, do_sno_oc - use clm_time_manager , only : get_step_size_real, is_near_local_noon + use clm_time_manager , only : get_step_size_real, is_near_local_noon, get_nstep ! [PORTED by Hui Tang: get_nstep for Phase-4 diagnostic] use abortutils , only : endrun ! ! !ARGUMENTS: @@ -645,14 +645,13 @@ subroutine SurfaceRadiation(bounds, num_nourbanp, filter_nourbanp, & fsds_sno_nd => surfrad_inst%fsds_sno_nd_patch , & ! Output: [real(r8) (:) ] incident near-IR, direct radiation on snow (for history files) (patch) [W/m2] fsds_sno_vi => surfrad_inst%fsds_sno_vi_patch , & ! Output: [real(r8) (:) ] incident visible, diffuse radiation on snow (for history files) (patch) [W/m2] fsds_sno_ni => surfrad_inst%fsds_sno_ni_patch , & ! Output: [real(r8) (:) ] incident near-IR, diffuse radiation on snow (for history files) (patch) [W/m2] - frac_sno_eff => waterdiagnosticbulk_inst%frac_sno_eff_col , & !Input: - frac_h2osfc => waterdiagnosticbulk_inst%frac_h2osfc_col & !Input: [real(r8) (:)] fraction of ground covered by surface water (0 to 1) + frac_sno => waterdiagnosticbulk_inst%frac_sno_col & ! Input: [real(r8) (:) ] fraction of ground covered by snow (0 to 1) ) ! Determine seconds off current time step dtime = get_step_size_real() - +y ! Initialize fluxes do fp = 1,num_nourbanp @@ -797,9 +796,12 @@ subroutine SurfaceRadiation(bounds, num_nourbanp, filter_nourbanp, & surfalb_inst%fabd_nvp_col(c,ib) * trd(p,ib) + & surfalb_inst%fabi_nvp_col(c,ib) * tri(p,ib) end do - sabg_nvp(p) = max(0._r8, min(sabg_nvp(p), sabg_lyr(p,1))) + sabg_nvp(p) = max(0._r8, min(sabg_nvp(p), sabg_lyr(p,1))) ! per-column + + ![PORTED by Hui Tang: Soil patches receive the same amount of radiation, no matter it is under moss or not (exposed), per-area] sabg_lyr(p,1) = sabg_lyr(p,1) - sabg_nvp(p) sabg_soil(p) = sabg_soil(p) - sabg_nvp(p) + ! [PORTED by Hui Tang (2026-06-12): keep sabg_lyr(p,0) = sabg_nvp so the SNICAR ! energy-conservation guard (sum(sabg_lyr)==sabg_snow) is satisfied without special- ! casing. This does NOT re-inject internal solar: the j=0 RHS internal term is @@ -825,17 +827,6 @@ subroutine SurfaceRadiation(bounds, num_nourbanp, filter_nourbanp, & sub_surf_abs_SW(p) = sub_surf_abs_SW(p) + sabg_lyr(p,i) endif enddo - ! [PORTED by Hui Tang: snow - NVP layer-0 SNICAR] - ! When use_nvp and SNICAR NVP layer-0 is active, flx_absdv(c,0)/flx_absiv(c,0) - ! already hold NVP absorption (set by SNICAR_RT above), so sabg_lyr(p,0) is - ! correct from the SNICAR loop above. Correct sabg_soil to use SNICAR soil layer. - ! [PORTED by Hui Tang: nest the NVP guard — see line ~768 for rationale] - if (use_nvp) then - if (surfalb_inst%nvp_tau_col(c) > 0._r8) then - ! sabg_lyr(p,1) = SNICAR soil-layer absorption (excludes NVP); use it directly. - sabg_soil(p) = sabg_lyr(p,1) - end if - end if ! Divide absorbed by total, to get fraction absorbed in subsurface if (sabg_snl_sum /= 0._r8) then @@ -913,7 +904,47 @@ subroutine SurfaceRadiation(bounds, num_nourbanp, filter_nourbanp, & surfalb_inst%fabd_nvp_col(c,ib) * trd(p,ib) + & surfalb_inst%fabi_nvp_col(c,ib) * tri(p,ib) end do - sabg_nvp(p) = max(0._r8, sabg_nvp_beer) + sabg_nvp(p) = max(0._r8, min(sabg_nvp_beer, sabg(p)-sabg_snow(p))) ! per-column + + ! [PORTED by Hui Tang: snow - NVP layer-0 SNICAR] + ! When use_nvp and SNICAR NVP layer-0 is active, flx_absdv(c,0)/flx_absiv(c,0) + ! already hold NVP absorption (set by SNICAR_RT above), so sabg_lyr(p,0) is + ! correct from the SNICAR loop above. Correct sabg_soil to use SNICAR soil layer. + ! [PORTED by Hui Tang: nest the NVP guard — see line ~768 for rationale] + ! sabg_lyr(p,1) = SNICAR soil-layer absorption (excludes NVP); use it directly. + frac_nvp_eff_loc = min(1._r8 - frac_sno(c), max(0._r8, col%frac_nvp(c) - frac_sno(c))) + + ! [PORTED by Hui Tang (2026-06-13): guard the exposed-soil back-out against full snow + ! cover. When frac_sno_eff==1 the denominator (1-frac_sno_eff)=0 -> sabg_soil=Inf/NaN, + ! which then contaminates sw_grnd via the 0*NaN trap in SoilFluxesMod (the zero soil + ! weight does NOT cancel a NaN). At full snow cover there is no exposed soil, so + ! sabg_soil must be a finite 0.] + if (frac_sno(c) < 1._r8) then + sabg_soil(p) = (sabg(p) - sabg_snow(p)*frac_sno(c) - (frac_nvp_eff_loc/col%frac_nvp(c))*sabg_nvp(p))/(1-frac_sno(c)) + else + sabg_soil(p) = 0._r8 + end if + + ! [PORTED by Hui Tang (2026-06-13): Phase-4 diagnostic (snl<0 partial snow). Dumps the + ! raw ground-solar pieces so we can measure how sabg(p) (albgrd total, now incl. the + ! exposed moss via Phase 3) decomposes vs the FGR reconstruction and the SABG tile. + ! Offline: M_alb = sabg(p) - fse*sabg_snow - (1-fse)*sabg_soil_bandloop (opaque moss); + ! M_beer = nvp_exp*sabg_nvp; FGR_solar ≈ (1-fse)*sabg_lyr1 + fse*sabg_snow + M_beer; + ! SABG_tile = fse*sabg_snow + (1-fse)*bandloop + M_beer. Residual to close: sabg(p)-FGR + ! and the soil seam (1-fse)*(bandloop - sabg_lyr1). REMOVE after Phase 4 verified.] + if (frac_sno_eff(c) > 0._r8 .and. frac_sno_eff(c) < col%frac_nvp(c)) then + write(iulog,*) '[NVP P4 RAD] nstep,c,p=', get_nstep(), c, p, & + ' sabg=', sabg(p), ' sabg_snow=', sabg_snow(p), & + ' bandloop=', sabg_soil_bandloop(p), ' sabg_lyr0=', sabg_lyr(p,0), & + ' sabg_lyr1=', sabg_lyr(p,1), ' sabg_nvp=', sabg_nvp(p), & + ' fse=', frac_sno_eff(c), ' fsno=', frac_sno(c), & + ' frac_nvp=', col%frac_nvp(c), & + ' nvp_exp=', min(1._r8-frac_sno_eff(c), & + max(0._r8,col%frac_nvp(c)-frac_sno_eff(c)))/col%frac_nvp(c), & + ' FGRrecon=', (1._r8-frac_sno_eff(c))*sabg_lyr(p,1) + frac_sno_eff(c)*sabg_snow(p) & + + (min(1._r8-frac_sno_eff(c), & + max(0._r8,col%frac_nvp(c)-frac_sno_eff(c)))/col%frac_nvp(c))*sabg_nvp(p) + end if end if end if ! [PORTED by Hui Tang: close nvp_layer_active guard] end if ! [PORTED by Hui Tang: close use_nvp guard] From 6d7cf4ebffbcbefc98155cb155a3fe7778653002 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Mon, 15 Jun 2026 00:37:02 +0300 Subject: [PATCH 108/113] Add sabg_nvp_patch for nvp solar absorption. --- src/biogeophys/SolarAbsorbedType.F90 | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/biogeophys/SolarAbsorbedType.F90 b/src/biogeophys/SolarAbsorbedType.F90 index fa1de4a753..d483b1546a 100644 --- a/src/biogeophys/SolarAbsorbedType.F90 +++ b/src/biogeophys/SolarAbsorbedType.F90 @@ -30,8 +30,16 @@ module SolarAbsorbedType real(r8), pointer :: par240x_z_patch (:,:) ! 10-day running mean of maximum patch absorbed PAR for leaves in canopy layer (W/m**2) real(r8), pointer :: par24d_z_patch (:,:) ! daily accumulated absorbed PAR for leaves in canopy layer from midnight to current step(J/m**2) real(r8), pointer :: par24x_z_patch (:,:) ! daily max of patch absorbed PAR for leaves in canopy layer from midnight to current step(W/m**2) - real(r8), pointer :: sabg_soil_patch (:) ! patch solar radiation absorbed by soil (W/m**2) - real(r8), pointer :: sabg_snow_patch (:) ! patch solar radiation absorbed by snow (W/m**2) + real(r8), pointer :: sabg_soil_patch (:) ! patch solar radiation absorbed by soil (W/m**2) + ! [PORTED by Hui Tang: exposed-NVP moss SURFACE-absorbed solar (Beer's law), analogous to + ! sabg_soil_patch for bare soil. Full (un-exposure-weighted) value; the frac_nvp_eff weighting + ! is applied in the thermal solve (hs_nvp). Paired with sabg_lyr_patch(:,0) = buried/SNICAR + ! internal moss absorption (analogous to sabg_lyr_patch(:,1) for soil).] + real(r8), pointer :: sabg_nvp_patch (:) ! patch solar absorbed by exposed NVP moss surface (W/m**2) + ! [PORTED by Hui Tang: band-loop (albsod) ground absorption snapshot, before the NVP carve-out / + ! SNICAR snow reassignment overwrite sabg_soil; used only to build the diagnostic SABG tile] + real(r8), pointer :: sabg_soil_bandloop_patch (:) ! patch band-loop soil/ground absorption (W/m**2) + real(r8), pointer :: sabg_snow_patch (:) ! patch solar radiation absorbed by snow (W/m**2) real(r8), pointer :: sabg_patch (:) ! patch solar radiation absorbed by ground (W/m**2) real(r8), pointer :: sabg_chk_patch (:) ! patch fsno weighted sum (W/m**2) real(r8), pointer :: sabg_lyr_patch (:,:) ! patch absorbed radiation in each snow layer and top soil layer (pft,lyr) [W/m2] @@ -130,6 +138,8 @@ subroutine InitAllocate(this, bounds) allocate(this%sabg_lyr_patch (begp:endp,-nlevsno+1:1)) ; this%sabg_lyr_patch (:,:) = nan allocate(this%sabg_pen_patch (begp:endp)) ; this%sabg_pen_patch (:) = nan allocate(this%sabg_soil_patch (begp:endp)) ; this%sabg_soil_patch (:) = nan + allocate(this%sabg_nvp_patch (begp:endp)) ; this%sabg_nvp_patch (:) = nan ! [PORTED by Hui Tang] + allocate(this%sabg_soil_bandloop_patch(begp:endp)) ; this%sabg_soil_bandloop_patch(:) = nan ! [PORTED by Hui Tang] allocate(this%sabg_snow_patch (begp:endp)) ; this%sabg_snow_patch (:) = nan allocate(this%sabg_chk_patch (begp:endp)) ; this%sabg_chk_patch (:) = nan allocate(this%sabs_roof_dir_lun (begl:endl,1:numrad)) ; this%sabs_roof_dir_lun (:,:) = nan From 289f0d5eb662df17c7ff18f1b5de3873be950aa8 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Mon, 15 Jun 2026 00:46:07 +0300 Subject: [PATCH 109/113] Re-partition asbsorbed solar radiation into soil, nvp, and snow for eflx_soil_grnd. --- src/biogeophys/SoilFluxesMod.F90 | 107 +++++++++++++++++++++++++++++-- 1 file changed, 102 insertions(+), 5 deletions(-) diff --git a/src/biogeophys/SoilFluxesMod.F90 b/src/biogeophys/SoilFluxesMod.F90 index b8b81d7641..85e57ad7f9 100644 --- a/src/biogeophys/SoilFluxesMod.F90 +++ b/src/biogeophys/SoilFluxesMod.F90 @@ -45,7 +45,7 @@ subroutine SoilFluxes (bounds, num_urbanl, filter_urbanl, & ! Update surface fluxes based on the new ground temperature ! ! !USES: - use clm_time_manager , only : get_step_size_real + use clm_time_manager , only : get_step_size_real, get_nstep ! [PORTED by Hui Tang: get_nstep for NVP SEB diagnostic] use clm_varcon , only : hvap, cpair, grav, vkc, tfrz, sb use landunit_varcon , only : istsoil, istcrop use column_varcon , only : icol_roof, icol_sunwall, icol_shadewall, icol_road_perv @@ -102,6 +102,7 @@ subroutine SoilFluxes (bounds, num_urbanl, filter_urbanl, & h2osoi_ice => waterstatebulk_inst%h2osoi_ice_col , & ! Input: [real(r8) (:,:) ] ice lens (kg/m2) (new) h2osoi_liq => waterstatebulk_inst%h2osoi_liq_col , & ! Input: [real(r8) (:,:) ] liquid water (kg/m2) (new) sabg_soil => solarabs_inst%sabg_soil_patch , & ! Input: [real(r8) (:) ] solar radiation absorbed by soil (W/m**2) + sabg_nvp => solarabs_inst%sabg_nvp_patch , & ! [PORTED by Hui Tang: exposed-NVP moss surface solar (W/m2)] sabg_snow => solarabs_inst%sabg_snow_patch , & ! Input: [real(r8) (:) ] solar radiation absorbed by snow (W/m**2) sabg => solarabs_inst%sabg_patch , & ! Input: [real(r8) (:) ] solar radiation absorbed by ground (W/m**2) ! [PORTED by Hui Tang: NVP errsoi fix - solar by layer and NVP sensible heat] @@ -467,7 +468,18 @@ subroutine SoilFluxes (bounds, num_urbanl, filter_urbanl, & +frac_h2osfc(c)*t_h2osfc_bef(c)**4) end if - eflx_soil_grnd(p) = ((1._r8- frac_sno_eff(c))*sabg_soil(p) + frac_sno_eff(c)*sabg_snow(p)) + dlrad(p) & + + if (use_nvp .and. col%nvp_layer_active(c)) then + frac_nvp_eff = min(1._r8 - frac_sno_eff(c), max(0._r8, col%frac_nvp(c) - frac_sno_eff(c))) + eflx_soil_grnd(p) = ((1._r8- frac_sno_eff(c))*sabg_soil(p) & + + (frac_nvp_eff/col%frac_nvp(c))*sabg_nvp(p) & + + frac_sno_eff(c)*sabg_snow(p)) + dlrad(p) & + + (1-frac_veg_nosno(p))*emg(c)*forc_lwrad(c) & + - emg(c)*sb*lw_grnd - emg(c)*sb*t_grnd0(c)**3*(4._r8*tinc(c)) & + - (eflx_sh_grnd(p)+qflx_evap_grnd_eff*htvp(c)) + + else + eflx_soil_grnd(p) = ((1._r8- frac_sno_eff(c))*sabg_soil(p) + frac_sno_eff(c)*sabg_snow(p)) + dlrad(p) & + (1-frac_veg_nosno(p))*emg(c)*forc_lwrad(c) & - emg(c)*sb*lw_grnd - emg(c)*sb*t_grnd0(c)**3*(4._r8*tinc(c)) & - (eflx_sh_grnd(p)+qflx_evap_grnd_eff*htvp(c)) ! [PORTED by Hui Tang: per-surface latent term] @@ -480,8 +492,37 @@ subroutine SoilFluxes (bounds, num_urbanl, filter_urbanl, & ! fabd_nvp = nvp_frac * (1 - exp(-k * lai_nvp)) ! so sabg_lyr(p,0) = fabd_nvp * trd + fabi_nvp * tri is already per unit ground area. ! Applying frac_nvp_eff again would double-count the NVP coverage fraction. - if (use_nvp .and. col%nvp_layer_active(c)) then - eflx_soil_grnd(p) = eflx_soil_grnd(p) + sabg_lyr(p,0) + ! [PORTED by Hui Tang (2026-06-12): add the EXPOSED-moss SURFACE solar at frac_nvp_eff + ! weight, EXACTLY matching the solve: hs_nvp carries sabg_nvp*nvp_exp*wtcol = + ! sabg_nvp*frac_nvp_eff (the surface solar injected at j=0). The BURIED-moss internal + ! absorption sabg_lyr(p,0) is part of the SNICAR sum and is therefore ALREADY inside + ! frac_sno_eff*sabg_snow above (exactly as sabg_lyr(p,1) is for soil) — re-adding it + ! would double-count. For snl==0: frac_nvp_eff=frac_nvp, sabg_lyr(p,0)=0. Must mirror + ! sabg_chk in SoilTemperatureMod (errseb/errsoi consistency invariant).] + ! [PORTED by Hui Tang (2026-06-12): sabg_nvp is per-COLUMN (fabd_nvp carries nvp_frac), so + ! the EXPOSED-moss surface solar into the column is nvp_exp*sabg_nvp = + ! (frac_nvp_eff/frac_nvp)*sabg_nvp (was frac_nvp_eff*sabg_nvp, which double-counted the + ! coverage -> offline SABG vs FGR positive residual). Matches sabg_chk and the hs_nvp deposit.] + end if + + ! [PORTED by Hui Tang: DEBUG (Bug B/C) — decompose eflx_soil_grnd for the NVP snow-covered + ! column to isolate the surface-energy-balance residual. Fires whenever any snow covers + ! the NVP layer (frac_sno_eff > 0): partial cover = spring/autumn Bug B (~-3 W/m2), full + ! cover (frac_sno_eff==1) = Bug C (~-61 W/m2 at autumn freeze-up, buried NVP). Compare + ! these ground terms with the column totals (eflx_sh_tot, eflx_lh_tot, eflx_lwrad_net) in + ! the BalanceCheck dump. Remove once both residuals are identified.] + if (use_nvp .and. col%nvp_layer_active(c) .and. frac_sno_eff(c) > 0._r8) then + write(iulog,*) '[NVP DBG SEB] nstep=', get_nstep(), ' c=', c, ' p=', p, & + ' frac_sno_eff=', frac_sno_eff(c), ' frac_h2osfc=', frac_h2osfc(c) + write(iulog,*) '[NVP DBG SEB] sw_grnd=', & + (1._r8-frac_sno_eff(c))*sabg_soil(p) + frac_sno_eff(c)*sabg_snow(p), & + ' sabg_lyr0=', sabg_lyr(p,0), ' dlrad=', dlrad(p) + write(iulog,*) '[NVP DBG SEB] lw_in=', (1-frac_veg_nosno(p))*emg(c)*forc_lwrad(c), & + ' lw_emit=', emg(c)*sb*lw_grnd, ' lw_lin=', emg(c)*sb*t_grnd0(c)**3*(4._r8*tinc(c)) + write(iulog,*) '[NVP DBG SEB] sh_grnd=', eflx_sh_grnd(p), & + ' lh_grnd=', qflx_evap_grnd_eff*htvp(c), ' eflx_soil_grnd=', eflx_soil_grnd(p) + write(iulog,*) '[NVP DBG SEB] t_grnd0=', t_grnd0(c), ' tinc=', tinc(c), & + ' tssbef0=', tssbef(c,0), ' tssbef1=', tssbef(c,1) end if if (lun%itype(l) == istsoil .or. lun%itype(l) == istcrop) then @@ -537,10 +578,18 @@ subroutine SoilFluxes (bounds, num_urbanl, filter_urbanl, & do fp = 1,num_nolakep p = filter_nolakep(fp) c = patch%column(p) + ! [PORTED by Hui Tang: errsoi input UNIFIED to the bulk eflx_soil_grnd for both snl==0 and + ! snl<0 (2026-06-11, snl==0 3-way refactor). Previously snl==0 used an NVP-basis + ! eflx_soil_grnd_nvp because the snl==0 solve "collapsed" the whole surface into the NVP layer + ! j=0 (single NVP surface). Now the snl==0 solve uses the 3-way split (exposed moss at j=0 + ! weight frac_nvp_eff, bare soil at j=1 weight frac_soil), so the matching errsoi is the + ! area-weighted bulk eflx_soil_grnd, whose lw_grnd / eflx_sh_grnd / qflx_evap_grnd_eff already + ! carry the NVP weighting via the NVP-weighted t_grnd. This is the same path snl<0 uses, so + ! the snow-appearance threshold is now continuous.] errsoi_patch(p) = eflx_soil_grnd(p) - xmf(c) - xmf_h2osfc(c) & - frac_h2osfc(c)*(t_h2osfc(c)-t_h2osfc_bef(c)) & *(c_h2osfc(c)/dtime) - errsoi_patch(p) = errsoi_patch(p)+eflx_h2osfc_to_snow_col(c) + errsoi_patch(p) = errsoi_patch(p)+eflx_h2osfc_to_snow_col(c) ! For urban sunwall, shadewall, and roof columns, the "soil" energy balance check ! must include the heat flux from the interior of the building. if (col%itype(c)==icol_sunwall .or. col%itype(c)==icol_shadewall .or. col%itype(c)==icol_roof) then @@ -648,6 +697,54 @@ subroutine SoilFluxes (bounds, num_urbanl, filter_urbanl, & eflx_soil_grnd(p) - xmf(c) - xmf_h2osfc(c) - heat_store_diag & + eflx_h2osfc_to_snow_col(c) & - frac_h2osfc(c)*(t_h2osfc(c)-t_h2osfc_bef(c))*(c_h2osfc(c)/dtime) + + ! [PORTED by Hui Tang: VERIFY-ONLY diagnostic — candidate NVP-consistent errsoi input. + ! Tests whether replacing eflx_soil_grnd's blended LW-emission + turbulent terms with the + ! NVP-specific fluxes that the temperature solve actually applied at j=0 (hs_nvp) closes + ! the energy balance. Solar (sabg_soil + sabg_lyr0) is kept as-is (already reconciled). + ! - LW emission : emg*sb*lw_grnd -> emg*sb*tssbef(c,0)**4 (lwrad_emit_nvp) + ! - LW lineariz. : t_grnd0/tinc blended -> NVP layer tssbef(c,0)/(t0-tbef0) + ! - sensible : eflx_sh_grnd (corrected) -> eflx_sh_nvp + (t0-tbef0)*cgrnds + ! - latent : qflx_evap_soi (corrected)-> qflx_ev_nvp (already tinc-corrected) + ! If errsoi_test ~ 0 across snow-free steps, promote this to the real errsoi input. + ! Pure diagnostic: changes NO physics. Remove after verification.] + if (use_nvp .and. col%nvp_layer_active(c) .and. col%snl(c) == 0) then + eflx_soil_grnd_nvp = sabg_soil(p) + sabg_lyr(p,0) + dlrad(p) & + + (1._r8 - frac_veg_nosno(p))*emg(c)*forc_lwrad(c) & + - emg(c)*sb*tssbef(c,0)**4 & + - emg(c)*sb*tssbef(c,0)**3*4._r8*(t_soisno(c,0)-tssbef(c,0)) & + - ( eflx_sh_nvp(p) + (t_soisno(c,0)-tssbef(c,0))*cgrnds(p) & + + qflx_ev_nvp(p)*htvp(c) ) + errsoi_test = eflx_soil_grnd_nvp - xmf(c) - xmf_h2osfc(c) - heat_store_diag & + + eflx_h2osfc_to_snow_col(c) & + - frac_h2osfc(c)*(t_h2osfc(c)-t_h2osfc_bef(c))*(c_h2osfc(c)/dtime) + write(iulog,*) ' [ERRSOI NVP TEST] nstep=', get_nstep(), ' c=', c, ' p=', p, & + ' eflx_soil_grnd_nvp=', eflx_soil_grnd_nvp, & + ' errsoi_test=', errsoi_test, ' (cur errsoi=', errsoi_patch(p), ')' + end if + + ! [PORTED by Hui Tang: DEBUG — per-layer heat-storage decomposition to locate which + ! layer carries the errsoi residual (esp. June thin-snow melt-out where xmf=0 but + ! heat_store mismatches eflx_soil_grnd). 'wgt' is the weight actually applied in the + ! errsoi sum: snow layers (j<0) use frac_sno_eff, NVP j=0 and soil (j>=1) use 1.0 + ! (cv per column area). 'term' = wgt*(t-tbef)/fact. Remove after fix.] + if (use_nvp .and. col%nvp_layer_active(c)) then + write(iulog,*) ' [ERRSOI LYR] frac_sno_eff=', frac_sno_eff(c) + do j = col%snl(c)+1, nlevgrnd + if (j >= 1) then + wgt = 1.0_r8 + else if (j == 0) then + wgt = 1.0_r8 ! NVP j=0: full column-area weight (loop frac_sno_eff + correction) + else + wgt = frac_sno_eff(c) ! snow layer: per-snow-area cv + end if + if (abs(wgt*(t_soisno(c,j)-tssbef(c,j))/fact(c,j)) > 1.0_r8 .or. j <= 1) then + write(iulog,*) ' [ERRSOI LYR] j=',j,' dT=',t_soisno(c,j)-tssbef(c,j), & + ' fact=',fact(c,j),' wgt=',wgt,' term=', & + wgt*(t_soisno(c,j)-tssbef(c,j))/fact(c,j) + end if + end do + end if end if end do From 8b8d6aa920afc8905a1e46e13fab81c63a6454fb Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Mon, 15 Jun 2026 08:07:37 +0300 Subject: [PATCH 110/113] Clear 3-way split (soil, nvp, snow), and separate treatment of h2osfc for energy flux (with frac_h2osfc) and solar radiation (implicit) --- src/biogeophys/SoilTemperatureMod.F90 | 536 +++++++++++++++++++++++--- 1 file changed, 479 insertions(+), 57 deletions(-) diff --git a/src/biogeophys/SoilTemperatureMod.F90 b/src/biogeophys/SoilTemperatureMod.F90 index 5f31110e80..2de7a8b7ab 100644 --- a/src/biogeophys/SoilTemperatureMod.F90 +++ b/src/biogeophys/SoilTemperatureMod.F90 @@ -165,6 +165,7 @@ subroutine SoilTemperature(bounds, num_urbanl, filter_urbanl, num_urbanc, filter real(r8) :: dzm ! used in computing tridiagonal matrix real(r8) :: dzp ! used in computing tridiagonal matrix real(r8) :: sabg_lyr_col(bounds%begc:bounds%endc,-nlevsno+1:1) ! absorbed solar radiation (col,lyr) [W/m2] + real(r8) :: sabg_soil_col(bounds%begc:bounds%endc) ! [PORTED by Hui Tang: col-level bare-soil surface solar (sabg_soil) for the snl<0 (1-fse)-weighted soil-solar deposit] real(r8) :: eflx_gnet_top ! net energy flux into surface layer, patch-level [W/m2] real(r8) :: hs_top(bounds%begc:bounds%endc) ! net energy flux into surface layer (col) [W/m2] logical :: cool_on(bounds%begl:bounds%endl) ! is urban air conditioning on? @@ -349,6 +350,7 @@ subroutine SoilTemperature(bounds, num_urbanl, filter_urbanl, num_urbanc, filter hs_nvp( begc:endc ), & dhsdT( begc:endc ), & sabg_lyr_col( begc:endc, -nlevsno+1: ), & + sabg_soil_col( begc:endc ), & ! [PORTED by Hui Tang: bare-soil surface solar for (1-fse) soil-solar deposit] atm2lnd_inst, urbanparams_inst, canopystate_inst, waterdiagnosticbulk_inst, & waterfluxbulk_inst, solarabs_inst, energyflux_inst, temperature_inst) @@ -390,6 +392,7 @@ subroutine SoilTemperature(bounds, num_urbanl, filter_urbanl, num_urbanc, filter hs_nvp( begc:endc ), & dhsdT( begc:endc ), & sabg_lyr_col (begc:endc, -nlevsno+1: ), & + sabg_soil_col( begc:endc ), & ! [PORTED by Hui Tang: bare-soil surface solar for (1-fse) soil-solar deposit] tk( begc:endc, -nlevsno+1: ), & tk_h2osfc( begc:endc ), & fact( begc:endc, -nlevsno+1: ), & @@ -606,7 +609,8 @@ subroutine SoilTemperature(bounds, num_urbanl, filter_urbanl, num_urbanc, filter c = filter_nolakec(fc) ! [PORTED by Hui Tang: NVP fractional area for t_grnd blend (excludes snow and h2osfc)] if (use_nvp) then - frac_nvp_eff = min(col%frac_nvp(c), max(0._r8, 1._r8 - frac_sno_eff(c) - frac_h2osfc(c))) + ! [PORTED by Hui Tang: re-wired frac_nvp_eff — snow buries NVP (frac_nvp - frac_sno_eff), cap = 1 - frac_h2osfc - frac_sno_eff] + frac_nvp_eff = min(1._r8 - frac_h2osfc(c) - frac_sno_eff(c), max(0._r8, col%frac_nvp(c) - frac_sno_eff(c))) else frac_nvp_eff = 0._r8 end if @@ -1038,9 +1042,20 @@ subroutine SoilThermProp (bounds, num_urbanc, filter_urbanc, num_nolakec, filter do fc = 1, num_nolakec c = filter_nolakec(fc) if (col%jbot_sno(c) == -1) then + ! [PORTED by Hui Tang (2026-06-12): option (b) step 1 — make cv(c,0) FULLY PER-MOSS-AREA, + ! mirroring snow's per-snow-area cv (line ~1027). The moss occupies only frac_nvp of the + ! column, so divide the ENTIRE per-column heat capacity by frac_nvp. NOTE: the solid term + ! also needs /frac_nvp because dz(c,0)=col%dz_nvp = nvp_dz*frac_nvp*canopy_frac + ! (clmfates_interfaceMod:1816) is the column-EFFECTIVE depth — it already carries frac_nvp, + ! so csol_nvp*(1-watsat_nvp)*dz(c,0) is PER-COLUMN, not per-moss. Dividing the whole + ! expression gives cv_moss = old_cv/frac_nvp, so frac_nvp*cv_moss = the actual per-column + ! heat capacity (solid+water) — consistent with the per-column accounting and the moss + ! solid added to heat(c) in TotalWaterAndHeatMod. Pairs with the moss/soil interface + ! (frac_nvp on SOIL side, FULL on MOSS side) so frac_nvp cancels across storage and + ! conduction. jbot_sno==-1 => frac_nvp > nvp_frac_min > 0.] cv(c,0) = max(thin_sfclayer, & - csol_nvp*(1._r8 - watsat_nvp)*dz(c,0) & - + cpliq*h2osoi_liq(c,0) + cpice*h2osoi_ice(c,0)) + ( csol_nvp*(1._r8 - watsat_nvp)*dz(c,0) & + + cpliq*h2osoi_liq(c,0) + cpice*h2osoi_ice(c,0) )/col%frac_nvp(c)) end if end do end if @@ -1167,6 +1182,13 @@ subroutine PhaseChangeH2osfc (bounds, num_nolakec, filter_nolakec, & int_snow(c) = int_snow(c) - xm(c) if (snl(c) == 0) then h2osno_no_layers(c) = h2osno_no_layers(c) - xm(c) + ! [PORTED by Hui Tang: NVP partial-freeze — route frozen surface water to the bottom + ! snow layer j=-1, NOT the NVP moss layer j=0 (mirrors the full-freeze case below, + ! lines ~1227). Without this, partial-freeze ice accumulates in the moss layer, + ! inflating its cv and corrupting t_soisno(c,0) -> errsoi spikes at h2osfc-freeze + ! transitions (the residual after Phase 1c).] + else if (use_nvp .and. col%jbot_sno(c) == -1 .and. snl(c) <= -2) then + h2osoi_ice(c,-1) = h2osoi_ice(c,-1) - xm(c) else h2osoi_ice(c,0) = h2osoi_ice(c,0) - xm(c) end if @@ -1191,6 +1213,19 @@ subroutine PhaseChangeH2osfc (bounds, num_nolakec, filter_nolakec, & !initialize for next time step t_soisno(c,0) = t_h2osfc(c) eflx_h2osfc_to_snow_col(c) = 0. + ! [PORTED by Hui Tang: NVP partial-freeze — the ice was added to the bottom snow layer + ! j=-1 above, so equilibrate t_soisno(c,-1)/fact(c,-1), NOT the moss layer j=0 (mirrors + ! the full-freeze NVP case below, lines ~1268).] + else if (use_nvp .and. col%jbot_sno(c) == -1 .and. snl(c) <= -2) then + c1=frac_sno(c)/fact(c,-1)*dtime + if ( frac_h2osfc(c) /= 0.0_r8 )then + c2=(-cpliq*xm(c) - frac_h2osfc(c)*dhsdT(c)*dtime) + else + c2=0.0_r8 + end if + t_soisno(c,-1) = (c1*t_soisno(c,-1)+ c2*t_h2osfc(c)) & + /(c1 + c2) + eflx_h2osfc_to_snow_col(c) =(t_h2osfc(c)-t_soisno(c,-1))*c2/dtime else if (snl(c) == -1)then c1=frac_sno(c)*(dtime/fact(c,0) - dhsdT(c)*dtime) @@ -1203,9 +1238,9 @@ subroutine PhaseChangeH2osfc (bounds, num_nolakec, filter_nolakec, & c2=0.0_r8 end if t_soisno(c,0) = (c1*t_soisno(c,0)+ c2*t_h2osfc(c)) & - /(c1 + c2) + /(c1 + c2) eflx_h2osfc_to_snow_col(c) =(t_h2osfc(c)-t_soisno(c,0))*c2/dtime - + endif !========================= xm > h2osfc ============================= @@ -1216,7 +1251,18 @@ subroutine PhaseChangeH2osfc (bounds, num_nolakec, filter_nolakec, & if (snl(c) == 0) then h2osno_no_layers(c) = h2osno_no_layers(c) + h2osfc(c) else - h2osoi_ice(c,0) = h2osoi_ice(c,0) + h2osfc(c) + ! [PORTED by Hui Tang: route frozen surface water to the bottom SNOW layer, not the + ! NVP moss layer. Standard CLM layer 0 is the bottom snow layer; with NVP, layer 0 + ! is the 79-micron moss layer and the bottom snow layer is j=-1 (jbot_sno==-1, and + ! the first snow on NVP makes snl<=-2). Without this, frozen h2osfc accumulates as + ! unphysical ice in the moss layer (ice0 grew 55->250 kg/m2 over snowmelt), inflating + ! cv(c,0) and causing the soil-energy balance (errsoi) spikes. The matching + ! temperature equilibration below is also routed to j=-1 for NVP.] + if (use_nvp .and. col%jbot_sno(c) == -1 .and. snl(c) <= -2) then + h2osoi_ice(c,-1) = h2osoi_ice(c,-1) + h2osfc(c) + else + h2osoi_ice(c,0) = h2osoi_ice(c,0) + h2osfc(c) + end if end if h2osno_total(c) = h2osno_total(c) + h2osfc(c) @@ -1250,6 +1296,20 @@ subroutine PhaseChangeH2osfc (bounds, num_nolakec, filter_nolakec, & t_h2osfc(c) = t_soisno(c,0) else + ! [PORTED by Hui Tang: equilibrate the frozen h2osfc with the layer that received + ! its ice. For NVP (jbot_sno==-1) the ice was added to the bottom snow layer j=-1 + ! above, so equilibrate t_soisno(c,-1)/fact(c,-1); otherwise use layer 0 as standard.] + if (use_nvp .and. col%jbot_sno(c) == -1) then + c1=frac_sno(c)/fact(c,-1)*dtime + if ( frac_h2osfc(c) /= 0.0_r8 )then + c2=frac_h2osfc(c)*(c_h2osfc(c) - dtime*dhsdT(c)) + else + c2=0.0_r8 + end if + t_soisno(c,-1) = (c1*t_soisno(c,-1)+ c2*t_h2osfc(c)) & + /(c1 + c2) + t_h2osfc(c) = t_soisno(c,-1) + else c1=frac_sno(c)/fact(c,0)*dtime if ( frac_h2osfc(c) /= 0.0_r8 )then c2=frac_h2osfc(c)*(c_h2osfc(c) - dtime*dhsdT(c)) @@ -1257,8 +1317,9 @@ subroutine PhaseChangeH2osfc (bounds, num_nolakec, filter_nolakec, & c2=0.0_r8 end if t_soisno(c,0) = (c1*t_soisno(c,0)+ c2*t_h2osfc(c)) & - /(c1 + c2) + /(c1 + c2) t_h2osfc(c) = t_soisno(c,0) + end if endif ! set h2osfc to zero (all liquid converted to ice) @@ -1331,6 +1392,8 @@ subroutine Phasechange (bounds, num_nolakec, filter_nolakec, dhsdT, & real(r8) :: supercool(bounds%begc:bounds%endc,nlevmaxurbgrnd) !supercooled water in soil (kg/m2) real(r8) :: propor !proportionality constant (-) real(r8) :: tinc(bounds%begc:bounds%endc,-nlevsno+1:nlevmaxurbgrnd) !t(n+1)-t(n) [K] + real(r8) :: frac_nvp_eff ! [PORTED by Hui Tang: exposed NVP fraction (Phase 1c iteration 2)] + real(r8) :: frac_soil ! [PORTED by Hui Tang: bare-soil fraction (Phase 1c iteration 2)] real(r8) :: smp !frozen water potential (mm) real(r8) :: wexice0(bounds%begc:bounds%endc,-nlevsno+1:nlevmaxurbgrnd) !initial mass of excess_ice at the timestep (kg/m2) @@ -1570,17 +1633,47 @@ subroutine Phasechange (bounds, num_nolakec, filter_nolakec, dhsdT, & if ( j==1 .and. frac_h2osfc(c) /= 0.0_r8 ) then hm(c,j) = hm(c,j) - frac_h2osfc(c)*(dhsdT(c)*tinc(c,j)) end if + ! [PORTED by Hui Tang: snl==0 3-way (2026-06-11) — for NVP snl==0, j=1 is the + ! TOP layer (j==snl+1) but its surface flux covers only the bare-soil fraction + ! frac_soil; the exposed-moss surface is at j=0. Remove the frac_nvp_eff portion + ! of the full-dhsdT surface term so hm uses frac_soil*dhsdT (= [1-fh2o-frac_nvp_eff]).] + if ( j==1 .and. use_nvp .and. col%jbot_sno(c) == -1 .and. snl(c) == 0 ) then + frac_nvp_eff = min(1._r8 - frac_h2osfc(c) - frac_sno_eff(c), & + max(0._r8, col%frac_nvp(c) - frac_sno_eff(c))) + hm(c,j) = hm(c,j) - frac_nvp_eff*(dhsdT(c)*tinc(c,j)) + end if + else if (j == 1 .and. use_nvp .and. col%jbot_sno(c) == -1 .and. snl(c) < 0) then + ! [PORTED by Hui Tang: Phase 1c iteration 2 — soil layer 1 below NVP under + ! partial snow (3-way split). The exposed-moss surface moved to j=0, so the + ! direct bare-soil surface flux at j=1 now covers only frac_soil (was + ! 1-fse-fh2o). Phase-change must credit frac_soil*dhsdT*tinc to match the + ! Phase 1c SetRHSVec_Soil/SetMatrix_Soil j=1 surface weight.] + frac_nvp_eff = min(1._r8 - frac_h2osfc(c) - frac_sno_eff(c), & + max(0._r8, col%frac_nvp(c) - frac_sno_eff(c))) + frac_soil = max(0._r8, 1._r8 - frac_sno_eff(c) - frac_h2osfc(c) - frac_nvp_eff) + hm(c,j) = frac_soil*dhsdT(c)*tinc(c,j) - tinc(c,j)/fact(c,j) else if (j == 1) then hm(c,j) = (1.0_r8 - frac_sno_eff(c) - frac_h2osfc(c)) & *dhsdT(c)*tinc(c,j) - tinc(c,j)/fact(c,j) else if (use_nvp .and. col%jbot_sno(c) == -1 .and. j == 0) then - ! [PORTED by Hui Tang: NVP j=0 phase-change energy correction] - ! cv(c,0) is per unit COLUMN area (unlike snow layers where cv is - ! per unit snow area scaled by frac_sno). Using the standard snow - ! formula hm=-frac_sno_eff*tinc/fact understates hm by frac_sno_eff, - ! causing xmf to miss (1-frac_sno_eff)*tinc/fact of latent heat and - ! leaving a residual errsoi of that magnitude during phase-change events. - hm(c,0) = -tinc(c,0) / fact(c,0) + ! [PORTED by Hui Tang: NVP j=0 phase-change energy (Phase 1c iteration 2, + ! 2026-06-11). This branch only fires for snl<0 (for snl==0, j=0 < snl+1 so + ! it is handled separately at the snl==0 block below). cv(c,0) is per unit + ! COLUMN area, hence -tinc/fact with no frac_sno_eff scaling. The MISSING + ! piece (the cause of the freeze/melt errsoi runaway, e.g. nstep~8710) was + ! the surface-flux derivative term: the Phase 1c solve drives j=0 with the + ! exposed-moss surface flux frac_nvp_eff*(hs_nvp - dhsdT*T0), so phase change + ! must credit frac_nvp_eff*dhsdT*tinc here, mirroring the bare-soil j=1 form + ! (1-fse-fh2o)*dhsdT*tinc. Without it the freeze energy was mis-accounted by + ! ~frac_nvp_eff*dhsdT*tinc each step -> oscillation/blow-up.] + frac_nvp_eff = min(1._r8 - frac_h2osfc(c) - frac_sno_eff(c), & + max(0._r8, col%frac_nvp(c) - frac_sno_eff(c))) + ! [PORTED by Hui Tang (2026-06-12): option (b) step 3 — cv(c,0) is now PER-MOSS- + ! area, so the storage term carries frac_nvp (per-moss -> per-column), like + ! snow's -frac_sno*tinc/fact. The surface-flux derivative keeps frac_nvp_eff + ! (exposed moss). For snl==0 (frac_nvp_eff=frac_nvp) this is the snow-top form + ! frac_nvp*(dhsdT*tinc - tinc/fact).] + hm(c,0) = frac_nvp_eff*dhsdT(c)*tinc(c,0) - col%frac_nvp(c)*tinc(c,0) / fact(c,0) else ! non-interfacial snow/soil layers if(j < 1) then hm(c,j) = - frac_sno_eff(c)*(tinc(c,j)/fact(c,j)) @@ -1665,14 +1758,34 @@ subroutine Phasechange (bounds, num_nolakec, filter_nolakec, dhsdT, & if (j == snl(c)+1) then if(j==1) then - t_soisno(c,j) = t_soisno(c,j) + fact(c,j)*heatr & - /(1._r8-(1.0_r8 - frac_h2osfc(c))*fact(c,j)*dhsdT(c)) + ! [PORTED by Hui Tang: snl==0 3-way (2026-06-11) — for NVP snl==0, j=1's + ! surface flux covers only frac_soil (exposed moss is at j=0), so the + ! T-correction denominator uses frac_soil (was 1-frac_h2osfc).] + if (use_nvp .and. col%jbot_sno(c) == -1 .and. snl(c) == 0) then + frac_nvp_eff = min(1._r8 - frac_h2osfc(c) - frac_sno_eff(c), & + max(0._r8, col%frac_nvp(c) - frac_sno_eff(c))) + frac_soil = max(0._r8, 1._r8 - frac_sno_eff(c) - frac_h2osfc(c) - frac_nvp_eff) + t_soisno(c,j) = t_soisno(c,j) + fact(c,j)*heatr & + /(1._r8 - frac_soil*fact(c,j)*dhsdT(c)) + else + t_soisno(c,j) = t_soisno(c,j) + fact(c,j)*heatr & + /(1._r8-(1.0_r8 - frac_h2osfc(c))*fact(c,j)*dhsdT(c)) + end if else t_soisno(c,j) = t_soisno(c,j) + (fact(c,j)/frac_sno_eff(c))*heatr & /(1._r8-fact(c,j)*dhsdT(c)) endif + else if (j == 1 .and. use_nvp .and. col%jbot_sno(c) == -1 .and. snl(c) < 0) then + ! [PORTED by Hui Tang: Phase 1c iteration 2 — soil layer 1 below NVP under + ! partial snow. Denominator uses frac_soil (bare-soil surface weight in the + ! 3-way split), matching the hm fix above and the Phase 1c j=1 surface BC.] + frac_nvp_eff = min(1._r8 - frac_h2osfc(c) - frac_sno_eff(c), & + max(0._r8, col%frac_nvp(c) - frac_sno_eff(c))) + frac_soil = max(0._r8, 1._r8 - frac_sno_eff(c) - frac_h2osfc(c) - frac_nvp_eff) + t_soisno(c,j) = t_soisno(c,j) + fact(c,j)*heatr & + /(1._r8 - frac_soil*fact(c,j)*dhsdT(c)) else if (j == 1) then t_soisno(c,j) = t_soisno(c,j) + fact(c,j)*heatr & @@ -1681,9 +1794,20 @@ subroutine Phasechange (bounds, num_nolakec, filter_nolakec, dhsdT, & if(j > 0) then t_soisno(c,j) = t_soisno(c,j) + fact(c,j)*heatr else if (use_nvp .and. col%jbot_sno(c) == -1 .and. j == 0) then - ! [PORTED by Hui Tang: NVP j=0 T-correction — cv(c,0) is per unit - ! column area so no frac_sno_eff division; mirrors the hm fix above] - t_soisno(c,0) = t_soisno(c,0) + fact(c,0)*heatr + ! [PORTED by Hui Tang: NVP j=0 T-correction (Phase 1c iteration 2). + ! cv(c,0) is per unit column area so no frac_sno_eff division. The + ! denominator (1 - frac_nvp_eff*fact*dhsdT) accounts for the implicit + ! dependence of the exposed-moss surface flux on T0, mirroring the + ! bare-soil j=1 form (1 - (1-fse-fh2o)*fact*dhsdT). Must match the hm + ! surface term above (frac_nvp_eff*dhsdT) or freeze/melt errsoi reopens.] + frac_nvp_eff = min(1._r8 - frac_h2osfc(c) - frac_sno_eff(c), & + max(0._r8, col%frac_nvp(c) - frac_sno_eff(c))) + ! [PORTED by Hui Tang (2026-06-12): option (b) step 3 — per-moss-area cv: + ! ΔT = fact*heatr/(frac_nvp - frac_nvp_eff*fact*dhsdT) (the leading 1 -> + ! frac_nvp; reduces to snow-top (fact/frac_nvp)*heatr/(1-fact*dhsdT) when + ! frac_nvp_eff=frac_nvp). dhsdT<0 so denom > frac_nvp > 0.] + t_soisno(c,0) = t_soisno(c,0) + fact(c,0)*heatr & + /(col%frac_nvp(c) - frac_nvp_eff*fact(c,0)*dhsdT(c)) else if(frac_sno_eff(c) > 0._r8) t_soisno(c,j) = t_soisno(c,j) + (fact(c,j)/frac_sno_eff(c))*heatr endif @@ -1738,7 +1862,13 @@ subroutine Phasechange (bounds, num_nolakec, filter_nolakec, dhsdT, & do fc = 1, num_nolakec c = filter_nolakec(fc) if (col%jbot_sno(c) == -1 .and. col%snl(c) == 0 .and. imelt(c,0) > 0) then - hm(c,0) = dhsdT(c)*tinc(c,0) - tinc(c,0)/fact(c,0) + ! [PORTED by Hui Tang: snl==0 3-way (2026-06-11) — surface-flux derivative weighted by + ! frac_nvp_eff (was full dhsdT under collapse), matching the snl==0 j=0 surface BC.] + frac_nvp_eff = min(1._r8 - frac_h2osfc(c) - frac_sno_eff(c), & + max(0._r8, col%frac_nvp(c) - frac_sno_eff(c))) + ! [PORTED by Hui Tang (2026-06-12): option (b) step 3 — per-moss-area cv: storage term + ! carries frac_nvp (=frac_nvp_eff here since snl==0). = frac_nvp*(dhsdT*tinc-tinc/fact).] + hm(c,0) = frac_nvp_eff*dhsdT(c)*tinc(c,0) - col%frac_nvp(c)*tinc(c,0)/fact(c,0) ! Tridiagonal error check (mirrors standard Phasechange logic) if (imelt(c,0) == 1 .and. hm(c,0) < 0._r8) then hm(c,0) = 0._r8 ; imelt(c,0) = 0 @@ -1758,9 +1888,10 @@ subroutine Phasechange (bounds, num_nolakec, filter_nolakec, dhsdT, & end if h2osoi_liq(c,0) = max(0._r8, wmass0(c,0) - h2osoi_ice(c,0)) if (abs(heatr) > 0._r8) then - ! Top-layer T correction (no frac_sno_eff or frac_h2osfc for NVP) + ! [PORTED by Hui Tang (2026-06-12): option (b) step 3 — per-moss-area cv: leading 1 + ! -> frac_nvp in the denominator (= snow-top (fact/frac_nvp)/(1-fact*dhsdT) here).] t_soisno(c,0) = t_soisno(c,0) + fact(c,0)*heatr & - / (1._r8 - fact(c,0)*dhsdT(c)) + / (col%frac_nvp(c) - frac_nvp_eff*fact(c,0)*dhsdT(c)) end if if (h2osoi_liq(c,0)*h2osoi_ice(c,0) > 0._r8) t_soisno(c,0) = tfrz xmf(c) = xmf(c) + hfus*(wice0(c,0)-h2osoi_ice(c,0))/dtime @@ -1800,7 +1931,7 @@ end subroutine Phasechange subroutine ComputeGroundHeatFluxAndDeriv(bounds, & num_nolakep, filter_nolakep, num_nolakec, filter_nolakec, & num_nvpc, filter_nvpc, & - hs_h2osfc, hs_top_snow, hs_soil, hs_top, hs_nvp, dhsdT, sabg_lyr_col, & + hs_h2osfc, hs_top_snow, hs_soil, hs_top, hs_nvp, dhsdT, sabg_lyr_col, sabg_soil_col, & atm2lnd_inst, urbanparams_inst, canopystate_inst, waterdiagnosticbulk_inst, & waterfluxbulk_inst, solarabs_inst, energyflux_inst, temperature_inst) ! @@ -1817,6 +1948,7 @@ subroutine ComputeGroundHeatFluxAndDeriv(bounds, & use column_varcon , only : icol_road_perv, icol_road_imperv use clm_varpar , only : nlevsno use UrbanParamsType, only : IsSimpleBuildTemp, IsProgBuildTemp + use clm_time_manager , only : get_nstep ! [PORTED by Hui Tang: Phase-0 NVP exposure diagnostic] ! ! !ARGUMENTS: implicit none @@ -1834,6 +1966,7 @@ subroutine ComputeGroundHeatFluxAndDeriv(bounds, & real(r8) , intent(out) :: hs_nvp( bounds%begc: ) ! [PORTED by Hui Tang: net surface flux at NVP layer 0] [W/m2] real(r8) , intent(out) :: dhsdT( bounds%begc: ) ! temperature derivative of "hs" [col] real(r8) , intent(out) :: sabg_lyr_col( bounds%begc:, -nlevsno+1: ) ! absorbed solar radiation (col,lyr) [W/m2] + real(r8) , intent(out) :: sabg_soil_col( bounds%begc: ) ! [PORTED by Hui Tang: col-level bare-soil surface solar sabg_soil [W/m2]] type(atm2lnd_type) , intent(in) :: atm2lnd_inst type(urbanparams_type) , intent(in) :: urbanparams_inst type(canopystate_type) , intent(in) :: canopystate_inst @@ -1860,6 +1993,10 @@ subroutine ComputeGroundHeatFluxAndDeriv(bounds, & ! [PORTED by Hui Tang: NVP surface flux variables] real(r8) :: lwrad_emit_nvp(bounds%begc:bounds%endc) ! NVP LW emission [W/m2] real(r8) :: eflx_gnet_nvp ! net surface flux at NVP layer, patch-level [W/m2] + ! [PORTED by Hui Tang: Phase-0 diagnostic — exposure-weighting of moss turbulent flux] + real(r8) :: frac_nvp_eff ! exposed NVP column fraction [-] + real(r8) :: nvp_exp ! snow-free fraction of moss coverage = frac_nvp_eff/frac_nvp [-] + real(r8) :: nvp_exp_solar ! [PORTED by Hui Tang: SOLAR exposed-moss weight = frac_nvp_eff_solar/frac_nvp (no fh2o cap — h2osfc is not a solar tile)] !----------------------------------------------------------------------- ! Enforce expected array sizes @@ -1877,8 +2014,9 @@ subroutine ComputeGroundHeatFluxAndDeriv(bounds, & forc_lwrad => atm2lnd_inst%forc_lwrad_downscaled_col , & ! Input: [real(r8) (:) ] downward infrared (longwave) radiation (W/m**2) frac_veg_nosno => canopystate_inst%frac_veg_nosno_patch , & ! Input: [integer (:) ] fraction of vegetation not covered by snow (0 OR 1) [-] - + frac_sno_eff => waterdiagnosticbulk_inst%frac_sno_eff_col , & ! Input: [real(r8) (:) ] eff. fraction of ground covered by snow (0 to 1) + frac_h2osfc => waterdiagnosticbulk_inst%frac_h2osfc_col , & ! [PORTED by Hui Tang: Phase-0 diag — needed for frac_nvp_eff cap] qflx_ev_snow => waterfluxbulk_inst%qflx_ev_snow_patch , & ! Input: [real(r8) (:) ] evaporation flux from snow (mm H2O/s) [+ to atm] qflx_ev_soil => waterfluxbulk_inst%qflx_ev_soil_patch , & ! Input: [real(r8) (:) ] evaporation flux from soil (mm H2O/s) [+ to atm] @@ -1913,6 +2051,8 @@ subroutine ComputeGroundHeatFluxAndDeriv(bounds, & sabg => solarabs_inst%sabg_patch , & ! Input: [real(r8) (:) ] solar radiation absorbed by ground (W/m**2) sabg_soil => solarabs_inst%sabg_soil_patch , & ! Input: [real(r8) (:) ] solar radiation absorbed by soil (W/m**2) + sabg_nvp => solarabs_inst%sabg_nvp_patch , & ! [PORTED by Hui Tang: exposed-NVP moss surface solar (W/m2)] + sabg_soil_bandloop => solarabs_inst%sabg_soil_bandloop_patch , & ! [PORTED by Hui Tang: band-loop ground absorption snapshot for SABG tile] sabg_snow => solarabs_inst%sabg_snow_patch , & ! Input: [real(r8) (:) ] solar radiation absorbed by snow (W/m**2) sabg_chk => solarabs_inst%sabg_chk_patch , & ! Output: [real(r8) (:) ] sum of soil/snow using current fsno, for balance check sabg_lyr => solarabs_inst%sabg_lyr_patch , & ! Output: [real(r8) (:,:) ] absorbed solar radiation (pft,lyr) [W/m2] @@ -1947,6 +2087,7 @@ subroutine ComputeGroundHeatFluxAndDeriv(bounds, & end do hs_soil(begc:endc) = 0._r8 + sabg_soil_col(begc:endc) = 0._r8 ! [PORTED by Hui Tang: bare-soil surface solar accumulator] hs_h2osfc(begc:endc) = 0._r8 hs(begc:endc) = 0._r8 dhsdT(begc:endc) = 0._r8 @@ -1961,6 +2102,55 @@ subroutine ComputeGroundHeatFluxAndDeriv(bounds, & - (eflx_sh_grnd(p)+qflx_evap_soi(p)*htvp(c)) ! save sabg for balancecheck, in case frac_sno is set to zero later sabg_chk(p) = frac_sno_eff(c) * sabg_snow(p) + (1._r8 - frac_sno_eff(c) ) * sabg_soil(p) + ! [PORTED by Hui Tang: include NVP layer-0 absorbed solar in sabg_chk so the surface + ! energy balance (BalanceCheckMod errseb) is consistent with eflx_soil_grnd + ! (SoilFluxesMod:411), which adds sabg_lyr(p,0) back. Without this, errseb = -sabg_lyr(p,0) + ! (large when snow-free: sabg_lyr(p,0) is the NVP Beer's-law absorption). sabg_lyr(p,0) + ! is already per unit ground area, so it is added at full weight to mirror SoilFluxesMod.] + if (use_nvp .and. col%nvp_layer_active(c)) then + ! [PORTED by Hui Tang (2026-06-12): mirror the eflx_soil_grnd add-back in SoilFluxesMod + ! — add the EXPOSED-moss surface solar at frac_nvp_eff weight (matching the solve's + ! hs_nvp = sabg_nvp*frac_nvp_eff). The BURIED-moss internal sabg_lyr(p,0) is already in + ! frac_sno_eff*sabg_snow above (SNICAR sum), so it is NOT re-added. errseb/errsoi + ! consistency invariant.] + frac_nvp_eff = min(1._r8 - frac_sno_eff(c), & + max(0._r8, col%frac_nvp(c) - frac_sno_eff(c))) + ! [PORTED by Hui Tang (2026-06-12): sabg_nvp is per-COLUMN (carries nvp_frac), so the + ! EXPOSED-moss surface solar entering the column is nvp_exp*sabg_nvp = + ! (frac_nvp_eff/frac_nvp)*sabg_nvp, NOT frac_nvp_eff*sabg_nvp (that double-counted the + ! coverage). Mirrors eflx_soil_grnd in SoilFluxesMod and the solve's hs_nvp deposit.] + sabg_chk(p) = sabg_chk(p) + (frac_nvp_eff/col%frac_nvp(c))*sabg_nvp(p) + end if + + ! [PORTED by Hui Tang: make the SABG diagnostic read the TRUE exposed-surface absorption. + ! Previously sabg(p) was overridden to sabg_chk, which under snow uses the SNICAR + ! soil-LAYER absorption sabg_soil=sabg_lyr(p,1) (solar that penetrates the snowpack AND + ! the NVP layer to soil) — tiny during melt, so SABG collapsed unphysically. Instead + ! build a tile: SNICAR snow absorption on the snow-covered fraction + the band-loop + ! (albsod) ground absorption on the exposed fraction (sabg_soil_bandloop, snapshotted in + ! SurfaceRadiationMod before the carve-out / SNICAR reassignment). sabg_soil_bandloop is + ! the LUMPED NVP+soil absorption (albsod treats all non-reflected flux as absorbed), so + ! the NVP term is already included — no separate +sabg_lyr(p,0) (that would double-count). + ! Snow-free: frac_sno_eff=0 => SABG = sabg_soil_bandloop = old sabg_chk (summer unchanged). + ! DIAGNOSTIC ONLY and decoupled from the budget: errseb uses sabg_chk (BalanceCheckMod + ! :1035, non-urban), errsoi uses eflx_soil_grnd, and the solve uses sabg_lyr — ALL + ! unchanged. So this deliberately makes SABG differ from the model's conserved SW input. + ! Placed after eflx_gnet(p) (line ~1986) which already used the original sabg(p), and + ! ComputeGroundHeatFluxAndDeriv runs once per timestep, so it cannot feed back.] + + !if (use_nvp .and. col%nvp_layer_active(c)) then + ! [PORTED by Hui Tang (2026-06-13): Phase 4 — add the EXPOSED-moss surface solar to the SABG + ! tile so the offline balance SABV+SABG-FIRA-FSH-LH-FGR closes on the moss term: FGR carries + ! nvp_exp*sabg_nvp (SoilFluxesMod), so SABG must too. nvp_exp=frac_nvp_eff/frac_nvp. + ! frac_nvp_eff is the same value computed in the sabg_chk block just above. The remaining + ! offline residual is the SOIL seam (1-fse)*(sabg_soil_bandloop - sabg_lyr(p,1)) and the + ! opaque-vs-Beer moss difference — both quantified by the Phase-4 diagnostic in + ! SurfaceRadiationMod before the (physics-affecting) sabg_soil/line-839 reconciliation.] + ! frac_nvp_eff = min(1._r8 - frac_sno_eff(c), & + ! max(0._r8, col%frac_nvp(c) - frac_sno_eff(c))) + ! sabg(p) = frac_sno_eff(c)*sabg_snow(p) + (1._r8 - frac_sno_eff(c))*sabg_soil(p) & + ! + (frac_nvp_eff/col%frac_nvp(c))*sabg_nvp(p) + !end if eflx_gnet_snow = sabg_snow(p) + dlrad(p) & + (1._r8-frac_veg_nosno(p))*emg(c)*forc_lwrad(c) - lwrad_emit_snow(c) & @@ -1973,6 +2163,20 @@ subroutine ComputeGroundHeatFluxAndDeriv(bounds, & eflx_gnet_h2osfc = sabg_soil(p) + dlrad(p) & + (1._r8-frac_veg_nosno(p))*emg(c)*forc_lwrad(c) - lwrad_emit_h2osfc(c) & - (eflx_sh_h2osfc(p)+qflx_ev_h2osfc(p)*htvp(c)) + ! [PORTED by Hui Tang (2026-06-14): the surface water sits OVER the ground, which here is mostly + ! moss, so it absorbs the moss-under-water SURFACE solar (not just sabg_soil). The moss-under- + ! water column fraction = frac_nvp_eff_solar - frac_nvp_eff (moss not-under-snow minus moss + ! exposed-to-air); its absorption (frac_uw/frac_nvp)*sabg_nvp is added here per h2osfc area + ! (=> /frac_h2osfc). This is the energy the MOSS layer cannot dissipate (its cooling weight + ! nvp_exp -> 0 under deep water -> divergence); the water CAN dissipate it via its own surface + ! fluxes. Bounded by sabg_nvp/frac_nvp since frac_uw <= frac_h2osfc. Paired with hs_nvp now + ! depositing only the EXPOSED moss (nvp_exp); together they sum to nvp_exp_solar*sabg_nvp so + ! the FGR/sabg_chk/carve-out total accounting is unchanged.] + if (use_nvp .and. col%nvp_layer_active(c) .and. frac_h2osfc(c) > 1.e-3_r8) then + eflx_gnet_h2osfc = eflx_gnet_h2osfc + sabg_nvp(p)/col%frac_nvp(c)/frac_h2osfc(c) * & + ( min(1._r8 - frac_sno_eff(c), max(0._r8, col%frac_nvp(c) - frac_sno_eff(c))) & + - min(1._r8 - frac_h2osfc(c) - frac_sno_eff(c), max(0._r8, col%frac_nvp(c) - frac_sno_eff(c))) ) + end if else ! For urban columns we use the net longwave radiation (eflx_lwrad_net) because of ! interactions between urban columns. @@ -2014,6 +2218,10 @@ subroutine ComputeGroundHeatFluxAndDeriv(bounds, & dhsdT(c) = dhsdT(c) + dgnetdT(p) * patch%wtcol(p) ! separate surface fluxes for soil/snow hs_soil(c) = hs_soil(c) + eflx_gnet_soil * patch%wtcol(p) + ! [PORTED by Hui Tang (2026-06-13): accumulate the bare-soil SURFACE solar (sabg_soil) at column + ! level so the snl<0 solve can deposit it at the (1-fse) radiation weight (h2osfc is not a solar + ! tile) instead of the frac_soil flux weight carried by hs_soil.] + sabg_soil_col(c) = sabg_soil_col(c) + sabg_soil(p) * patch%wtcol(p) hs_h2osfc(c) = hs_h2osfc(c) + eflx_gnet_h2osfc * patch%wtcol(p) end do @@ -2065,14 +2273,58 @@ subroutine ComputeGroundHeatFluxAndDeriv(bounds, & ! tridiagonal solver; the main loop (do j=lyr_top,1) already accumulates ! sabg_lyr_col(c,0), so we must NOT add it again here or it is double-counted. if (col%nvp_layer_active(c)) then - eflx_gnet_nvp = sabg_lyr(p,0) + dlrad(p) & + ! [PORTED by Hui Tang: exposure-weight the moss surface flux. The WHOLE eflx_gnet_nvp + ! below (solar sabg_nvp + down/emitted LW + sensible + latent) is the moss surface flux; + ! Each patch (nvp or non-nvp patches) gets a share of contribution from nvp gnet (which + ! is unphysical), but the sum of NVP patches and non-NVP patches = frac_nvp_eff at column + ! level to form the exposed-moss per-column contribution. frac_nvp_eff is the exposed + ! (snow-free) moss area fraction.] + frac_nvp_eff = min(1._r8 - frac_h2osfc(c) - frac_sno_eff(c), & + max(0._r8, col%frac_nvp(c) - frac_sno_eff(c))) + nvp_exp = frac_nvp_eff / col%frac_nvp(c) ! FLUX exposed fraction of the moss (with-fh2o) [-] + ! [PORTED by Hui Tang (2026-06-13): SOLAR exposed-moss weight uses NO fh2o cap — h2osfc + ! is a tile for surface LW/turbulent fluxes but NOT for solar (albgrd has no h2osfc term). + ! So the moss SOLAR is deposited at nvp_exp_solar (no fh2o), matching the accounting + ! (carve-out, FGR add-back, sabg_chk); the LW/turbulent at nvp_exp (with fh2o).] + nvp_exp_solar = min(1._r8 - frac_sno_eff(c), & + max(0._r8, col%frac_nvp(c) - frac_sno_eff(c))) / col%frac_nvp(c) + + ! [PORTED by Hui Tang: UNIFIED hs_nvp + ! for BOTH snl==0 and snl<0. hs_nvp carries the exposed-NVP surface flux, weighted by + ! frac_nvp_eff*wtcol (summed over patches -> frac_nvp_eff at column level). Solar enters + ! the NVP layer via sabg_nvp (surface, below) and sabg_lyr_col(c,0) (buried internal): + ! snl<0 via the do j=lyr_top,1 loop above; snl==0 via the explicit add below. This + ! replaces the previous snl==0 "collapse" (full solar-inclusive hs_nvp, bare soil folded + ! into NVP) with the 3-way split (exposed moss at j=0, bare soil at j=1) for continuity.] + ! [PORTED by Hui Tang (2026-06-11): hs_nvp now carries the EXPOSED-moss SURFACE solar + ! sabg_nvp(p) (Beer's law), exactly as hs_soil carries sabg_soil. The whole gnet + ! (incl. solar) is weighted by frac_nvp_eff*wtcol, so the exposed solar gets + ! the -dhsdT surface thermostat — this is what was missing (overheating). The BURIED + ! internal solar is sabg_lyr(p,0) (=snicar for snl<0, =0 for snl==0), accumulated into + ! sabg_lyr_col(c,0) and applied at fse weight in the j=0 RHS (like fse*sabg_lyr_col(c,1) + ! for soil).] + ! [PORTED by Hui Tang (2026-06-12): sabg_nvp is per-COLUMN (fabd_nvp carries nvp_frac), + ! but eflx_gnet_nvp is the moss surface energy balance PER UNIT MOSS area (the LW/turbulent + ! terms are per-moss), so convert the solar to per-moss with /col%frac_nvp(c). The exposed + ! weighting (nvp_exp) is applied afterwards in hs_nvp = eflx_gnet_nvp*nvp_exp*wtcol, so the + ! per-column moss solar deposited = frac_nvp*nvp_exp*(sabg_nvp/frac_nvp) = nvp_exp*sabg_nvp.] + eflx_gnet_nvp = sabg_nvp(p)/col%frac_nvp(c) + dlrad(p) & + (1._r8-frac_veg_nosno(p))*emg(c)*forc_lwrad(c) - lwrad_emit_nvp(c) & - (eflx_sh_nvp(p) + qflx_ev_nvp(p)*htvp(c)) - hs_nvp(c) = hs_nvp(c) + eflx_gnet_nvp * patch%wtcol(p) - ! [PORTED by Hui Tang: only accumulate sabg_lyr_col(c,0) here when there is - ! no snow (snl==0). When snow covers NVP (snl<0), the do j=lyr_top,1 loop - ! above already added sabg_lyr(p,0); adding it again would double-count it, - ! causing errsoi = -sabg_lyr(p,0).] + ! [PORTED by Hui Tang (2026-06-12): option (b) — per-MOSS-area cv requires the SOLVE-side + ! surface flux on the per-moss basis: nvp_exp*eflx_gnet (=frac_nvp_eff/frac_nvp), NOT the + ! per-column frac_nvp_eff. The frac_nvp in cv/interface bridges to the per-column + ! accounting (eflx_soil_grnd, sabg_chk stay frac_nvp_eff), exactly like snow. Mixing + ! per-moss cv with per-column hs_nvp under-cooled the moss -> overheat/crash.] + ! [PORTED by Hui Tang (2026-06-14): the moss layer deposits only the EXPOSED-to-air solar+flux + ! at nvp_exp (with-fh2o). The moss-under-water surface solar is NOT deposited here — it goes + ! to eflx_gnet_h2osfc above (the water can dissipate it; the moss cannot, nvp_exp->0 under + ! deep water -> divergence). moss(nvp_exp) + water((nvp_exp_solar-nvp_exp)) = nvp_exp_solar, + ! so the FGR/sabg_chk/carve-out total (nvp_exp_solar*sabg_nvp) is unchanged.] + hs_nvp(c) = hs_nvp(c) + eflx_gnet_nvp * nvp_exp * patch%wtcol(p) + ! sabg_lyr_col(c,0) for snl==0 (the do j=lyr_top,1 loop above only covers j>=1 then); + ! sabg_lyr(p,0)=0 for snl==0 so this adds nothing, but keep it for the snl<0 internal path + ! symmetry. For snl<0 the do-loop already set sabg_lyr_col(c,0)=snicar*wtcol. if (snl(c) == 0) then sabg_lyr_col(c,0) = sabg_lyr_col(c,0) + sabg_lyr(p,0) * patch%wtcol(p) end if @@ -2081,6 +2333,20 @@ subroutine ComputeGroundHeatFluxAndDeriv(bounds, & ' sabg_lyr(p,0)=', sabg_lyr(p,0), & ' eflx_gnet_nvp=', eflx_gnet_nvp, & ' hs_nvp(c)=', hs_nvp(c) + ! [PORTED by Hui Tang: VERIFY diagnostic (remove after confirmation) — exposure + ! weighting is now APPLIED in eflx_gnet_nvp above. turb_unweighted is the old value, + ! turb_applied = frac_nvp_eff*turb_unweighted is what now enters the solve.] + if (frac_sno_eff(c) > 0._r8) then + write(iulog,*) '[NVP EXP DIAG] nstep=', get_nstep(), ' c=', c, ' p=', p, & + ' snl=', snl(c), ' frac_nvp=', col%frac_nvp(c), & + ' frac_sno_eff=', frac_sno_eff(c), ' frac_h2osfc=', frac_h2osfc(c), & + ' frac_nvp_eff=', frac_nvp_eff + write(iulog,*) '[NVP EXP DIAG] eflx_sh_nvp=', eflx_sh_nvp(p), & + ' qflx_ev_nvp=', qflx_ev_nvp(p), & + ' turb_unweighted=', (eflx_sh_nvp(p)+qflx_ev_nvp(p)*htvp(c)), & + ' turb_applied=', frac_nvp_eff*(eflx_sh_nvp(p)+qflx_ev_nvp(p)*htvp(c)), & + ' eflx_gnet_nvp=', eflx_gnet_nvp + end if end if else @@ -2211,7 +2477,7 @@ end subroutine ComputeHeatDiffFluxAndFactor !----------------------------------------------------------------------- subroutine SetRHSVec(bounds, num_nolakec, filter_nolakec, dtime, & - hs_h2osfc, hs_top_snow, hs_soil, hs_top, hs_nvp, dhsdT, sabg_lyr_col, tk, & + hs_h2osfc, hs_top_snow, hs_soil, hs_top, hs_nvp, dhsdT, sabg_lyr_col, sabg_soil_col, tk, & tk_h2osfc, fact, fn, c_h2osfc, dz_h2osfc, & temperature_inst, waterdiagnosticbulk_inst, rvector) @@ -2246,6 +2512,7 @@ subroutine SetRHSVec(bounds, num_nolakec, filter_nolakec, dtime, & real(r8) , intent(in) :: hs_nvp( bounds%begc: ) ! [PORTED by Hui Tang: surface heat flux at NVP layer 0] [W/m2] real(r8) , intent(in) :: dhsdT( bounds%begc: ) ! temperature derivative of "hs" [col] real(r8) , intent(in) :: sabg_lyr_col( bounds%begc: , -nlevsno+1: ) ! absorbed solar radiation (col,lyr) [W/m2] + real(r8) , intent(in) :: sabg_soil_col( bounds%begc: ) ! [PORTED by Hui Tang: col-level bare-soil surface solar [W/m2]] real(r8) , intent(in) :: tk( bounds%begc: , -nlevsno+1: ) ! thermal conductivity [W/(m K)] real(r8) , intent(in) :: tk_h2osfc( bounds%begc: ) ! thermal conductivity of h2osfc [W/(m K)] [col] real(r8) , intent(in) :: fact( bounds%begc: , -nlevsno+1: ) ! used in computing tridiagonal matrix [col, lev] @@ -2310,6 +2577,8 @@ subroutine SetRHSVec(bounds, num_nolakec, filter_nolakec, dtime, & fn( begc:endc, -nlevsno+1: ), & t_soisno ( begc:endc, -nlevsno+1: ), & t_h2osfc ( begc:endc ), & + frac_sno_eff( begc:endc ), & ! [PORTED by Hui Tang: Phase 1c] + frac_h2osfc( begc:endc ), & ! [PORTED by Hui Tang: Phase 1c] rt_snow( begc:endc, -nlevsno:)) ! Set entries in RHS vector for surface water layer @@ -2332,6 +2601,7 @@ subroutine SetRHSVec(bounds, num_nolakec, filter_nolakec, dtime, & hs_top( begc:endc ), & dhsdT( begc:endc ), & sabg_lyr_col (begc:endc, -nlevsno+1: ), & + sabg_soil_col( begc:endc ), & ! [PORTED by Hui Tang] fact( begc:endc, -nlevsno+1: ), & fn( begc:endc, -nlevsno+1: ), & fn_h2osfc( begc:endc ), & @@ -2356,7 +2626,7 @@ end subroutine SetRHSVec !----------------------------------------------------------------------- subroutine SetRHSVec_Snow(bounds, num_nolakec, filter_nolakec, & hs_top_snow, hs_top, hs_nvp, dhsdT, sabg_lyr_col, & - fact, fn, t_soisno, t_h2osfc, rt) + fact, fn, t_soisno, t_h2osfc, frac_sno_eff, frac_h2osfc, rt) ! ! !DESCRIPTION: ! Sets up RHS vector corresponding to snow layers for all columns. @@ -2380,8 +2650,10 @@ subroutine SetRHSVec_Snow(bounds, num_nolakec, filter_nolakec, & real(r8), intent(in) :: sabg_lyr_col( bounds%begc: , -nlevsno+1: ) ! absorbed solar radiation (col,lyr) [W/m2] real(r8), intent(in) :: fact( bounds%begc: , -nlevsno+1: ) ! used in computing tridiagonal matrix [col, lev] real(r8), intent(in) :: fn (bounds%begc: , -nlevsno+1: ) ! heat diffusion through the layer interface [W/m2] - real(r8), intent(in) :: t_soisno(bounds%begc:, -nlevsno+1:) ! soil temperature [K] - real(r8), intent(in) :: t_h2osfc(bounds%begc:) ! surface water temperature [K] + real(r8), intent(in) :: t_soisno(bounds%begc:, -nlevsno+1:) ! soil temperature [K] + real(r8), intent(in) :: t_h2osfc(bounds%begc:) ! surface water temperature [K] + real(r8), intent(in) :: frac_sno_eff(bounds%begc: ) ! [PORTED by Hui Tang: fraction of ground covered by snow (0 to 1)] + real(r8), intent(in) :: frac_h2osfc(bounds%begc: ) ! [PORTED by Hui Tang: fraction of ground covered by surface water (0 to 1)] real(r8), intent(out) :: rt(bounds%begc: , -nlevsno: ) ! rhs vector entries !----------------------------------------------------------------------- ! @@ -2390,6 +2662,10 @@ subroutine SetRHSVec_Snow(bounds, num_nolakec, filter_nolakec, & integer :: fc ! lake filtered column indices real(r8) :: dzp, dzm ! used in computing tridiagonal matrix real(r8) :: hs_top_lev(bounds%endc) + real(r8) :: frac_nvp_eff ! [PORTED by Hui Tang: exposed NVP fraction (Phase 1c)] + real(r8) :: w_cond ! [PORTED by Hui Tang: NVP-over-soil conduction weight = frac_sno_eff + frac_nvp_eff] + real(r8) :: nvp_exp ! [PORTED by Hui Tang: exposed fraction of the moss = frac_nvp_eff/frac_nvp (per-moss surface weight)] + real(r8) :: sno_exp ! [PORTED by Hui Tang: buried (under-snow) fraction of the moss = frac_sno_eff/frac_nvp (per-moss snow-conduction/buried-solar weight)] ! Enforce expected array sizes SHR_ASSERT_ALL_FL((ubound(hs_top_snow) == (/bounds%endc/)), sourcefile, __LINE__) @@ -2400,6 +2676,8 @@ subroutine SetRHSVec_Snow(bounds, num_nolakec, filter_nolakec, & SHR_ASSERT_ALL_FL((ubound(fn) == (/bounds%endc, nlevmaxurbgrnd/)), sourcefile, __LINE__) SHR_ASSERT_ALL_FL((ubound(t_soisno) == (/bounds%endc, nlevmaxurbgrnd/)), sourcefile, __LINE__) SHR_ASSERT_ALL_FL((ubound(t_h2osfc) == (/bounds%endc/)), sourcefile, __LINE__) + SHR_ASSERT_ALL_FL((ubound(frac_sno_eff) == (/bounds%endc/)), sourcefile, __LINE__) + SHR_ASSERT_ALL_FL((ubound(frac_h2osfc) == (/bounds%endc/)), sourcefile, __LINE__) SHR_ASSERT_ALL_FL((ubound(rt) == (/bounds%endc, -1/)), sourcefile, __LINE__) associate( & @@ -2440,6 +2718,31 @@ subroutine SetRHSVec_Snow(bounds, num_nolakec, filter_nolakec, & rt(c,j-1) = t_soisno(c,j) + fact(c,j)*( hs_top_lev(c) & - dhsdT(c)*t_soisno(c,j) + cnfac*fn(c,j) ) + ! [PORTED by Hui Tang: Phase 1c — NVP layer 0 dual surface under partial snow (snl<0). + ! Tested BEFORE the "j > snl+1" internal branch (which would treat j=0 as buried). + ! Exposed moss (frac_nvp_eff) gets the atmospheric surface flux hs_nvp (per-column, + ! NON-solar; solar in sabg_lyr_col(c,0)); snow-covered moss (frac_sno_eff) conducts from + ! snow above fn(-1); NVP->soil conduction fn(0) over w_cond = frac_sno_eff + frac_nvp_eff.] + else if (j == 0 .and. use_nvp .and. jbot_sno(c) == -1 .and. snl(c) < 0) then + dzm = z(c,0) - z(c,-1) + dzp = z(c,1) - z(c,0) + frac_nvp_eff = min(1._r8 - frac_h2osfc(c) - frac_sno_eff(c), & + max(0._r8, col%frac_nvp(c) - frac_sno_eff(c))) + w_cond = frac_sno_eff(c) + frac_nvp_eff + nvp_exp = frac_nvp_eff / col%frac_nvp(c) ! exposed fraction of the moss [-] + sno_exp = frac_sno_eff(c) / col%frac_nvp(c) ! buried (under-snow) fraction of the moss [-] + ! [PORTED by Hui Tang (2026-06-12): option (b) — ALL per-column fluxes into the per-moss-area + ! moss layer are divided by frac_nvp (per-column -> per-moss). Surface: nvp_exp*dhsdT/hs_nvp. + ! Soil conduction fn(0): FULL on moss side (=frac_nvp/frac_nvp), w_cond on soil side. SNOW + ! conduction fn(-1) and BURIED internal solar sabg_lyr_col(c,0): both /frac_nvp via sno_exp + ! (=frac_sno_eff/frac_nvp). This CONSERVES at the moss/snow interface — snow loses + ! frac_sno_eff*fn(-1) per column, moss gains frac_nvp*sno_exp*fn(-1)=frac_sno_eff*fn(-1). + ! (Before this, per-moss cv under-counted the snow flux by frac_nvp -> winter errsoi leak.)] + rt(c,j-1) = t_soisno(c,0) + fact(c,0) * & + ( hs_nvp(c) - nvp_exp*dhsdT(c)*t_soisno(c,0) & + + cnfac*( fn(c,0) - sno_exp*fn(c,-1) ) ) & + + sno_exp*fact(c,0)*sabg_lyr_col(c,0) ! [PORTED by Hui Tang: buried-moss internal solar, per-moss (sno_exp=fse/frac_nvp); exposed solar is in hs_nvp] + else if (j > col%snl(c)+1) then dzm = (z(c,j)-z(c,j-1)) dzp = (z(c,j+1)-z(c,j)) @@ -2447,12 +2750,24 @@ subroutine SetRHSVec_Snow(bounds, num_nolakec, filter_nolakec, & rt(c,j-1) = t_soisno(c,j) + cnfac*fact(c,j)*( fn(c,j) - fn(c,j-1) ) rt(c,j-1) = rt(c,j-1) + fact(c,j)*sabg_lyr_col(c,j) - ! [PORTED by Hui Tang: NVP layer 0 is the top surface when snl=0 and no snow] - ! When NVP active, jtop=-1 so layer 0 is in the TVD system but falls through - ! the conditions above (j=0 < snl+1=1). Apply NVP surface BC here. + ! [PORTED by Hui Tang: NVP layer 0 surface BC when snl==0 (no snow), 3-way split + ! (2026-06-11). j=0 < snl+1=1 so it falls through the conditions above. With no snow, + ! frac_sno_eff=0 so w_cond = frac_nvp_eff and there is no fn(-1) snow-conduction term: + ! exposed moss (frac_nvp_eff) gets hs_nvp WHICH NOW INCLUDES the SURFACE solar sabg_nvp + ! (so the solar gets the -dhsdT thermostat) + conduction frac_nvp_eff*fn(0). The buried + ! internal solar term frac_sno_eff*sabg_lyr_col(c,0) vanishes here (frac_sno_eff=0). The + ! bare-soil surface flux is applied at j=1 (SetRHSVec_Soil).] else if (j == 0 .and. use_nvp .and. jbot_sno(c) == -1 .and. snl(c) == 0) then + frac_nvp_eff = min(1._r8 - frac_h2osfc(c) - frac_sno_eff(c), & + max(0._r8, col%frac_nvp(c) - frac_sno_eff(c))) + nvp_exp = frac_nvp_eff / col%frac_nvp(c) ! exposed fraction of the moss [-] (=1 here, snl==0) + ! [PORTED by Hui Tang (2026-06-12): option (b) step 2 — moss/soil conduction fn(0) at + ! FULL weight on the moss side (per-moss-area cv); soil side (j=1) keeps frac_nvp. The + ! surface thermostat is per-moss nvp_exp*dhsdT (=dhsdT here, nvp_exp=1) to match hs_nvp.] rt(c,j-1) = t_soisno(c,0) + fact(c,0) * & - (hs_nvp(c) - dhsdT(c)*t_soisno(c,0) + cnfac*fn(c,0)) + ( hs_nvp(c) - nvp_exp*dhsdT(c)*t_soisno(c,0) & + + cnfac*fn(c,0) ) & + + frac_sno_eff(c)*fact(c,0)*sabg_lyr_col(c,0) end if end do end do @@ -2538,7 +2853,7 @@ end subroutine SetRHSVec_StandingSurfaceWater !----------------------------------------------------------------------- subroutine SetRHSVec_Soil(bounds, num_nolakec, filter_nolakec, & - hs_top_snow, hs_soil, hs_top, dhsdT, sabg_lyr_col, fact, fn, fn_h2osfc, c_h2osfc, & + hs_top_snow, hs_soil, hs_top, dhsdT, sabg_lyr_col, sabg_soil_col, fact, fn, fn_h2osfc, c_h2osfc, & frac_h2osfc, frac_sno_eff, t_soisno, rt) ! ! !DESCRIPTION: @@ -2561,6 +2876,7 @@ subroutine SetRHSVec_Soil(bounds, num_nolakec, filter_nolakec, & real(r8), intent(in) :: hs_top(bounds%begc: ) ! net energy flux into surface layer (col) [W/m2] real(r8), intent(in) :: dhsdT(bounds%begc: ) ! temperature derivative of "hs" [col] real(r8), intent(in) :: sabg_lyr_col(bounds%begc:, -nlevsno+1: ) ! absorbed solar radiation (col,lyr) [W/m2] + real(r8), intent(in) :: sabg_soil_col(bounds%begc:) ! [PORTED by Hui Tang: col-level bare-soil surface solar [W/m2]] real(r8), intent(in) :: fact( bounds%begc: , -nlevsno+1: ) ! used in computing tridiagonal matrix [col, lev] real(r8), intent(in) :: fn (bounds%begc: ,-nlevsno+1: ) ! heat diffusion through the layer interface [W/m2] real(r8), intent(in) :: fn_h2osfc (bounds%begc: ) ! heat diffusion through standing-water/soil interface [W/m2] @@ -2574,6 +2890,9 @@ subroutine SetRHSVec_Soil(bounds, num_nolakec, filter_nolakec, & ! !LOCAL VARIABLES: integer :: j,c,l ! indices integer :: fc ! lake filtered column indices + real(r8) :: frac_nvp_eff ! [PORTED by Hui Tang: exposed NVP fraction (Phase 1c)] + real(r8) :: w_cond ! [PORTED by Hui Tang: NVP-over-soil conduction weight = frac_sno_eff + frac_nvp_eff] + real(r8) :: frac_soil ! [PORTED by Hui Tang: bare-soil fraction (Phase 1c)] !----------------------------------------------------------------------- ! Enforce expected array sizes SHR_ASSERT_ALL_FL((ubound(hs_soil) == (/bounds%endc/)), sourcefile, __LINE__) @@ -2644,10 +2963,48 @@ subroutine SetRHSVec_Soil(bounds, num_nolakec, filter_nolakec, & if (j == col%snl(c)+1 .and. .not. (use_nvp .and. jbot_sno(c) == -1)) then rt(c,j) = t_soisno(c,j) + fact(c,j)*( hs_top_snow(c) & - dhsdT(c)*t_soisno(c,j) + cnfac*fn(c,j) ) - ! [PORTED by Hui Tang: layer 1 below NVP is interior: conduction + transmitted solar] + ! [PORTED by Hui Tang: soil layer 1 below NVP, snl==0, 3-way split (2026-06-11). The + ! bare-soil fraction (frac_soil) gets its surface flux hs_soil here (exposed moss is at + ! j=0); up-conduction from NVP (fn(0)=fn(j-1)) weighted frac_nvp_eff = w_cond (frac_sno_eff + ! =0). Replaces the previous pure-internal treatment (collapse, bare soil folded into NVP). + ! Solar kept as full sabg_lyr_col(c,1) (snow-free).] else if (j == 1 .and. use_nvp .and. jbot_sno(c) == -1 .and. col%snl(c) == 0) then - rt(c,j) = t_soisno(c,j) + cnfac*fact(c,j)*( fn(c,j) - fn(c,j-1) ) + frac_nvp_eff = min(1._r8 - frac_h2osfc(c) - frac_sno_eff(c), & + max(0._r8, col%frac_nvp(c) - frac_sno_eff(c))) + frac_soil = max(0._r8, 1._r8 - frac_sno_eff(c) - frac_h2osfc(c) - frac_nvp_eff) + ! [PORTED by Hui Tang: SOLAR DOUBLE-COUNT FIX (2026-06-11). For snl==0, lyr_top=1 so + ! hs_soil(=eflx_gnet_soil) carries sabg_lyr(p,1) as its solar — the SAME quantity as + ! sabg_lyr_col(c,1) (SurfaceRadiationMod:793-795 sets sabg_lyr(p,1)=total soil solar, + ! no separate bare-soil-direct term). Adding frac_soil*hs_soil AND sabg_lyr_col(c,1) + ! double-counted sabg_lyr(p,1) by frac_soil, overheating the soil and (via the dry- + ! moss feedback) the moss. Subtract sabg_lyr_col(c,1) inside the bracket so the + ! bare-soil surface flux is NON-solar; the explicit term below supplies the soil + ! solar exactly once (= the collapse behaviour).] + rt(c,j) = t_soisno(c,j) + fact(c,j) & + *( frac_soil*(hs_soil(c) - sabg_lyr_col(c,1) - dhsdT(c)*t_soisno(c,j)) & + + cnfac*(fn(c,j) - frac_nvp_eff*fn(c,j-1)) ) rt(c,j) = rt(c,j) + fact(c,j)*sabg_lyr_col(c,j) + ! [PORTED by Hui Tang: Phase 1c — soil layer 1 below NVP under partial snow (snl<0). + ! Exposed-moss surface flux enters at j=0, so j=1 gets only the bare-soil surface flux + ! hs_soil over frac_soil; up-conduction from NVP (fn(0)) weighted by w_cond = + ! frac_sno_eff + frac_nvp_eff, matching the j=0 down-conduction. Solar kept as fse.] + else if (j == 1 .and. use_nvp .and. jbot_sno(c) == -1 .and. col%snl(c) < 0) then + frac_nvp_eff = min(1._r8 - frac_h2osfc(c) - frac_sno_eff(c), & + max(0._r8, col%frac_nvp(c) - frac_sno_eff(c))) + w_cond = frac_sno_eff(c) + frac_nvp_eff + frac_soil = max(0._r8, 1._r8 - frac_sno_eff(c) - frac_h2osfc(c) - frac_nvp_eff) + rt(c,j) = t_soisno(c,j) + fact(c,j) & + *( frac_soil*(hs_soil(c) - dhsdT(c)*t_soisno(c,j)) & + + cnfac*(fn(c,j) - w_cond*fn(c,j-1)) ) + rt(c,j) = rt(c,j) + frac_sno_eff(c)*fact(c,j)*sabg_lyr_col(c,j) + ! [PORTED by Hui Tang (2026-06-13/14): SOLAR/flux split — frac_soil*hs_soil deposits the + ! bare-soil surface solar at the FLUX weight frac_soil. Lift it to the soil's true solar + ! share = (1-fse-fh2o)*sabg_soil = (frac_soil+frac_nvp_eff)*sabg_soil (bare soil + + ! soil-under-EXPOSED-moss). Add ((1-fse-fh2o)-frac_soil)=frac_nvp_eff times sabg_soil_col. + ! fh2o is EXCLUDED: the h2osfc gets its sabg_soil share via eflx_gnet_h2osfc; and the + ! moss-under-water solar now also goes to the water (not the soil/moss), so this no longer + ! destabilizes the moss (the moss deposits only its exposed solar at nvp_exp).] + rt(c,j) = rt(c,j) + fact(c,j)*frac_nvp_eff*sabg_soil_col(c) else if (j == 1) then ! this is the snow/soil interface layer rt(c,j) = t_soisno(c,j) + fact(c,j) & @@ -2761,6 +3118,7 @@ subroutine SetMatrix(bounds, num_nolakec, filter_nolakec, dtime, nband, & tk( begc:endc, -nlevsno+1: ), & fact( begc:endc, -nlevsno+1: ), & frac_sno_eff(begc:endc), & + frac_h2osfc(begc:endc), & ! [PORTED by Hui Tang: Phase 1c] bmatrix_snow( begc:endc, 1:, -nlevsno: ), & bmatrix_snow_soil( begc:endc, 1:, -1: )) @@ -2920,7 +3278,7 @@ end subroutine AssembleMatrixFromSubmatrices !----------------------------------------------------------------------- subroutine SetMatrix_Snow(bounds, num_nolakec, filter_nolakec, nband, & - dhsdT, tk, fact, frac_sno_eff, bmatrix_snow, bmatrix_snow_soil) + dhsdT, tk, fact, frac_sno_eff, frac_h2osfc, bmatrix_snow, bmatrix_snow_soil) ! ! !DESCRIPTION: ! Setup the matrix entries corresponding to internal snow layers @@ -2941,6 +3299,7 @@ subroutine SetMatrix_Snow(bounds, num_nolakec, filter_nolakec, nband, & real(r8), intent(in) :: tk(bounds%begc: ,-nlevsno+1: ) ! thermal conductivity [W/(m K)] real(r8), intent(in) :: fact( bounds%begc: , -nlevsno+1: ) ! used in computing tridiagonal matrix [col, lev] real(r8), intent(in) :: frac_sno_eff(bounds%begc: ) ! fraction of ground covered by snow (0 to 1) + real(r8), intent(in) :: frac_h2osfc(bounds%begc: ) ! [PORTED by Hui Tang: fraction of ground covered by surface water (0 to 1)] real(r8), intent(out) :: bmatrix_snow(bounds%begc: , 1:, -nlevsno: ) ! matrix enteries real(r8), intent(out) :: bmatrix_snow_soil(bounds%begc: , 1:,-1: ) ! matrix enteries ! @@ -2950,6 +3309,10 @@ subroutine SetMatrix_Snow(bounds, num_nolakec, filter_nolakec, nband, & integer :: nlev_thresh(1:num_nolakec) real(r8) :: dzm ! used in computing tridiagonal matrix real(r8) :: dzp ! used in computing tridiagonal matrix + real(r8) :: frac_nvp_eff ! [PORTED by Hui Tang: exposed NVP fraction (Phase 1c)] + real(r8) :: w_cond ! [PORTED by Hui Tang: NVP-over-soil conduction weight = frac_sno_eff + frac_nvp_eff] + real(r8) :: nvp_exp ! [PORTED by Hui Tang: exposed fraction of the moss = frac_nvp_eff/frac_nvp (per-moss surface weight)] + real(r8) :: sno_exp ! [PORTED by Hui Tang: buried (under-snow) fraction of the moss = frac_sno_eff/frac_nvp (per-moss snow-conduction/buried-solar weight)] !----------------------------------------------------------------------- ! Enforce expected array sizes @@ -2957,6 +3320,7 @@ subroutine SetMatrix_Snow(bounds, num_nolakec, filter_nolakec, nband, & SHR_ASSERT_ALL_FL((ubound(tk) == (/bounds%endc, nlevmaxurbgrnd/)), sourcefile, __LINE__) SHR_ASSERT_ALL_FL((ubound(fact) == (/bounds%endc, nlevmaxurbgrnd/)), sourcefile, __LINE__) SHR_ASSERT_ALL_FL((ubound(frac_sno_eff) == (/bounds%endc/)), sourcefile, __LINE__) + SHR_ASSERT_ALL_FL((ubound(frac_h2osfc) == (/bounds%endc/)), sourcefile, __LINE__) SHR_ASSERT_ALL_FL((ubound(bmatrix_snow) == (/bounds%endc, nband, -1/)), sourcefile, __LINE__) SHR_ASSERT_ALL_FL((ubound(bmatrix_snow_soil) == (/bounds%endc, nband, -1/)), sourcefile, __LINE__) @@ -2991,10 +3355,33 @@ subroutine SetMatrix_Snow(bounds, num_nolakec, filter_nolakec, nband, & do j = -nlevsno+1,0 do fc = 1,num_nolakec c = filter_nolakec(fc) - if (j >= col%snl(c)+1) then - dzp = z(c,j+1)-z(c,j) + ! [PORTED by Hui Tang: Phase 1c — NVP layer 0 dual-surface matrix under partial snow + ! (snl<0). Tested BEFORE the generic "j >= snl+1" branch. -frac_nvp_eff*dhsdT on the + ! diagonal (exposed-moss surface flux derivative); up-conduction to snow frac_sno_eff; + ! down-conduction to soil w_cond = frac_sno_eff + frac_nvp_eff. Snow rows keep full + ! fn(-1) (per-snow-area cv convention, as the standard snow/soil interface).] + if (j == 0 .and. use_nvp .and. col%jbot_sno(c) == -1 .and. col%snl(c) < 0) then + dzm = z(c,0) - z(c,-1) + dzp = z(c,1) - z(c,0) + frac_nvp_eff = min(1._r8 - frac_h2osfc(c) - frac_sno_eff(c), & + max(0._r8, col%frac_nvp(c) - frac_sno_eff(c))) + w_cond = frac_sno_eff(c) + frac_nvp_eff + nvp_exp = frac_nvp_eff / col%frac_nvp(c) ! exposed fraction of the moss [-] + sno_exp = frac_sno_eff(c) / col%frac_nvp(c) ! buried (under-snow) fraction of the moss [-] + ! [PORTED by Hui Tang (2026-06-12): option (b) — per-moss-area cv: every per-column flux + ! into the moss is /frac_nvp. moss/soil conduction tk(0)/dzp FULL on the moss side (soil + ! side keeps w_cond). SNOW up-conduction tk(-1)/dzm at sno_exp=frac_sno_eff/frac_nvp + ! (per-moss) so the interface CONSERVES (moss gains frac_nvp*sno_exp*fn=frac_sno_eff*fn = + ! snow's loss). Surface derivative per-moss nvp_exp*dhsdT (matches hs_nvp and the RHS).] + bmatrix_snow(c,4,j-1) = - sno_exp*(1._r8-cnfac)*fact(c,0)*tk(c,-1)/dzm + bmatrix_snow(c,3,j-1) = 1._r8 + (1._r8-cnfac)*fact(c,0) & + *( tk(c,0)/dzp + sno_exp*tk(c,-1)/dzm ) & + - nvp_exp*fact(c,0)*dhsdT(c) + bmatrix_snow_soil(c,1,j-1) = - (1._r8-cnfac)*fact(c,0)*tk(c,0)/dzp + else if (j >= col%snl(c)+1) then + dzp = z(c,j+1)-z(c,j) if (j == col%snl(c)+1) then - bmatrix_snow (c,4,j-1) = 0._r8 + bmatrix_snow (c,4,j-1) = 0._r8 bmatrix_snow (c,3,j-1) = 1._r8+(1._r8-cnfac)*fact(c,j)*tk(c,j)/dzp-fact(c,j)*dhsdT(c) else dzm = (z(c,j)-z(c,j-1)) @@ -3006,16 +3393,24 @@ subroutine SetMatrix_Snow(bounds, num_nolakec, filter_nolakec, nband, & else !if ( j == 0) bmatrix_snow_soil(c,1,j-1) = - (1._r8-cnfac)*fact(c,j)* tk(c,j)/dzp end if - ! [PORTED by Hui Tang: NVP top-layer BC in SetMatrix_Snow] - ! With snl=0, condition j>=snl+1=j>=1 never fires for j=0, leaving - ! bmatrix_snow(c,:,-1) all zeros -> singular matrix (dgbsv info=1). - ! Explicitly set the NVP surface BC here: j=0 is the top of the system - ! with no layer above and coupled to soil j=1 below. - else if (use_nvp .and. col%jbot_sno(c) == -1 .and. j == 0) then + ! [PORTED by Hui Tang: NVP top-layer BC in SetMatrix_Snow, snl==0, 3-way split (2026-06-11). + ! j=0 < snl+1=1 so it falls through above (snl<0 is handled by the dedicated branch first). + ! frac_sno_eff=0 here so w_cond=frac_nvp_eff: -frac_nvp_eff*dhsdT on the diagonal (exposed- + ! moss surface derivative), no up-coupling (no snow), down-coupling to soil weighted + ! frac_nvp_eff (matches the SetRHSVec_Snow j=0 snl==0 branch). Replaces the previous + ! "collapse" (full dhsdT, full down-coupling).] + else if (use_nvp .and. col%jbot_sno(c) == -1 .and. col%snl(c) == 0 .and. j == 0) then dzp = z(c,j+1) - z(c,j) + frac_nvp_eff = min(1._r8 - frac_h2osfc(c) - frac_sno_eff(c), & + max(0._r8, col%frac_nvp(c) - frac_sno_eff(c))) + nvp_exp = frac_nvp_eff / col%frac_nvp(c) ! exposed fraction of the moss [-] (=1 here, snl==0) + ! [PORTED by Hui Tang (2026-06-12): option (b) step 2 — moss/soil conduction tk(0)/dzp at + ! FULL weight on the moss side (per-moss-area cv); soil side (j=1) keeps frac_nvp. The + ! surface derivative is per-moss nvp_exp*dhsdT (=dhsdT here, nvp_exp=1) to match hs_nvp + ! and the RHS — required for the per-moss cv to stay stable (was frac_nvp_eff -> overheat).] bmatrix_snow(c,4,j-1) = 0._r8 bmatrix_snow(c,3,j-1) = 1._r8 + (1._r8-cnfac)*fact(c,j)*tk(c,j)/dzp & - - fact(c,j)*dhsdT(c) + - nvp_exp*fact(c,j)*dhsdT(c) bmatrix_snow_soil(c,1,j-1) = -(1._r8-cnfac)*fact(c,j)*tk(c,j)/dzp end if enddo @@ -3061,6 +3456,9 @@ subroutine SetMatrix_Soil(bounds, num_nolakec, filter_nolakec, nband, & integer :: fc ! lake filtered column indices real(r8) :: dzm ! used in computing tridiagonal matrix real(r8) :: dzp ! used in computing tridiagonal matrix + real(r8) :: frac_nvp_eff ! [PORTED by Hui Tang: exposed NVP fraction (Phase 1c)] + real(r8) :: w_cond ! [PORTED by Hui Tang: NVP-over-soil conduction weight = frac_sno_eff + frac_nvp_eff] + real(r8) :: frac_soil ! [PORTED by Hui Tang: bare-soil fraction (Phase 1c)] ! ----------------------------------------------------------------------- ! Enforce expected array sizes @@ -3149,15 +3547,39 @@ subroutine SetMatrix_Soil(bounds, num_nolakec, filter_nolakec, nband, & end if bmatrix_soil(c,3,j) = 1._r8+(1._r8-cnfac)*fact(c,j)*tk(c,j)/dzp-fact(c,j)*dhsdT(c) bmatrix_soil(c,2,j) = -(1._r8-cnfac)*fact(c,j)*tk(c,j)/dzp - ! [PORTED by Hui Tang: j=1 below NVP is interior — full conduction coupling to NVP at j=0] - ! frac_sno_eff=0 when snl=0 so the standard snow/soil branch would give zero coupling; - ! NVP is always fully present above j=1, so use full conductance (no dhsdT term). + ! [PORTED by Hui Tang: soil layer 1 below NVP, snl==0, 3-way split (2026-06-11). Matrix + ! counterpart of the SetRHSVec_Soil j=1 snl==0 branch: bare-soil surface derivative + ! weighted frac_soil; up-conduction to NVP weighted frac_nvp_eff (= w_cond, frac_sno_eff=0), + ! matching the j=0 down-conduction. Replaces the previous full-conductance/no-dhsdT + ! "collapse" treatment.] else if (j == 1 .and. use_nvp .and. col%jbot_sno(c) == -1 .and. col%snl(c) == 0) then dzm = (z(c,j)-z(c,j-1)) dzp = (z(c,j+1)-z(c,j)) + frac_nvp_eff = min(1._r8 - frac_h2osfc(c) - frac_sno_eff(c), & + max(0._r8, col%frac_nvp(c) - frac_sno_eff(c))) + frac_soil = max(0._r8, 1._r8 - frac_sno_eff(c) - frac_h2osfc(c) - frac_nvp_eff) bmatrix_soil(c,2,j) = -(1._r8-cnfac)*fact(c,j)*tk(c,j)/dzp - bmatrix_soil(c,3,j) = 1._r8 + (1._r8-cnfac)*fact(c,j)*(tk(c,j)/dzp + tk(c,j-1)/dzm) - bmatrix_soil_snow(c,5,j) = -(1._r8-cnfac)*fact(c,j)*tk(c,j-1)/dzm + bmatrix_soil(c,3,j) = 1._r8 + (1._r8-cnfac)*fact(c,j)*(tk(c,j)/dzp & + + frac_nvp_eff*tk(c,j-1)/dzm) & + - frac_soil*fact(c,j)*dhsdT(c) + bmatrix_soil_snow(c,5,j) = -frac_nvp_eff*(1._r8-cnfac)*fact(c,j)*tk(c,j-1)/dzm + ! [PORTED by Hui Tang: Phase 1c — soil layer 1 below NVP under partial snow (snl<0). + ! Matrix counterpart of the SetRHSVec_Soil j=1 snl<0 branch: bare-soil surface + ! derivative weighted frac_soil; up-conduction to NVP weighted w_cond = frac_sno_eff + ! + frac_nvp_eff (matches the j=0 down-conduction so the interface conserves energy).] + else if (j == 1 .and. use_nvp .and. col%jbot_sno(c) == -1 .and. col%snl(c) < 0) then + dzm = (z(c,j)-z(c,j-1)) + dzp = (z(c,j+1)-z(c,j)) + frac_nvp_eff = min(1._r8 - frac_h2osfc(c) - frac_sno_eff(c), & + max(0._r8, col%frac_nvp(c) - frac_sno_eff(c))) + w_cond = frac_sno_eff(c) + frac_nvp_eff + frac_soil = max(0._r8, 1._r8 - frac_sno_eff(c) - frac_h2osfc(c) - frac_nvp_eff) + bmatrix_soil(c,2,j) = - (1._r8-cnfac)*fact(c,j)*tk(c,j)/dzp + bmatrix_soil(c,3,j) = 1._r8 + (1._r8-cnfac)*fact(c,j)*(tk(c,j)/dzp & + + w_cond * tk(c,j-1)/dzm) & + - frac_soil*fact(c,j)*dhsdT(c) + bmatrix_soil_snow(c,5,j) = - w_cond * (1._r8-cnfac) * fact(c,j) & + * tk(c,j-1)/dzm else if (j == 1) then ! this is the snow/soil interface layer dzm = (z(c,j)-z(c,j-1)) From 0417da415d9dff309aebdf767a18ed0ce42eb529 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Mon, 15 Jun 2026 08:17:12 +0300 Subject: [PATCH 111/113] Add debug output for soil and snow hydrology. --- src/biogeophys/SnowHydrologyMod.F90 | 24 ++++++++++++++++++++++++ src/biogeophys/SoilHydrologyMod.F90 | 23 ++++++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/biogeophys/SnowHydrologyMod.F90 b/src/biogeophys/SnowHydrologyMod.F90 index 6347e1dbcb..5ed50b781f 100644 --- a/src/biogeophys/SnowHydrologyMod.F90 +++ b/src/biogeophys/SnowHydrologyMod.F90 @@ -2605,6 +2605,30 @@ subroutine CombineSnowLayers(bounds, num_snowc, filter_snowc, & end do end do + ! [DEBUG: temporary, remove after diagnosing SNOWDP growth during melt] + ! Per-timestep snapshot of final snow state for NVP-active columns. Compare + ! across May→June: if dz(c,j<0) grows while h2osoi_ice/liq shrink, the standard + ! CTSM compaction floor (mass/frac_sno) is inflating snow-layer dz as frac_sno + ! drops. dz(c,0) should stay at the structural nvp_dz value. + if (use_nvp) then + do fc = 1, num_snowc + c = filter_snowc(fc) + if (col%jbot_sno(c) == -1) then + write(iulog,*) '[NVP DBG snow] c=', c, & + ' snl=', snl(c), & + ' jbot_sno=', col%jbot_sno(c), & + ' snow_depth=', snow_depth(c), & + ' h2osno_total=', h2osno_total(c), & + ' frac_sno=', frac_sno(c), & + ' frac_sno_eff=', frac_sno_eff(c), & + ' dz_nvp=', dz(c,0) + write(iulog,*) ' dz(snl+1..0)= ', (dz(c,j), j=snl(c)+1, 0) + write(iulog,*) ' h2osoi_ice(snl+1..0)= ',(h2osoi_ice_bulk(c,j), j=snl(c)+1, 0) + write(iulog,*) ' h2osoi_liq(snl+1..0)= ',(h2osoi_liq_bulk(c,j), j=snl(c)+1, 0) + end if + end do + end if + end associate end associate end subroutine CombineSnowLayers diff --git a/src/biogeophys/SoilHydrologyMod.F90 b/src/biogeophys/SoilHydrologyMod.F90 index b1bb34171e..26acafe349 100644 --- a/src/biogeophys/SoilHydrologyMod.F90 +++ b/src/biogeophys/SoilHydrologyMod.F90 @@ -302,6 +302,8 @@ subroutine SetQflxInputs(bounds, num_hydrologyc, filter_hydrologyc, & ! !DESCRIPTION: ! Set various input fluxes of water ! + ! !USES: + use clm_time_manager, only : get_nstep ! [PORTED by Hui Tang: NVP rain-partition diagnostic] ! !ARGUMENTS: type(bounds_type) , intent(in) :: bounds integer , intent(in) :: num_hydrologyc ! number of column soil points in column filter @@ -315,6 +317,7 @@ subroutine SetQflxInputs(bounds, num_hydrologyc, filter_hydrologyc, & real(r8) :: fsno ! copy of frac_sno ! [PORTED by Hui Tang: NVP water infiltration] real(r8) :: frac_nvp_eff ! effective NVP area fraction (not covered by h2osfc) [-] + real(r8) :: frac_nvp_eff_soil ! [PORTED by Hui Tang: DEBUG] saved soil-input-partition frac_nvp_eff (line ~365, no snow term) character(len=*), parameter :: subname = 'SetQflxInputs' !----------------------------------------------------------------------- @@ -366,6 +369,7 @@ subroutine SetQflxInputs(bounds, num_hydrologyc, filter_hydrologyc, & else frac_nvp_eff = 0._r8 end if + frac_nvp_eff_soil = frac_nvp_eff ! [PORTED by Hui Tang: DEBUG] capture before the evap-block recompute ! Soil receives the non-h2osfc, non-NVP fraction; NVP handled by NVPWaterBalance_Column qflx_in_soil(c) = (1._r8 - frac_h2osfc(c) - frac_nvp_eff) * & @@ -374,7 +378,8 @@ subroutine SetQflxInputs(bounds, num_hydrologyc, filter_hydrologyc, & if (use_nvp .and. col%nvp_layer_active(c)) then if (snl(c) >= -1) then - frac_nvp_eff = min(col%frac_nvp(c), max(0._r8, 1._r8 - frac_h2osfc(c)- fsno)) + ! [PORTED by Hui Tang: re-wired frac_nvp_eff — snow buries NVP (frac_nvp - fsno), cap = 1 - frac_h2osfc - fsno] + frac_nvp_eff = min(1._r8 - frac_h2osfc(c) - fsno, max(0._r8, col%frac_nvp(c) - fsno)) else frac_nvp_eff = min(col%frac_nvp(c), max(0._r8, 1._r8 - frac_h2osfc(c))) end if @@ -386,6 +391,22 @@ subroutine SetQflxInputs(bounds, num_hydrologyc, filter_hydrologyc, & qflx_in_soil(c) = qflx_in_soil(c) - max(0._r8, 1.0_r8 - fsno - frac_h2osfc(c) - frac_nvp_eff)*qflx_evap qflx_top_soil_to_h2osfc(c) = qflx_top_soil_to_h2osfc(c) - frac_h2osfc(c) * qflx_ev_h2osfc(c) + ! [PORTED by Hui Tang: DEBUG (errh2o rain-partition) — the soil-input partition (~line 365) + ! uses frac_nvp_eff WITHOUT a snow term, while the moss only receives rain via qflx_nvp_infl + ! (frac_sno_eff-subtracted) or via snow percolation. 'withheld_for_nvp' is the top water + ! deducted from qflx_in_soil for the NVP fraction; compare it against what the moss actually + ! gains ([NVP DBG] after NVPWaterBal: qflx_nvp_infl/Δliq0, + snow percolation into j=0). Any + ! gap is the rain-on-snow leak. Guard: NVP active + water on top. Remove after diagnosis.] + if (use_nvp .and. col%nvp_layer_active(c) .and. qflx_top_soil(c) > 1.e-8_r8) then + write(iulog,*) '[NVP DBG QIN] nstep=', get_nstep(), ' c=', c, ' snl=', col%snl(c), & + ' fsno=', fsno, ' frac_h2osfc=', frac_h2osfc(c), ' frac_nvp=', col%frac_nvp(c) + write(iulog,*) '[NVP DBG QIN] frac_nvp_eff_soil(365)=', frac_nvp_eff_soil, & + ' frac_nvp_eff_evap(378)=', frac_nvp_eff + write(iulog,*) '[NVP DBG QIN] qflx_top_soil=', qflx_top_soil(c), & + ' qflx_in_soil=', qflx_in_soil(c), & + ' withheld_for_nvp=', frac_nvp_eff_soil*(qflx_top_soil(c)-qflx_sat_excess_surf(c)) + end if + end do end associate From 9410b725f9649856e8822a4e477690954fa9c134 Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Thu, 18 Jun 2026 02:06:42 +0300 Subject: [PATCH 112/113] Correct syntax errors in SurfaceRadiationMod.F90 --- src/biogeophys/SurfaceRadiationMod.F90 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/biogeophys/SurfaceRadiationMod.F90 b/src/biogeophys/SurfaceRadiationMod.F90 index bc3efc5b16..413c11f661 100644 --- a/src/biogeophys/SurfaceRadiationMod.F90 +++ b/src/biogeophys/SurfaceRadiationMod.F90 @@ -645,13 +645,13 @@ subroutine SurfaceRadiation(bounds, num_nourbanp, filter_nourbanp, & fsds_sno_nd => surfrad_inst%fsds_sno_nd_patch , & ! Output: [real(r8) (:) ] incident near-IR, direct radiation on snow (for history files) (patch) [W/m2] fsds_sno_vi => surfrad_inst%fsds_sno_vi_patch , & ! Output: [real(r8) (:) ] incident visible, diffuse radiation on snow (for history files) (patch) [W/m2] fsds_sno_ni => surfrad_inst%fsds_sno_ni_patch , & ! Output: [real(r8) (:) ] incident near-IR, diffuse radiation on snow (for history files) (patch) [W/m2] - frac_sno => waterdiagnosticbulk_inst%frac_sno_col & ! Input: [real(r8) (:) ] fraction of ground covered by snow (0 to 1) + frac_sno_eff => waterdiagnosticbulk_inst%frac_sno_eff_col & ! Input: [real(r8) (:) ] fraction of ground covered by snow (0 to 1) ) ! Determine seconds off current time step dtime = get_step_size_real() -y + ! Initialize fluxes do fp = 1,num_nourbanp From 4f85cbca258ed9d409e169304ddf8de1a24a255b Mon Sep 17 00:00:00 2001 From: Hui Tang Date: Thu, 18 Jun 2026 02:07:59 +0300 Subject: [PATCH 113/113] Add cair, oair and rb_pa into the wrap func of nvp photosynthesis. --- src/main/clm_driver.F90 | 2 +- src/utils/clmfates_interfaceMod.F90 | 21 ++++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/main/clm_driver.F90 b/src/main/clm_driver.F90 index 7f23ea7614..d8a1f977de 100644 --- a/src/main/clm_driver.F90 +++ b/src/main/clm_driver.F90 @@ -793,7 +793,7 @@ subroutine clm_drv(doalb, nextsw_cday, declinp1, declin, rstwr, nlend, rdate, ro if (use_fates .and. use_nvp) then call clm_fates%wrap_nvp_photosynthesis(nc, bounds_clump, & atm2lnd_inst, temperature_inst, & - water_inst%waterdiagnosticbulk_inst) + water_inst%waterdiagnosticbulk_inst, frictionvel_inst) end if ! Fluxes for all urban landunits diff --git a/src/utils/clmfates_interfaceMod.F90 b/src/utils/clmfates_interfaceMod.F90 index de01a929c9..0a6c97c995 100644 --- a/src/utils/clmfates_interfaceMod.F90 +++ b/src/utils/clmfates_interfaceMod.F90 @@ -2835,7 +2835,7 @@ end subroutine wrap_photosynthesis ! ====================================================================================== subroutine wrap_nvp_photosynthesis(this, nc, bounds, & - atm2lnd_inst, temperature_inst, waterdiagnosticbulk_inst) + atm2lnd_inst, temperature_inst, waterdiagnosticbulk_inst, frictionvel_inst) ! [PORTED by Hui Tang: separate NVP (moss/lichen) photosynthesis call. ! @@ -2862,6 +2862,9 @@ subroutine wrap_nvp_photosynthesis(this, nc, bounds, & type(atm2lnd_type), intent(in) :: atm2lnd_inst type(temperature_type), intent(in) :: temperature_inst type(waterdiagnosticbulk_type), intent(in) :: waterdiagnosticbulk_inst + ! [PORTED by Hui Tang: frictionvel_inst supplies the bare-ground aerodynamic + ! resistance (ram1_patch) used as the NVP leaf boundary-layer resistance rb_pa.] + type(frictionvel_type), intent(in) :: frictionvel_inst integer :: s, c, p, ifp, g real(r8) :: dtime @@ -2894,6 +2897,22 @@ subroutine wrap_nvp_photosynthesis(this, nc, bounds, & this%fates(nc)%bc_in(s)%t_nvp_pa(ifp) = temperature_inst%t_nvp_col(c) this%fates(nc)%bc_in(s)%fwet_nvp_pa(ifp) = waterdiagnosticbulk_inst%fwet_nvp_col(c) + ! [PORTED by Hui Tang: set atmospheric O2/CO2 partial pressures for NVP photosynthesis. + ! CanopyFluxes (and hence wrap_photosynthesis, which normally sets oair_pa/cair_pa + ! from forc_po2_grc/forc_pco2_grc) is never called for NVP columns, so these would + ! otherwise retain uninitialized/stale values and corrupt the Farquhar CO2/O2 terms + ! in FatesPlantRespPhotosynthDrive. Mirror the assignment in wrap_photosynthesis.] + this%fates(nc)%bc_in(s)%oair_pa(ifp) = atm2lnd_inst%forc_po2_grc(g) + this%fates(nc)%bc_in(s)%cair_pa(ifp) = atm2lnd_inst%forc_pco2_grc(g) + + ! [PORTED by Hui Tang: NVP leaf boundary-layer resistance. CanopyFluxes + ! (which normally sets rb_pa) is never called for NVP columns, so use the + ! bare-ground aerodynamic resistance ram1_patch from BareGroundFluxes. This + ! is consumed in LeafBiophysicsMod (gb = 1/rb_pa) for the NVP boundary-layer- + ! only CO2 diffusion; the moss water-film resistance is handled separately by + ! the fwet_nvp term, so ram (not ram+rnvp) is used here.] + this%fates(nc)%bc_in(s)%rb_pa(ifp) = frictionvel_inst%ram1_patch(p) + ! [PORTED by Hui Tang: dayl_factor_pa — CanopyFluxesMod is never called for NVP ! columns, so dayl_factor_pa is never set there; compute it here directly from ! gridcell daylength following the same formula as CanopyFluxesMod line 840.]