Repository: NGEET/fates
Pull request #1364: vegetation bin indexing
Author: login: rgknox
Labels: name: type: refactor
description: Restructures code without changing functionality
Created: 2025-03-18T21:34:26Z | Merged: 2025-04-28T13:40:27Z

<!--- Provide a general summary of your changes in the Title above -->

### Description:

This set of changes introduces some consistency improvements to how we determine the number of vegetation layers, and how we access and use those indices.  Of note, the lower bound of this array has been changed to 0.

<!--- Describe your changes in detail -->
<!--- please add issue number if one exists -->

### Collaborators:

@ckoven 

<!--- List names of collaborators or people who have interacted -->
<!--- in bringing about this set of changes -->
<!--- consultation, discussions, etc. -->

### Expectation of Answer Changes:

Not sure. This may be a bug fix as well.  Looking again.

<!--- Please describe under what conditions, if any, -->
<!--- the model is expected to generated different answers -->
<!--- from the master version of the code -->

### Checklist
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
*If this is your first time contributing, please read the [**CONTRIBUTING**](https://github.com/NGEET/fates/blob/main/CONTRIBUTING.md) document.*

All checklist items must be checked to enable merging this pull request:

*Contributor*
- [x] The in-code documentation has been updated with descriptive comments
- [x] The documentation has been assessed to determine if updates are necessary

*Integrator*
- [x] FATES PASS/FAIL regression tests were run
- [ ] Evaluation of test results for answer changes was performed and results provided

### Documentation
<!--- If this pull requests warrants an update to the tech doc or user's guide, and said changes have been made paste a link to the documentation pull request below.  -->
<!--- If documentation updates are needed, but changes do not yet have their own separate pull request, please create an issue on either repo so that we can keep track of necessary updates.-->
- [Technical Note](https://github.com/NGEET/fates-docs) update:
- [User's Guide](https://github.com/NGEET/fates-users-guide) update: 

### Test Results:
<!--- Non-trivial changes require the PASS/FAIL regression tests. -->
<!--- If changes to code are NOT expected to change answers, tests must -->
<!--- be run against a baseline. -->

*CTSM (or) E3SM (specify which) test hash-tag:*

*CTSM (or) E3SM (specify which) baseline hash-tag:*

*FATES baseline hash-tag:*

*Test Output:*

<!--- paste in test results here -->


<!--this template is from https://www.talater.com/open-source-templates/#/page/99--> 




Diff:
diff --git a/biogeochem/EDCanopyStructureMod.F90 b/biogeochem/EDCanopyStructureMod.F90
index 081c734dd5..f7d4782f15 100644
--- a/biogeochem/EDCanopyStructureMod.F90
+++ b/biogeochem/EDCanopyStructureMod.F90
@@ -25,8 +25,9 @@ module EDCanopyStructureMod
   use FatesAllometryMod     , only : CrownDepth
   use FatesPatchMod,          only : fates_patch_type
   use FatesCohortMod,         only : fates_cohort_type
-  use EDParamsMod            , only : nclmax
-  use EDParamsMod            , only : nlevleaf
+  use EDParamsMod           , only : nclmax
+  use EDParamsMod           , only : nlevleaf
+  use EDParamsMod           , only : GetNVegLayers
   use EDtypesMod            , only : AREA
   use EDLoggingMortalityMod , only : UpdateHarvestC
   use FatesGlobals          , only : endrun => fates_endrun
@@ -1517,7 +1518,7 @@ subroutine leaf_area_profile( currentSite )
     ! !USES:
 
     use EDtypesMod           , only : area, heightmax, n_height_bins
-    use EDParamsMod,           only : dinc_vai, dlower_vai
+    use EDParamsMod,           only : dlower_vai,dinc_vai
 
     !
     ! !ARGUMENTS
@@ -1669,15 +1670,7 @@ subroutine leaf_area_profile( currentSite )
                    endif
 
                    if(iv==currentCohort%NV) then
-                      remainder = (currentCohort%treelai + currentCohort%treesai) - &
-                           (dlower_vai(iv) - dinc_vai(iv))
-                      if(remainder > dinc_vai(iv) )then
-                         write(fates_log(), *)'ED: issue with remainder', &
-                              currentCohort%treelai,currentCohort%treesai,dinc_vai(iv), & 
-                              currentCohort%NV,remainder
-
-                         call endrun(msg=errMsg(sourcefile, __LINE__))
-                      endif
+                      remainder = (currentCohort%treelai + currentCohort%treesai) - dlower_vai(iv)
                    else
                       remainder = dinc_vai(iv)
                    end if
@@ -2240,9 +2233,6 @@ subroutine UpdateCohortLAI(currentCohort, canopy_layer_tlai, total_canopy_area)
    
    ! Update LAI and related variables for a given cohort
    
-   ! Uses
-   use EDParamsMod, only : dlower_vai, dinc_vai
-   
    ! Arguments
    type(fates_cohort_type),intent(inout), target   :: currentCohort
    real(r8), intent(in) :: canopy_layer_tlai(nclmax)  ! total leaf area index of each canopy layer
@@ -2266,14 +2256,7 @@ subroutine UpdateCohortLAI(currentCohort, canopy_layer_tlai, total_canopy_area)
    end if
    
    ! Number of actual vegetation layers in this cohort's crown
-   currentCohort%nv =  count((currentCohort%treelai+currentCohort%treesai) .gt. dlower_vai(:)) + 1
-
-   if( currentCohort%nv .ne. minloc(dlower_vai, DIM=1, MASK=(dlower_vai>(currentCohort%treelai+currentCohort%treesai))) ) then
-      write(fates_log(),*) 'We use two methods of finding maximum leaf layers, and they are not equivalent'
-      write(fates_log(),*) 'count method:',currentCohort%nv
-      write(fates_log(),*) 'minloc method:',minloc(dlower_vai, DIM=1, MASK=(dlower_vai>(currentCohort%treelai+currentCohort%treesai)))
-      call endrun(msg=errMsg(sourcefile, __LINE__))
-   end if
+   currentCohort%nv = GetNVegLayers(currentCohort%treelai+currentCohort%treesai)
    
   end subroutine UpdateCohortLAI
   
diff --git a/biogeochem/EDPhysiologyMod.F90 b/biogeochem/EDPhysiologyMod.F90
index 3b1d61e914..54f68036cf 100644
--- a/biogeochem/EDPhysiologyMod.F90
+++ b/biogeochem/EDPhysiologyMod.F90
@@ -18,7 +18,8 @@ module EDPhysiologyMod
   use FatesInterfaceTypesMod, only    : hlm_parteh_mode
   use FatesInterfaceTypesMod, only    : hlm_use_fixed_biogeog
   use FatesInterfaceTypesMod, only    : hlm_use_nocomp
-  use EDParamsMod           , only    : crop_lu_pft_vector     
+  use EDParamsMod           , only    : crop_lu_pft_vector
+  use EDParamsMod           , only    : GetNVegLayers
   use FatesInterfaceTypesMod, only    : hlm_nitrogen_spec
   use FatesInterfaceTypesMod, only    : hlm_phosphorus_spec
   use FatesInterfaceTypesMod, only    : hlm_use_tree_damage
@@ -629,14 +630,11 @@ subroutine trim_canopy( currentSite )
     real(r8) :: sapw_c                ! sapwood carbon [kg]
     real(r8) :: store_c               ! storage carbon [kg]
     real(r8) :: struct_c              ! structure carbon [kg]
-    real(r8) :: leaf_inc              ! LAI-only portion of the vegetation increment of dinc_vai
     real(r8) :: lai_canopy_above      ! the LAI in the canopy layers above the layer of interest
-    real(r8) :: lai_layers_above      ! the LAI in the leaf layers, within the current canopy,
-    ! above the leaf layer of interest
-    real(r8) :: lai_current           ! the LAI in the current leaf layer
     real(r8) :: cumulative_lai        ! whole canopy cumulative LAI, top down, to the leaf layer of interest
     real(r8) :: cumulative_lai_cohort ! cumulative LAI within the current cohort only
-
+    real(r8) :: leaf_veg_frac         ! fraction of vegetation area (leaf+stem) that is just leaf
+    
     ! Temporary diagnostic ouptut
 
     ! LAPACK linear least squares fit variables
@@ -704,15 +702,10 @@ subroutine trim_canopy( currentSite )
                currentCohort%dbh, currentCohort%crowndamage, currentCohort%canopy_trim, &
                currentCohort%efstem_coh, 0, currentCohort%treelai, currentCohort%treesai )
 
-          currentCohort%nv      = count((currentCohort%treelai+currentCohort%treesai) .gt. dlower_vai(:)) + 1
-
-          if (currentCohort%nv > nlevleaf)then
-             write(fates_log(),*) 'nv > nlevleaf',currentCohort%nv, &
-                  currentCohort%treelai,currentCohort%treesai, &
-                  currentCohort%c_area,currentCohort%n,leaf_c
-             call endrun(msg=errMsg(sourcefile, __LINE__))
-          endif
+          currentCohort%nv = GetNVegLayers(currentCohort%treelai+currentCohort%treesai)
 
+          leaf_veg_frac = currentCohort%treelai/(currentCohort%treelai+currentCohort%treesai)
+          
           ! Find target leaf biomass. Here we assume that leaves would be fully flushed 
           ! (elongation factor = 1)
           call bleaf(currentcohort%dbh,ipft,&
@@ -746,20 +739,18 @@ subroutine trim_canopy( currentSite )
           !Leaf cost vs net uptake for each leaf layer.
           do z = 1, currentCohort%nv
 
-             ! Calculate the cumulative total vegetation area index (no snow occlusion, stems and leaves)
-             leaf_inc    = dinc_vai(z) * &
-                  currentCohort%treelai/(currentCohort%treelai+currentCohort%treesai)
-             
-             ! Now calculate the cumulative top-down lai of the current layer's midpoint within the current cohort
-             lai_layers_above      = (dlower_vai(z) - dinc_vai(z)) * &
-                  currentCohort%treelai/(currentCohort%treelai+currentCohort%treesai)
-             lai_current           = min(leaf_inc, currentCohort%treelai - lai_layers_above)
-             cumulative_lai_cohort = lai_layers_above + 0.5*lai_current
-
-             ! Now add in the lai above the current cohort for calculating the sla leaf level
              lai_canopy_above  = sum(currentPatch%canopy_layer_tlai(1:cl-1))
-             cumulative_lai    = lai_canopy_above + cumulative_lai_cohort
+                                    
+             if(z == currentCohort%nv) then
+                cumulative_lai_cohort = leaf_veg_frac * &
+                     (dlower_vai(z)+0.5_r8*(currentCohort%treelai+currentCohort%treesai-dlower_vai(z)))
+             else
+                cumulative_lai_cohort = leaf_veg_frac * &
+                     (dlower_vai(z)+0.5_r8*dinc_vai(z))
+             end if
 
+             cumulative_lai = cumulative_lai_cohort + lai_canopy_above
+             
              ! There was activity this year in this leaf layer.  This should only occur for bottom most leaf layer
              if (currentCohort%year_net_uptake(z) /= 999._r8)then
 
diff --git a/biogeochem/FatesAllometryMod.F90 b/biogeochem/FatesAllometryMod.F90
index 28cd59dde3..d7e09393d4 100644
--- a/biogeochem/FatesAllometryMod.F90
+++ b/biogeochem/FatesAllometryMod.F90
@@ -860,7 +860,11 @@ subroutine tree_lai_sai(leaf_c, pft, c_area, nplant, cl, canopy_lai, vcmax25top,
     
     real(r8), intent(out) :: treelai         ! plant LAI [m2 leaf area/m2 crown area]
     real(r8), intent(out) :: treesai         ! plant SAI [m2 stem area/m2 crown area]
-   
+
+    ! If this is true, prevent plants from exceeding the maximum VAI
+    ! that is specified in the dinc_vai array.
+    logical, parameter :: do_vai_capping = .true.
+    
     
     treelai = tree_lai( leaf_c, pft, c_area, nplant, cl, canopy_lai, vcmax25top)
     
@@ -868,12 +872,13 @@ subroutine tree_lai_sai(leaf_c, pft, c_area, nplant, cl, canopy_lai, vcmax25top,
                              cl, canopy_lai, treelai, vcmax25top, call_id )
 
     ! Don't allow lai+sai to exceed the vertical discretization bounds
-    if( (treelai + treesai) > (sum(dinc_vai)) )then
-       treelai = sum(dinc_vai) * (1._r8 - prt_params%allom_sai_scaler(pft)) - nearzero
-       treesai = sum(dinc_vai) * prt_params%allom_sai_scaler(pft) - nearzero
+    if( do_vai_capping ) then
+       if( (treelai + treesai) > (sum(dinc_vai)) )then
+          treelai = sum(dinc_vai) * (1._r8 - prt_params%allom_sai_scaler(pft)) - nearzero
+          treesai = sum(dinc_vai) * prt_params%allom_sai_scaler(pft) - nearzero
+       end if
     end if
     
-    
     return
   end subroutine tree_lai_sai
 
@@ -3177,22 +3182,21 @@ subroutine VegAreaLayer(tree_lai,tree_sai,tree_height,iv,nv,pft,snow_depth, &
 
     if_any_vai: if(tree_vai>0._r8)then
 
+       ! This function will return the total VAI of the cohort
+       ! if an index of 0 is passed in...
        if(iv==0)then
           vai_top = 0.0
           vai_bot = tree_vai
        else
 
-          if(iv>1)then
-             vai_top = dlower_vai(iv) - dinc_vai(iv)
-          else
-             vai_top = 0._r8
-          end if
-
-          if(iv<nv) then
-             vai_bot = dlower_vai(iv)
-          else
+          vai_top = dlower_vai(iv)
+          
+          if(iv==nv) then
              vai_bot = tree_vai
+          else
+             vai_bot = dlower_vai(iv+1)
           end if
+          
        end if
 
        if(layer_height_method .eq. layer_height_const_depth)then
diff --git a/biogeophys/FatesPlantRespPhotosynthMod.F90 b/biogeophys/FatesPlantRespPhotosynthMod.F90
index b51dde4c37..a88dc681fd 100644
--- a/biogeophys/FatesPlantRespPhotosynthMod.F90
+++ b/biogeophys/FatesPlantRespPhotosynthMod.F90
@@ -216,11 +216,8 @@ subroutine FatesPlantRespPhotosynthDrive (nsites, sites,bc_in,bc_out,dtime)
     real(r8) :: lnc_top                          ! Leaf nitrogen content per unit area at canopy top [gN/m2]
     real(r8) :: lmr25top                         ! canopy top leaf maint resp rate at 25C 
                                                  ! for this plant or pft (umol CO2/m**2/s)
-    real(r8) :: leaf_inc                         ! LAI-only portion of the vegetation increment of dinc_vai
     real(r8) :: lai_canopy_above                 ! the LAI in the canopy layers above the layer of interest
-    real(r8) :: lai_layers_above                 ! the LAI in the leaf layers, within the current canopy,
-                                                 ! above the leaf layer of interest
-    real(r8) :: lai_current                      ! the LAI in the current leaf layer
+    real(r8) :: leaf_veg_frac                    ! fraction of vegetation area (leaf+stem) that is just leaf
     real(r8) :: cumulative_lai                   ! the cumulative LAI, top down, to the leaf layer of interest
     real(r8) :: leaf_psi                         ! leaf xylem matric potential [MPa] (only meaningful/used w/ hydro)
     real(r8) :: fnrt_mr_layer                    ! fine root maintenance respiation per layer [kgC/plant/s]
@@ -478,6 +475,9 @@ subroutine FatesPlantRespPhotosynthDrive (nsites, sites,bc_in,bc_out,dtime)
                         canopy_mask_if: if(currentPatch%canopy_mask(cl,ft) == 1)then
 
                            ! Loop over leaf-layers
+
+                           leaf_veg_frac = currentCohort%treelai/(currentCohort%treelai+currentCohort%treesai)
+                           
                            leaf_layer_loop : do iv = 1,currentCohort%nv
 
                               ! ------------------------------------------------------------
@@ -510,20 +510,18 @@ subroutine FatesPlantRespPhotosynthDrive (nsites, sites,bc_in,bc_out,dtime)
                                  if (hlm_use_planthydro.eq.itrue ) then
 
                                     btran_eff = currentCohort%co_hydr%btran 
+                                    
+                                    ! Find the cumulative LAI from the top of this cohort's crown to the
+                                    ! center of the current veg layer. If this is the cohort's last layer
+                                    ! then the mid-point is between the dlower and the total lai
 
-                                    ! dinc_vai(:) is the total vegetation area index of each "leaf" layer
-                                    ! we convert to the leaf only portion of the increment
-                                    ! ------------------------------------------------------
-                                    leaf_inc    = dinc_vai(iv) * &
-                                         currentCohort%treelai/(currentCohort%treelai+currentCohort%treesai)
-
-                                    ! Now calculate the cumulative top-down lai of the current layer's midpoint
-                                    lai_canopy_above  = sum(currentPatch%canopy_layer_tlai(1:cl-1)) 
-
-                                    lai_layers_above  = (dlower_vai(iv) - dinc_vai(iv)) * &
-                                         currentCohort%treelai/(currentCohort%treelai+currentCohort%treesai)
-                                    lai_current       = min(leaf_inc, currentCohort%treelai - lai_layers_above)
-                                    cumulative_lai    = lai_canopy_above + lai_layers_above + 0.5*lai_current 
+                                    lai_canopy_above  = sum(currentPatch%canopy_layer_tlai(1:cl-1))
+                                    
+                                    if(iv == currentCohort%nv) then
+                                       cumulative_lai = lai_canopy_above + leaf_veg_frac * (dlower_vai(iv)+0.5_r8*(currentCohort%treelai+currentCohort%treesai-dlower_vai(iv)))
+                                    else
+                                       cumulative_lai = lai_canopy_above + leaf_veg_frac * (dlower_vai(iv)+0.5_r8*dinc_vai(iv))
+                                    end if
 
                                     leaf_psi = currentCohort%co_hydr%psi_ag(1)
 
diff --git a/main/EDParamsMod.F90 b/main/EDParamsMod.F90
index 92d8178ab0..b90b547dad 100644
--- a/main/EDParamsMod.F90
+++ b/main/EDParamsMod.F90
@@ -1,8 +1,8 @@
 module EDParamsMod
 
-   !
-   ! module that deals with reading the ED parameter file
-   !
+                                                                      !
+                                                                      ! module that deals with reading the ED parameter file
+                                                                      !
 
    use FatesConstantsMod, only : r8 => fates_r8
    use FatesConstantsMod, only : nearzero
@@ -12,37 +12,37 @@ module EDParamsMod
    use FatesConstantsMod,    only : fates_unset_r8
    use FatesConstantsMod,    only : n_landuse_cats
 
-   ! CIME Globals
+                                                                      ! CIME Globals
    use shr_log_mod         , only : errMsg => shr_log_errMsg
 
    implicit none
    private
    save
 
-   !
-   ! this is what the user can use for the actual values
-   !
-
-   real(r8),protected, public :: vai_top_bin_width           ! width in VAI units of uppermost leaf+stem
-                                                             ! layer scattering element in each canopy layer [m2/m2]
-   real(r8),protected, public :: vai_width_increase_factor   ! factor by which each leaf+stem scattering element
-                                                             ! increases in VAI width (1 = uniform spacing)
-   real(r8),protected, public :: photo_temp_acclim_timescale ! Length of the window for the exponential moving average (ema)
-                                                             ! of vegetation temperature used in photosynthesis and respiration
-                                                             ! temperature acclimation [days]
-   real(r8),protected, public :: photo_temp_acclim_thome_time ! Length of the window for the long-term exponential moving average (ema)
-                                                              ! of vegetation temperature used in photosynthesis 
-                                                              ! T_home term in Kumarathunge parameterization [years]
-   real(r8),protected, public :: sdlng_emerg_h2o_timescale !Length of the window for the exponential moving
-                                                                 !average of smp used to calculate seedling emergence
-   real(r8),protected, public :: sdlng_mort_par_timescale !Length of the window for the exponential moving average 
-                                                                !of par at the seedling layer used to calculate 
-                                                                !seedling mortality
-   real(r8),protected, public :: sdlng_mdd_timescale !Length of the window for the exponential moving average
-                                                           ! of moisture deficit days used to calculate seedling mortality
-   real(r8),protected, public :: sdlng2sap_par_timescale !Length of the window for the exponential 
-                                                               !moving average of par at the seedling layer used to 
-                                                               !calculate seedling to sapling transition rates
+                                                                      !
+                                                                      ! this is what the user can use for the actual values
+                                                                      !
+
+   real(r8),protected, public :: vai_top_bin_width                    ! width in VAI units of uppermost leaf+stem
+                                                                      ! layer scattering element in each canopy layer [m2/m2]
+   real(r8),protected, public :: vai_width_increase_factor            ! factor by which each leaf+stem scattering element
+                                                                      ! increases in VAI width (1 = uniform spacing)
+   real(r8),protected, public :: photo_temp_acclim_timescale          ! Length of the window for the exponential moving average (ema)
+                                                                      ! of vegetation temperature used in photosynthesis and respiration
+                                                                      ! temperature acclimation [days]
+   real(r8),protected, public :: photo_temp_acclim_thome_time         ! Length of the window for the long-term exponential moving average (ema)
+                                                                      ! of vegetation temperature used in photosynthesis 
+                                                                      ! T_home term in Kumarathunge parameterization [years]
+   real(r8),protected, public :: sdlng_emerg_h2o_timescale            !Length of the window for the exponential moving
+                                                                      !average of smp used to calculate seedling emergence
+   real(r8),protected, public :: sdlng_mort_par_timescale             !Length of the window for the exponential moving average 
+                                                                      !of par at the seedling layer used to calculate 
+                                                                      !seedling mortality
+   real(r8),protected, public :: sdlng_mdd_timescale                  !Length of the window for the exponential moving average
+                                                                      ! of moisture deficit days used to calculate seedling mortality
+   real(r8),protected, public :: sdlng2sap_par_timescale              !Length of the window for the exponential 
+                                                                      !moving average of par at the seedling layer used to 
+                                                                      !calculate seedling to sapling transition rates
    real(r8),protected, public :: fates_mortality_disturbance_fraction ! the fraction of canopy mortality that results in disturbance
    real(r8),protected, public :: ED_val_comp_excln                    ! weighting factor for canopy layer exclusion and promotion
    real(r8),protected, public :: ED_val_vai_top_bin_width             ! width in VAI units of uppermost leaf+stem layer scattering element
@@ -64,33 +64,37 @@ module EDParamsMod
    real(r8),protected, public :: ED_val_patch_fusion_tol              ! minimum fraction in difference in profiles between patches
    real(r8),protected, public :: ED_val_canopy_closure_thresh         ! site-level canopy closure point where trees take on forest (narrow) versus savannah (wide) crown allometry
 
-   logical,protected, public :: active_crown_fire        ! flag, 1=active crown fire 0=no active crown fire
+   logical,protected, public :: active_crown_fire                     ! flag, 1=active crown fire 0=no active crown fire
    character(len=param_string_length),parameter :: fates_name_active_crown_fire = "fates_fire_active_crown_fire"
 
-   real(r8), protected, public :: cg_strikes             ! fraction of cloud to ground lightning strikes (0-1)
+   real(r8), protected, public :: cg_strikes                          ! fraction of cloud to ground lightning strikes (0-1)
    character(len=param_string_length),parameter :: fates_name_cg_strikes="fates_fire_cg_strikes"
 
-   ! Global identifier of how nutrients interact with the host land model
-  ! either they are fully coupled, or they generate uptake rates synthetically
-  ! in prescribed mode. In the latter, there is both NO mass removed from the HLM's soil
-  ! BGC N and P pools, and there is also none removed.
+                                                                      ! Global identifier of how nutrients interact with the host land model
+                                                                      ! either they are fully coupled, or they generate uptake rates synthetically
+                                                                      ! in prescribed mode. In the latter, there is both NO mass removed from the HLM's soil
+                                                                      ! BGC N and P pools, and there is also none removed.
 
    integer, public :: n_uptake_mode
    integer, public :: p_uptake_mode
 
-   real(r8), parameter, public :: soil_tfrz_thresh = -2.0_r8 ! Soil temperature threshold below which hydraulic failure mortality is off (non-hydro only) in degrees C
+   real(r8), parameter, public :: soil_tfrz_thresh = -2.0_r8          ! Soil temperature threshold below which hydraulic failure mortality is off (non-hydro only) in degrees C
+   
+   integer, parameter, public :: nclmax = 2                           ! Maximum number of canopy layers (used only for scratch arrays)
+                                                                      ! We would make this even higher, but making this
+                                                                      ! a little lower keeps the size down on some output arrays
+                                                                      ! For large arrays at patch level we use dynamic allocation
+
+                                                                      ! parameters that govern the VAI (LAI+SAI) bins used in radiative transfer code
+   integer, parameter, public :: nlevleaf = 30                        ! number of leaf+stem layers in each canopy layer
+
+   real(r8), public :: dinc_vai(nlevleaf)   = fates_unset_r8          ! VAI bin widths array
+   real(r8), public :: dlower_vai(nlevleaf) = fates_unset_r8          ! numericaly (not vertically) lower edges of VAI bins
+                                                                      ! starting with zero in the first index, the last bin
+                                                                      ! is assumed to be bounded, but a user can override this
+                                                                      ! if change a local parameter vai_capping in tree_lai()
+                                                                      ! in the allometry module
    
-   integer, parameter, public :: nclmax = 2   ! Maximum number of canopy layers (used only for scratch arrays)
-                                              ! We would make this even higher, but making this
-                                              ! a little lower keeps the size down on some output arrays
-                                              ! For large arrays at patch level we use dynamic allocation
-
-   ! parameters that govern the VAI (LAI+SAI) bins used in radiative transfer code
-   integer, parameter, public :: nlevleaf = 30   ! number of leaf+stem layers in each canopy layer
-
-   real(r8), public :: dinc_vai(nlevleaf)   = fates_unset_r8 ! VAI bin widths array
-   real(r8), public :: dlower_vai(nlevleaf) = fates_unset_r8 ! lower edges of VAI bins
- 
    integer, parameter, public :: maxpft = 16      ! maximum number of PFTs allowed
    
    real(r8),protected,public  :: q10_mr     ! Q10 for respiration rate (for soil fragmenation and plant respiration)    (unitless)
@@ -264,10 +268,23 @@ module EDParamsMod
    public :: FatesRegisterParams
    public :: FatesReceiveParams
    public :: FatesReportParams
-  
-contains
+   public :: GetNVegLayers
+
+   
+ contains
 
+
+   function GetNVegLayers(treevai) result(nv)
+
+     real(r8) :: treevai  ! The LAI+SAI of the cohort (m2/m2)
+     integer  :: nv
+
+     nv = count(treevai .gt. dlower_vai(:))
+
+   end function GetNVegLayers
+     
   !-----------------------------------------------------------------------
+   
   subroutine FatesParamsInit()
     ! Initialize all parameters to nan to ensure that we get valid
     ! values back from the host.
diff --git a/main/FatesInterfaceMod.F90 b/main/FatesInterfaceMod.F90
index 075b8e34f7..555fa526c1 100644
--- a/main/FatesInterfaceMod.F90
+++ b/main/FatesInterfaceMod.F90
@@ -952,9 +952,10 @@ subroutine SetFatesGlobalElements2(use_fates)
             call endrun(msg=errMsg(sourcefile, __LINE__))
          end if
          
-         ! lower edges of VAI bins       
-         do i = 1,nlevleaf
-            dlower_vai(i) = sum(dinc_vai(1:i))
+         ! lower edges of VAI bins
+         dlower_vai(1) = 0._r8
+         do i = 2,nlevleaf
+            dlower_vai(i) =  dlower_vai(i-1) + dinc_vai(i-1)
          end do
 
          ! Identify number of size and age class bins for history output
diff --git a/radiation/FatesRadiationDriveMod.F90 b/radiation/FatesRadiationDriveMod.F90
index 603c4488c5..fb97b80cf0 100644
--- a/radiation/FatesRadiationDriveMod.F90
+++ b/radiation/FatesRadiationDriveMod.F90
@@ -13,6 +13,7 @@ module FatesRadiationDriveMod
   use EDTypesMod        , only : ed_site_type
   use FatesPatchMod,      only : fates_patch_type
   use EDParamsMod,        only : maxpft
+  use EDParamsMod       , only : GetNVegLayers
   use FatesConstantsMod , only : r8 => fates_r8
   use FatesConstantsMod , only : fates_unset_r8
   use FatesConstantsMod , only : itrue
@@ -377,11 +378,18 @@ subroutine FatesSunShadeFracs(nsites, sites,bc_in,bc_out)
                            if_notair: if (ft>0) then
                               area_frac = twostr%scelg(cl,icol)%area
                               vai = twostr%scelg(cl,icol)%sai+twostr%scelg(cl,icol)%lai
-                              nv = minloc(dlower_vai, DIM=1, MASK=(dlower_vai>vai))
+
+                              nv = GetNVegLayers(vai)
+
                               do iv = 1, nv
                                  
-                                 vai_top = dlower_vai(iv)-dinc_vai(iv)
-                                 vai_bot = min(dlower_vai(iv),twostr%scelg(cl,icol)%sai+twostr%scelg(cl,icol)%lai)
+                                 vai_top = dlower_vai(iv)
+
+                                 if(iv == nv) then
+                                    vai_bot = twostr%scelg(cl,icol)%sai+twostr%scelg(cl,icol)%lai
+                                 else
+                                    vai_bot = dlower_vai(iv+1)
+                                 end if
                                  
                                  cpatch%parprof_pft_dir_z(cl,ft,iv) = cpatch%parprof_pft_dir_z(cl,ft,iv) + &
                                       area_frac*twostr%GetRb(cl,icol,ivis,vai_top)
diff --git a/testing/testing_shr/FatesFactoryMod.F90 b/testing/testing_shr/FatesFactoryMod.F90
index e0fe2293c6..aede3dbab9 100644
--- a/testing/testing_shr/FatesFactoryMod.F90
+++ b/testing/testing_shr/FatesFactoryMod.F90
@@ -103,10 +103,12 @@ subroutine InitializeGlobals(step_size)
       dinc_vai(i) = ED_val_vai_top_bin_width*ED_val_vai_width_increase_factor**(i-1)
     end do 
   
-    do i = 1, nlevleaf
-      dlower_vai(i) = sum(dinc_vai(1:i))
+    ! lower edges of VAI bins
+    dlower_vai(1) = 0._r8
+    do i = 2,nlevleaf
+       dlower_vai(i) =  dlower_vai(i-1) + dinc_vai(i-1)
     end do
-        
+    
   end subroutine InitializeGlobals
   
   !---------------------------------------------------------------------------------------
@@ -533,4 +535,4 @@ subroutine CreateTestPatchList(patch, heights, dbhs)
   
   end subroutine CreateTestPatchList
   
-end module FatesFactoryMod
\ No newline at end of file
+end module FatesFactoryMod


Reviews:
user: login: glemieux
body: I have a couple of questions, but I think they shouldn't necessarily hold up this PR.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T21:49:35Z
user: login: rgknox
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T19:39:34Z
user: login: rgknox
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T19:40:20Z

Review comments:
diff_hunk: @@ -438,11 +439,18 @@ subroutine FatesSunShadeFracs(nsites, sites,bc_in,bc_out)
                            if_notair: if (ft>0) then
                               area_frac = twostr%scelg(cl,icol)%area
                               vai = twostr%scelg(cl,icol)%sai+twostr%scelg(cl,icol)%lai
-                              nv = minloc(dlower_vai, DIM=1, MASK=(dlower_vai>vai))
+
+                              nv = GetNVegLayers(vai)
+
                               do iv = 1, nv
                                  
-                                 vai_top = dlower_vai(iv)-dinc_vai(iv)
-                                 vai_bot = min(dlower_vai(iv),twostr%scelg(cl,icol)%sai+twostr%scelg(cl,icol)%lai)
+                                 vai_top = dlower_vai(iv)
path: radiation/FatesRadiationDriveMod.F90
user: login: glemieux
body: Is it correct to say that `vai_top` has shifted now that the lower bound value is zero?
created_at: 2025-04-23T21:17:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -438,11 +439,18 @@ subroutine FatesSunShadeFracs(nsites, sites,bc_in,bc_out)
                            if_notair: if (ft>0) then
                               area_frac = twostr%scelg(cl,icol)%area
                               vai = twostr%scelg(cl,icol)%sai+twostr%scelg(cl,icol)%lai
-                              nv = minloc(dlower_vai, DIM=1, MASK=(dlower_vai>vai))
+
+                              nv = GetNVegLayers(vai)
+
                               do iv = 1, nv
                                  
-                                 vai_top = dlower_vai(iv)-dinc_vai(iv)
-                                 vai_bot = min(dlower_vai(iv),twostr%scelg(cl,icol)%sai+twostr%scelg(cl,icol)%lai)
+                                 vai_top = dlower_vai(iv)
+
+                                 if(iv == nv) then
+                                    vai_bot = twostr%scelg(cl,icol)%sai+twostr%scelg(cl,icol)%lai
+                                 else
+                                    vai_bot = dlower_vai(iv)+dinc_vai(iv)
path: radiation/FatesRadiationDriveMod.F90
user: login: glemieux
body: This could match the change in FatesAllometryMod line 3197, correct?: `vai_bot = dlower_vai(iv+1)`
created_at: 2025-04-23T21:29:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -438,11 +439,18 @@ subroutine FatesSunShadeFracs(nsites, sites,bc_in,bc_out)
                            if_notair: if (ft>0) then
                               area_frac = twostr%scelg(cl,icol)%area
                               vai = twostr%scelg(cl,icol)%sai+twostr%scelg(cl,icol)%lai
-                              nv = minloc(dlower_vai, DIM=1, MASK=(dlower_vai>vai))
+
+                              nv = GetNVegLayers(vai)
+
                               do iv = 1, nv
                                  
-                                 vai_top = dlower_vai(iv)-dinc_vai(iv)
-                                 vai_bot = min(dlower_vai(iv),twostr%scelg(cl,icol)%sai+twostr%scelg(cl,icol)%lai)
+                                 vai_top = dlower_vai(iv)
path: radiation/FatesRadiationDriveMod.F90
user: login: rgknox
body: vai_top still has the same meaning, but dlower_vai has changed.  Lower used to refer to lower vertical position, now it refers to the numericaly lower edge of the bin.  So vai_top points directly to dlower_vai now since they conceptually align.
created_at: 2025-04-24T19:39:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -438,11 +439,18 @@ subroutine FatesSunShadeFracs(nsites, sites,bc_in,bc_out)
                            if_notair: if (ft>0) then
                               area_frac = twostr%scelg(cl,icol)%area
                               vai = twostr%scelg(cl,icol)%sai+twostr%scelg(cl,icol)%lai
-                              nv = minloc(dlower_vai, DIM=1, MASK=(dlower_vai>vai))
+
+                              nv = GetNVegLayers(vai)
+
                               do iv = 1, nv
                                  
-                                 vai_top = dlower_vai(iv)-dinc_vai(iv)
-                                 vai_bot = min(dlower_vai(iv),twostr%scelg(cl,icol)%sai+twostr%scelg(cl,icol)%lai)
+                                 vai_top = dlower_vai(iv)
+
+                                 if(iv == nv) then
+                                    vai_bot = twostr%scelg(cl,icol)%sai+twostr%scelg(cl,icol)%lai
+                                 else
+                                    vai_bot = dlower_vai(iv)+dinc_vai(iv)
path: radiation/FatesRadiationDriveMod.F90
user: login: rgknox
body: yes, that's true!
created_at: 2025-04-24T19:40:20Z
author_association: CONTRIBUTOR

PR comments:
user: login: glemieux
created_at: 2025-04-24T00:05:20Z
body: Regression testing on `derecho` is underway.
author_association: CONTRIBUTOR
user: login: glemieux
created_at: 2025-04-24T21:05:17Z
body: Regression testing against `fates-sci.1.82.7_api.39.0.0-ctsm5.3.040` is complete.  This results in mostly targeted DIFFs across all testmods in the `fates_levleaf`:
```
 209  fates_levleaf   (fates_levleaf)
 210          30       30  (    30) (     1) (     1) 
 211                   30  0.290000000000000E+02  0.000000000000000E+00  0.100000000000000E+01       0  0.000000000000000E+00  0.000000000000000E+00
 212                   30  0.300000000000000E+02  0.100000000000000E+01                              1                         0.000000000000000E+00
 213                   30  (    30) (     1)
 214  RMS fates_levleaf                    1.0000E+00            NORMALIZED  6.6667E-02
````

As a reminder, the third line (here line number `211`) is the test branch output and the fourth line (`212`) are the baseline comparison values.  The first and second numbers in each line are the max and min values respectively.  This highlights that the lower bound is indeed zero as expected per this PR.

It appears that there are additional answer changes beyond just `fates_levleaf` in some of the longer term tests only:
```
ERS_Ld397.f45_f45_mg37.I2000Clm50Fates.derecho_intel.clm-FatesColdNoComp
SMS_Lm13.1x1_brazil.I2000Clm50FatesCruRsGs.derecho_intel.clm-FatesCold
ERS_P128x1_Ld765.f10_f10_mg37.I2000Clm60Fates.derecho_intel.clm-FatesColdNoComp
```

Results: `/glade/u/home/glemieux/scratch/ctsm-tests/tests_pr1364-fates-binidx`

author_association: CONTRIBUTOR
user: login: rgknox
created_at: 2025-04-28T13:39:42Z
body: I agree with @glemieux 's assessment. I also blocked out and reverted the changes to the trimming code and retested to the same results, since they had the most moved around. This made the change set temporarily pretty simple and there were still small but non-b4b differences on the  multi-year runs, so I think this is just an artifact of numerical noise growing from subtle changes in math. 
author_association: CONTRIBUTOR