diff --git a/schemes/microp_aero/ccpp/hetfrz_classnuc_stub.F90 b/schemes/microp_aero/ccpp/hetfrz_classnuc_stub.F90
new file mode 100644
index 000000000..31ee96b14
--- /dev/null
+++ b/schemes/microp_aero/ccpp/hetfrz_classnuc_stub.F90
@@ -0,0 +1,56 @@
+! Stub for heterogeneous freezing by classical nucleation theory (hetfrz_classnuc).
+!
+! This scheme exists to read the use_hetfrz_classnuc namelist flag and expose it
+! via its CCPP standard name so downstream schemes (e.g. nucleate_ice_ccpp,
+! eventually PUMAS) can see the flag. It performs no heterogeneous-freezing
+! computation -- the real hetfrz_classnuc_ccpp is not yet ported to CAM-SIMA.
+!
+! Safe to use with use_hetfrz_classnuc=.true. for snapshot-testing flows where
+! the flag must propagate downstream; a warning is printed at init time.
+module hetfrz_classnuc_stub
+ use ccpp_kinds, only: kind_phys
+
+ implicit none
+ private
+
+ public :: hetfrz_classnuc_stub_init
+
+contains
+
+!> \section arg_table_hetfrz_classnuc_stub_init Argument Table
+!! \htmlinclude hetfrz_classnuc_stub_init.html
+ subroutine hetfrz_classnuc_stub_init( &
+ amIRoot, iulog, &
+ use_hetfrz_classnuc, &
+ errmsg, errflg)
+
+ logical, intent(in) :: amIRoot
+ integer, intent(in) :: iulog
+ logical, intent(in) :: use_hetfrz_classnuc
+
+ character(len=*), intent(out) :: errmsg
+ integer, intent(out) :: errflg
+
+ errmsg = ''
+ errflg = 0
+
+ if (amIRoot) then
+ if (use_hetfrz_classnuc) then
+ write(iulog, '(A)') 'WARNING: hetfrz_classnuc_stub: use_hetfrz_classnuc=.true., but ' // &
+ 'heterogeneous freezing by classical nucleation theory is not yet ' // &
+ 'implemented in CAM-SIMA (stub scheme).'
+ write(iulog, '(A)') 'WARNING: hetfrz_classnuc_stub: The flag is still propagated to ' // &
+ 'downstream schemes (e.g. nucleate_ice_ccpp) via its CCPP standard ' // &
+ 'name, but no hetfrz_classnuc computation will occur.'
+
+ ! this is not intended to error out for now as the flag needs to be passed to downstream schemes
+ ! (e.g., PUMAS) and modifies their behavior
+ else
+ write(iulog, '(A)') 'hetfrz_classnuc_stub: use_hetfrz_classnuc=.false. (stub scheme, ' // &
+ 'no-op).'
+ end if
+ end if
+
+ end subroutine hetfrz_classnuc_stub_init
+
+end module hetfrz_classnuc_stub
diff --git a/schemes/microp_aero/ccpp/hetfrz_classnuc_stub.meta b/schemes/microp_aero/ccpp/hetfrz_classnuc_stub.meta
new file mode 100644
index 000000000..0476324e9
--- /dev/null
+++ b/schemes/microp_aero/ccpp/hetfrz_classnuc_stub.meta
@@ -0,0 +1,37 @@
+[ccpp-table-properties]
+ name = hetfrz_classnuc_stub
+ type = scheme
+
+[ccpp-arg-table]
+ name = hetfrz_classnuc_stub_init
+ type = scheme
+[ amIRoot ]
+ standard_name = flag_for_mpi_root
+ units = flag
+ type = logical
+ dimensions = ()
+ intent = in
+[ iulog ]
+ standard_name = log_output_unit
+ units = 1
+ type = integer
+ dimensions = ()
+ intent = in
+[ use_hetfrz_classnuc ]
+ standard_name = do_heterogeneous_ice_nucleation
+ units = flag
+ type = logical
+ dimensions = ()
+ intent = in
+[ errmsg ]
+ standard_name = ccpp_error_message
+ units = none
+ type = character | kind = len=*
+ dimensions = ()
+ intent = out
+[ errflg ]
+ standard_name = ccpp_error_code
+ units = 1
+ type = integer
+ dimensions = ()
+ intent = out
diff --git a/schemes/microp_aero/ccpp/hetfrz_classnuc_stub_namelist.xml b/schemes/microp_aero/ccpp/hetfrz_classnuc_stub_namelist.xml
new file mode 100644
index 000000000..55877e15f
--- /dev/null
+++ b/schemes/microp_aero/ccpp/hetfrz_classnuc_stub_namelist.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+ logical
+ microphys
+ hetfrz_classnuc_nl
+ do_heterogeneous_ice_nucleation
+ flag
+
+ Switch to turn on heterogeneous freezing by classical nucleation theory.
+ Currently a stub in CAM-SIMA: the flag is propagated to downstream schemes
+ (nucleate_ice_ccpp, eventually PUMAS) but no hetfrz computation is performed.
+ Setting this to TRUE is supported for snapshot-testing flows where the flag
+ must match CAM; a warning is emitted at init time.
+ Default: FALSE; TRUE for CAM6 and CAM7.
+
+
+ .false.
+ .true.
+ .true.
+
+
+
+
diff --git a/schemes/pumas/micro_pumas_ccpp_dimensions_post.meta b/schemes/pumas/micro_pumas_ccpp_dimensions_post.meta
index d9df3fa60..1de7cc204 100644
--- a/schemes/pumas/micro_pumas_ccpp_dimensions_post.meta
+++ b/schemes/pumas/micro_pumas_ccpp_dimensions_post.meta
@@ -152,7 +152,7 @@
kind = pumas_r8
intent = in
[numliq_tend]
- standard_name = tendency_of_mass_number_concentration_of_cloud_liquid_water_wrt_moist_air_and_condensed_water
+ standard_name = tendency_of_mass_number_concentration_of_cloud_liquid_wrt_moist_air_and_condensed_water
long_name = mass number concentration of cloud liquid water wrt moist air and condensed water
units = kg-1 s-1
dimensions = (horizontal_loop_extent, vertical_layer_dimension)
@@ -169,7 +169,7 @@
kind = pumas_r8
intent = in
[numice_tend]
- standard_name = tendency_of_mass_number_concentration_of_cloud_ice_wrt_moist_air_and_condensed_water
+ standard_name = tendency_of_mass_number_concentration_of_ice_wrt_moist_air_and_condensed_water
long_name = mass number concentration of cloud ice wrt moist air and condensed water
units = kg-1 s-1
dimensions = (horizontal_loop_extent, vertical_layer_dimension)
diff --git a/schemes/pumas/micro_pumas_ccpp_dimensions_pre.F90 b/schemes/pumas/micro_pumas_ccpp_dimensions_pre.F90
index 76fc1bc57..c69a1aac9 100644
--- a/schemes/pumas/micro_pumas_ccpp_dimensions_pre.F90
+++ b/schemes/pumas/micro_pumas_ccpp_dimensions_pre.F90
@@ -62,8 +62,8 @@ subroutine micro_pumas_ccpp_dimensions_pre_run(ncol, nlev, nlevp1,
pdel_in, pumas_pdel, &
pint_in, pumas_pint, &
strat_cldfrc_in, pumas_strat_cldfrc, &
- strat_liq_cldfrc_in, pumas_strat_liq_cldfrc, &
- strat_ice_cldfrc_in, pumas_strat_ice_cldfrc, &
+ pumas_strat_liq_cldfrc, &
+ pumas_strat_ice_cldfrc, &
qsatfac_in, pumas_qsatfac, &
naai_in, pumas_naai, &
npccn_in, pumas_npccn, &
@@ -147,10 +147,8 @@ subroutine micro_pumas_ccpp_dimensions_pre_run(ncol, nlev, nlevp1,
real(kind_phys), intent(in) :: strat_cldfrc_in(:, :)
real(pumas_r8), intent(out) :: pumas_strat_cldfrc(:, :)
! Stratiform cloud liquid area fraction (fraction)
- real(kind_phys), intent(in) :: strat_liq_cldfrc_in(:, :)
real(pumas_r8), intent(out) :: pumas_strat_liq_cldfrc(:, :)
! Stratiform cloud ice area fraction (fraction)
- real(kind_phys), intent(in) :: strat_ice_cldfrc_in(:, :)
real(pumas_r8), intent(out) :: pumas_strat_ice_cldfrc(:, :)
! Subgrid cloud water saturation scaling factor (1)
real(kind_phys), intent(in) :: qsatfac_in(:, :)
@@ -218,8 +216,10 @@ subroutine micro_pumas_ccpp_dimensions_pre_run(ncol, nlev, nlevp1,
pumas_pdel(:ncol,:) = real(pdel_in(:,:), pumas_r8)
pumas_pint(:ncol,:) = real(pint_in(:,:micro_nlevp1), pumas_r8)
pumas_strat_cldfrc(:ncol,:) = real(strat_cldfrc_in(:,:), pumas_r8)
- pumas_strat_liq_cldfrc(:ncol,:) = real(strat_liq_cldfrc_in(:,:), pumas_r8)
- pumas_strat_ice_cldfrc(:ncol,:) = real(strat_ice_cldfrc_in(:,:), pumas_r8)
+ ! PUMAS uses total stratiform fraction for both liquid and ice stratus.
+ ! Mirrors CAM micro_pumas_cam.F90: alst_mic => ast; aist_mic => ast.
+ pumas_strat_liq_cldfrc(:ncol,:) = real(strat_cldfrc_in(:,:), pumas_r8)
+ pumas_strat_ice_cldfrc(:ncol,:) = real(strat_cldfrc_in(:,:), pumas_r8)
pumas_qsatfac(:ncol,:) = real(qsatfac_in(:,:), pumas_r8)
pumas_naai(:ncol,:) = real(naai_in(:,:), pumas_r8)
pumas_npccn(:ncol,:) = real(npccn_in(:,:), pumas_r8)
@@ -230,10 +230,7 @@ subroutine micro_pumas_ccpp_dimensions_pre_run(ncol, nlev, nlevp1,
pumas_effi_external(:ncol,:) = real(effi_external_in(:,:), pumas_r8)
pumas_frzcnt(:ncol,:) = real(frzcnt_in(:,:), pumas_r8)
pumas_frzdep(:ncol,:) = real(frzdep_in(:,:), pumas_r8)
-
- pumas_naai(:,:) = 0._pumas_r8
- pumas_npccn(:,:) = 0._pumas_r8
- pumas_qsatfac(:,:) = 0._pumas_r8
+ pumas_frzimm(:ncol,:) = real(frzimm_in(:,:), pumas_r8)
end subroutine micro_pumas_ccpp_dimensions_pre_run
diff --git a/schemes/pumas/micro_pumas_ccpp_dimensions_pre.meta b/schemes/pumas/micro_pumas_ccpp_dimensions_pre.meta
index ce95d823d..fde83b7ea 100644
--- a/schemes/pumas/micro_pumas_ccpp_dimensions_pre.meta
+++ b/schemes/pumas/micro_pumas_ccpp_dimensions_pre.meta
@@ -221,7 +221,7 @@
kind = pumas_r8
intent = out
[numliq_in]
- standard_name = mass_number_concentration_of_cloud_liquid_water_wrt_moist_air_and_condensed_water
+ standard_name = mass_number_concentration_of_cloud_liquid_wrt_moist_air_and_condensed_water
long_name = mass number concentration of cloud liquid wrt moist air and condensed water of new state
units = kg-1
dimensions = (horizontal_loop_extent, vertical_layer_dimension)
@@ -238,7 +238,7 @@
kind = pumas_r8
intent = out
[numice_in]
- standard_name = mass_number_concentration_of_cloud_ice_wrt_moist_air_and_condensed_water
+ standard_name = mass_number_concentration_of_ice_wrt_moist_air_and_condensed_water
long_name = mass number concentration of cloud ice wrt moist air and condensed water of new state
units = kg-1
dimensions = (horizontal_loop_extent, vertical_layer_dimension)
@@ -452,14 +452,6 @@
type = real
kind = pumas_r8
intent = out
-[strat_liq_cldfrc_in]
- standard_name = stratiform_cloud_liquid_area_fraction
- long_name = stratiform cloud liquid area fraction
- units = fraction
- dimensions = (horizontal_loop_extent, vertical_layer_dimension)
- type = real
- kind = kind_phys
- intent = in
[pumas_strat_liq_cldfrc]
standard_name = pumas_stratiform_cloud_liquid_area_fraction
long_name = microphysics stratiform cloud liquid area fraction
@@ -468,14 +460,6 @@
type = real
kind = pumas_r8
intent = out
-[strat_ice_cldfrc_in]
- standard_name = stratiform_cloud_ice_area_fraction
- long_name = stratiform cloud ice area fraction
- units = fraction
- dimensions = (horizontal_loop_extent, vertical_layer_dimension)
- type = real
- kind = kind_phys
- intent = in
[pumas_strat_ice_cldfrc]
standard_name = pumas_stratiform_cloud_ice_area_fraction
long_name = microphysics stratiform cloud ice area fraction
diff --git a/schemes/pumas/pumas b/schemes/pumas/pumas
index 4ba6311dd..c04b38adf 160000
--- a/schemes/pumas/pumas
+++ b/schemes/pumas/pumas
@@ -1 +1 @@
-Subproject commit 4ba6311dd46b5046c515c7316239620cba31b867
+Subproject commit c04b38adf4b8c54fe3490e3cab0be8c36053dc52
diff --git a/schemes/pumas/pumas_post_main.F90 b/schemes/pumas/pumas_post_main.F90
new file mode 100644
index 000000000..66ffd8dd4
--- /dev/null
+++ b/schemes/pumas/pumas_post_main.F90
@@ -0,0 +1,59 @@
+! Post-processing interstitial for PUMAS.
+module pumas_post_main
+ implicit none
+ private
+
+ public :: pumas_post_main_run
+
+contains
+
+ !> \section arg_table_pumas_post_main_run Argument Table
+ !! \htmlinclude pumas_post_main_run.html
+ subroutine pumas_post_main_run(ncol, cldice, numice, strat_cldfrc, effi, errmsg, errcode)
+ use ccpp_kinds, only: kind_phys
+ use pumas_kinds, only: pumas_r8=>kind_r8
+ use micro_pumas_utils, only: size_dist_param_basic, mg_ice_props, mincld, qsmall
+
+ integer, intent(in) :: ncol
+ real(kind_phys), dimension(:,:), intent(in) :: cldice ! updated cloud ice mixing ratio (kg/kg)
+ real(kind_phys), dimension(:,:), intent(in) :: numice ! updated cloud ice number concentration (kg-1)
+ real(kind_phys), dimension(:,:), intent(in) :: strat_cldfrc! total stratiform cloud area fraction (= ast)
+ real(kind_phys), dimension(:,:), intent(out) :: effi ! ice effective radius (micron)
+ character(len=512), intent(out) :: errmsg
+ integer, intent(out) :: errcode
+
+ integer :: k, nlev
+ real(pumas_r8) :: icimrst(ncol, size(cldice,2)) ! in-cloud (grid-mean) ice mixing ratio
+ real(pumas_r8) :: niic(ncol, size(cldice,2)) ! in-cloud (grid-mean) ice number conc
+ real(pumas_r8) :: rei(ncol, size(cldice,2)) ! ice slope param, then effective radius
+
+ errmsg = ' '
+ errcode = 0
+
+ nlev = size(cldice, 2)
+
+ ! Ice effective radius is recomputed here from the post-microphysics
+ ! grid-mean in-cloud ice, NOT from the raw per-substep value PUMAS returns.
+ ! PUMAS uses the total stratiform fraction for ice cloud (icecldf = ast).
+ ! Mirrors CAM micro_pumas_cam.F90:2677 (icimrst), :3174 (niic), :3171-3193 (rei).
+ icimrst(:,:) = min(real(cldice(:ncol,:), pumas_r8) / &
+ max(mincld, real(strat_cldfrc(:ncol,:), pumas_r8)), 0.005_pumas_r8)
+ niic(:,:) = real(numice(:ncol,:), pumas_r8) / &
+ max(mincld, real(strat_cldfrc(:ncol,:), pumas_r8))
+
+ rei(:,:) = 25._pumas_r8
+ do k = 1, nlev
+ call size_dist_param_basic(mg_ice_props, icimrst(:,k), niic(:,k), rei(:,k), ncol)
+ end do
+
+ where (icimrst(:,:) >= qsmall)
+ rei(:,:) = 1.5_pumas_r8 / rei(:,:) * 1.e6_pumas_r8
+ elsewhere
+ rei(:,:) = 25._pumas_r8
+ end where
+
+ effi(:ncol,:) = real(rei(:,:), kind_phys)
+
+ end subroutine pumas_post_main_run
+
+end module pumas_post_main
diff --git a/schemes/pumas/pumas_post_main.meta b/schemes/pumas/pumas_post_main.meta
new file mode 100644
index 000000000..b894787c4
--- /dev/null
+++ b/schemes/pumas/pumas_post_main.meta
@@ -0,0 +1,64 @@
+[ccpp-table-properties]
+ name = pumas_post_main
+ type = scheme
+ dependencies = pumas/micro_pumas_utils.F90
+
+[ccpp-arg-table]
+ name = pumas_post_main_run
+ type = scheme
+[ncol]
+ standard_name = horizontal_loop_extent
+ long_name = number of horizontal columns
+ units = count
+ dimensions = ()
+ type = integer
+ intent = in
+[cldice]
+ standard_name = cloud_ice_mixing_ratio_wrt_moist_air_and_condensed_water
+ long_name = cloud ice mixing ratio wrt moist air and condensed water
+ units = kg kg-1
+ dimensions = (horizontal_loop_extent, vertical_layer_dimension)
+ type = real
+ kind = kind_phys
+ intent = in
+ advected = true
+[numice]
+ standard_name = mass_number_concentration_of_ice_wrt_moist_air_and_condensed_water
+ long_name = mass number concentration of cloud ice wrt moist air and condensed water
+ units = kg-1
+ dimensions = (horizontal_loop_extent, vertical_layer_dimension)
+ type = real
+ kind = kind_phys
+ intent = in
+ advected = true
+[strat_cldfrc]
+ standard_name = stratiform_cloud_area_fraction
+ long_name = stratiform cloud area fraction
+ units = fraction
+ dimensions = (horizontal_loop_extent, vertical_layer_dimension)
+ type = real
+ kind = kind_phys
+ intent = in
+[effi]
+ standard_name = effective_radius_of_stratiform_cloud_ice_particle
+ long_name = effective radius of stratiform cloud ice particle
+ units = micron
+ dimensions = (horizontal_loop_extent, vertical_layer_dimension)
+ type = real
+ kind = kind_phys
+ intent = out
+[errmsg]
+ standard_name = ccpp_error_message
+ long_name = error message for error handling in CCPP
+ units = none
+ dimensions = ()
+ type = character
+ kind = len=512
+ intent = out
+[errcode]
+ standard_name = ccpp_error_code
+ long_name = error code for error handling in CCPP
+ units = 1
+ dimensions = ()
+ type = integer
+ intent = out
diff --git a/schemes/utilities/clamp_number_concentrations.F90 b/schemes/utilities/clamp_number_concentrations.F90
new file mode 100644
index 000000000..ffcce544d
--- /dev/null
+++ b/schemes/utilities/clamp_number_concentrations.F90
@@ -0,0 +1,167 @@
+! Clamp number concentration constituent tendencies
+! so that after tendency application, values remain within [qmin, qmax].
+!
+! This scheme must be placed immediately after apply_constituent_tendencies
+! in the SDF to replicate the same behavior as in physics_types.F90 in CAM.
+module clamp_number_concentrations
+
+ use ccpp_kinds, only: kind_phys
+
+ implicit none
+ private
+ save
+
+ public :: clamp_number_concentrations_init
+ public :: clamp_number_concentrations_run
+
+ ! Number of number-concentration species to clamp
+ integer, parameter :: num_species = 4
+
+ ! Constituent indices (-1 = not present; looked up in _init)
+ integer :: ix_numliq = -1
+ integer :: ix_numrai = -1
+ integer :: ix_numice = -1
+ integer :: ix_numsno = -1
+
+ ! Clamp bounds
+ real(kind_phys), parameter :: qmin = 1.0e-12_kind_phys ! minimum number concentration [kg-1]
+ real(kind_phys), parameter :: qmax = 1.0e10_kind_phys ! maximum number concentration [kg-1]
+
+ ! Standard names for each species (order matches ix_ variables above)
+ character(len=75), parameter :: std_names(num_species) = &
+ (/'mass_number_concentration_of_cloud_liquid_wrt_moist_air_and_condensed_water', &
+ 'mass_number_concentration_of_rain_wrt_moist_air_and_condensed_water ', &
+ 'mass_number_concentration_of_ice_wrt_moist_air_and_condensed_water ', &
+ 'mass_number_concentration_of_snow_wrt_moist_air_and_condensed_water '/)
+
+contains
+
+!> \section arg_table_clamp_number_concentrations_init Argument Table
+!! \htmlinclude clamp_number_concentrations_init.html
+ subroutine clamp_number_concentrations_init(const_props, errmsg, errflg)
+
+ use ccpp_constituent_prop_mod, only: ccpp_constituent_prop_ptr_t
+ use ccpp_const_utils, only: ccpp_const_get_idx
+
+ type(ccpp_constituent_prop_ptr_t), &
+ intent(in) :: const_props(:)
+ character(len=512), intent(out) :: errmsg
+ integer, intent(out) :: errflg
+
+ ! Local variables
+ integer :: ix_species(num_species)
+ integer :: n
+ character(len=512) :: local_errmsg
+ integer :: local_errflg
+
+ errmsg = ''
+ errflg = 0
+
+ ix_species(:) = -1
+ ! Look up constituent indices. A missing species is skipped
+ do n = 1, num_species
+ call ccpp_const_get_idx(const_props, trim(std_names(n)), &
+ ix_species(n), local_errmsg, local_errflg)
+ if (local_errflg /= 0) then
+ ! Constituent not found — mark as inactive, reset error
+ ix_species(n) = -1
+ local_errflg = 0
+ local_errmsg = ''
+ end if
+ end do
+
+ ! Store in module variables
+ ix_numliq = ix_species(1)
+ ix_numrai = ix_species(2)
+ ix_numice = ix_species(3)
+ ix_numsno = ix_species(4)
+
+ end subroutine clamp_number_concentrations_init
+
+!> \section arg_table_clamp_number_concentrations_run Argument Table
+!! \htmlinclude clamp_number_concentrations_run.html
+ subroutine clamp_number_concentrations_run( &
+ ncol, pver, dt, &
+ const_q, const_tend, &
+ errmsg, errflg)
+
+ integer, intent(in) :: ncol
+ integer, intent(in) :: pver
+ real(kind_phys), intent(in) :: dt ! physics timestep [s]
+ real(kind_phys), intent(inout) :: const_q(:, :, :) ! constituent mixing ratios
+ real(kind_phys), intent(inout) :: const_tend(:, :, :) ! constituent tendencies
+ character(len=512), intent(out) :: errmsg
+ integer, intent(out) :: errflg
+
+ ! Local variables
+ integer :: ix_species(num_species)
+ integer :: n, i, k, ix
+ real(kind_phys) :: q_projected ! projected value after tendency application [kg-1]
+
+ errmsg = ''
+ errflg = 0
+
+ ix_species = (/ix_numliq, ix_numrai, ix_numice, ix_numsno/)
+
+ do n = 1, num_species
+ ix = ix_species(n)
+ if (ix <= 0) cycle
+
+ do k = 1, pver
+ do i = 1, ncol
+ if (const_q(i, k, ix) < qmin) then
+ const_q(i, k, ix) = qmin
+ const_tend(i, k, ix) = 0.0_kind_phys
+ else if (const_q(i, k, ix) > qmax) then
+ const_q(i, k, ix) = qmax
+ const_tend(i, k, ix) = 0.0_kind_phys
+ end if
+ end do
+ end do
+ end do
+
+ ! The below alternative is a formulation based on the principle that we do not alter
+ ! the constituent tendencies themselves.
+ !
+ ! However, the (qmin - q) / dt will cause floating point errors and introduce
+ ! more harm than good (this is unfortunately because qmin and q may often have
+ ! a large difference)
+ !
+ ! e.g.,
+ ! (gdb) p const_q(356,23,1)
+ ! $15 = 1826.1689801273931 <-- q >> qmin.
+ ! (gdb) p const_tend(356,23,1)
+ ! $16 = -1.0145383222929962
+ ! (gdb) p dt
+ ! $17 = 1800
+ ! (gdb) p const_tend(356,23,1)*dt
+ ! $18 = -1826.1689801273933
+ ! (gdb) p const_q(356,23,1)+const_tend(356,23,1)*dt
+ ! $19 = -2.2737367544323206e-13 <-- projected fall below qmin
+ ! which triggers the clamp
+ ! yet because q >> qmin, q + (qmin - q)/dt*dt will be far from qmin
+ ! introducing numerical noise. (hplin, 3/18/26)
+ !
+ ! do n = 1, num_species
+ ! ix = ix_species(n)
+ ! if (ix <= 0) cycle
+
+ ! do k = 1, pver
+ ! do i = 1, ncol
+ ! ! Project what the value would be after tendency application
+ ! q_projected = const_q(i, k, ix) + const_tend(i, k, ix) * dt
+
+ ! if (q_projected < qmin) then
+ ! ! Rewrite tendency so the updater lands exactly on qmin
+ ! const_tend(i, k, ix) = (qmin - const_q(i, k, ix)) / dt
+ ! else if (q_projected > qmax) then
+ ! ! Rewrite tendency so the updater lands exactly on qmax
+ ! const_tend(i, k, ix) = (qmax - const_q(i, k, ix)) / dt
+ ! end if
+ ! end do
+ ! end do
+ ! end do
+
+ end subroutine clamp_number_concentrations_run
+
+end module clamp_number_concentrations
diff --git a/schemes/utilities/clamp_number_concentrations.meta b/schemes/utilities/clamp_number_concentrations.meta
new file mode 100644
index 000000000..33eb91a9e
--- /dev/null
+++ b/schemes/utilities/clamp_number_concentrations.meta
@@ -0,0 +1,71 @@
+[ccpp-table-properties]
+ name = clamp_number_concentrations
+ type = scheme
+
+[ccpp-arg-table]
+ name = clamp_number_concentrations_init
+ type = scheme
+[ const_props ]
+ standard_name = ccpp_constituent_properties
+ units = none
+ type = ccpp_constituent_prop_ptr_t
+ dimensions = (number_of_ccpp_constituents)
+ intent = in
+[ errmsg ]
+ standard_name = ccpp_error_message
+ units = none
+ type = character | kind = len=512
+ dimensions = ()
+ intent = out
+[ errflg ]
+ standard_name = ccpp_error_code
+ units = 1
+ type = integer
+ dimensions = ()
+ intent = out
+
+[ccpp-arg-table]
+ name = clamp_number_concentrations_run
+ type = scheme
+[ ncol ]
+ standard_name = horizontal_loop_extent
+ units = count
+ type = integer
+ dimensions = ()
+ intent = in
+[ pver ]
+ standard_name = vertical_layer_dimension
+ units = count
+ type = integer
+ dimensions = ()
+ intent = in
+[ dt ]
+ standard_name = timestep_for_physics
+ units = s
+ type = real | kind = kind_phys
+ dimensions = ()
+ intent = in
+[ const_q ]
+ standard_name = ccpp_constituents
+ units = none
+ type = real | kind = kind_phys
+ dimensions = (horizontal_loop_extent, vertical_layer_dimension, number_of_ccpp_constituents)
+ intent = inout
+[ const_tend ]
+ standard_name = ccpp_constituent_tendencies
+ units = none
+ type = real | kind = kind_phys
+ dimensions = (horizontal_loop_extent, vertical_layer_dimension, number_of_ccpp_constituents)
+ intent = inout
+[ errmsg ]
+ standard_name = ccpp_error_message
+ units = none
+ type = character | kind = len=512
+ dimensions = ()
+ intent = out
+[ errflg ]
+ standard_name = ccpp_error_code
+ units = 1
+ type = integer
+ dimensions = ()
+ intent = out
diff --git a/test/test_suites/suite_pumas.xml b/test/test_suites/suite_pumas.xml
index 680ba9523..6638885f1 100644
--- a/test/test_suites/suite_pumas.xml
+++ b/test/test_suites/suite_pumas.xml
@@ -6,6 +6,7 @@
initialize_constituents
to_be_ccppized_temporary
clubb_stub
+ hetfrz_classnuc_stub
pumas_pre_main
@@ -17,7 +18,11 @@
micro_pumas_ccpp_dimensions_post
apply_constituent_tendencies
- apply_heating_rate
clamp_number_concentrations
+ apply_heating_rate
+
+ geopotential_temp
+
+ pumas_post_main