Skip to content

Commit 760412a

Browse files
committed
Initial try (v2) at unifying nucleate_ice_cam paths in microp_aero for BAM and MAM/CARMA
1 parent 0873d11 commit 760412a

4 files changed

Lines changed: 197 additions & 107 deletions

File tree

src/chemistry/aerosol/bulk_aerosol_properties_mod.F90

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,9 @@ subroutine apply_number_limits( self, naerosol, vaerosol, istart, istop, m )
370370
integer, intent(in) :: istop ! stop column index
371371
integer, intent(in) :: m ! mode or bin index
372372

373-
call endrun('ERROR: bulk_aerosol_properties_mod%apply_number_limits not yet implemented')
373+
! no-op for bulk aerosols: no min/max number constraints since numbers are diagnosed
374+
! from bulk mass concentrations
375+
return
374376

375377
end subroutine apply_number_limits
376378

src/chemistry/aerosol/bulk_aerosol_state_mod.F90

Lines changed: 163 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,23 @@ module bulk_aerosol_state_mod
88
use physics_types, only: physics_state
99
!REMOVECAM_END
1010
use cam_abortutils, only: endrun
11+
use radiative_aerosol, only: rad_aer_get_props
12+
use string_utils, only: to_lower
1113

1214
use aerosol_state_mod, only: aerosol_state, ptr2d_t
1315
use aerosol_properties_mod, only: aerosol_properties
1416

1517
implicit none
1618

19+
private
20+
21+
! BAM sulfate scaling factor — set once at init by microp_aero_init,
22+
! read-only thereafter. Thread-safe for concurrent chunk processing.
23+
real(r8), save :: bulk_scale_mod_ = 1.0_r8
24+
25+
public :: bulk_aerosol_state
26+
public :: bulk_aerosol_state_set_bulk_scale
27+
1728
type, extends(aerosol_state) :: bulk_aerosol_state
1829
private
1930

@@ -22,6 +33,13 @@ module bulk_aerosol_state_mod
2233
type(physics_buffer_desc), pointer :: pbuf(:) => null()
2334
!REMOVECAM_END
2435

36+
! Per-object workspace for derived number mixing ratio.
37+
! Allocated in constructor, deallocated in destructor.
38+
! Pointer members can have their target data modified even
39+
! when self is intent(in) — only pointer association is protected.
40+
real(r8), pointer :: num_work_(:,:) => null() ! (horizontal_dimension, vertical_layer_dimension)
41+
real(r8), pointer :: zero_fld_(:,:) => null() ! (horizontal_dimension, vertical_layer_dimension)
42+
2543
contains
2644

2745
procedure :: get_transported
@@ -45,6 +63,8 @@ module bulk_aerosol_state_mod
4563
procedure :: wet_diameter
4664
procedure :: convcld_actfrac
4765
procedure :: wgtpct
66+
! for bit-for-bit
67+
procedure :: nuclice_get_numdens => nuclice_get_numdens_bam
4868

4969
final :: destructor
5070

@@ -59,6 +79,10 @@ module bulk_aerosol_state_mod
5979
!------------------------------------------------------------------------------
6080
!------------------------------------------------------------------------------
6181
function constructor(state,pbuf,list_idx) result(newobj)
82+
!REMOVECAM: host-model specific dimensions
83+
use ppgrid, only: pcols, pver
84+
!REMOVECAM_END
85+
6286
type(physics_state), target :: state
6387
type(physics_buffer_desc), pointer :: pbuf(:)
6488
integer, intent(in), optional :: list_idx
@@ -77,6 +101,14 @@ function constructor(state,pbuf,list_idx) result(newobj)
77101

78102
if (present(list_idx)) call newobj%set_list_idx(list_idx)
79103

104+
! Allocate per-object workspace for derived number fields.
105+
! Thread-safe: in CAM, each chunk has its own state object.
106+
allocate(newobj%num_work_(pcols, pver), stat=ierr)
107+
if (ierr /= 0) call endrun('bulk_aerosol_state constructor: num_work_ allocation error')
108+
allocate(newobj%zero_fld_(pcols, pver), stat=ierr)
109+
if (ierr /= 0) call endrun('bulk_aerosol_state constructor: zero_fld_ allocation error')
110+
newobj%zero_fld_(:,:) = 0._r8
111+
80112
end function constructor
81113

82114
!------------------------------------------------------------------------------
@@ -87,6 +119,15 @@ subroutine destructor(self)
87119
nullify(self%state)
88120
nullify(self%pbuf)
89121

122+
if (associated(self%num_work_)) then
123+
deallocate(self%num_work_)
124+
nullify(self%num_work_)
125+
end if
126+
if (associated(self%zero_fld_)) then
127+
deallocate(self%zero_fld_)
128+
nullify(self%zero_fld_)
129+
end if
130+
90131
end subroutine destructor
91132

92133
!------------------------------------------------------------------------------
@@ -154,7 +195,8 @@ subroutine get_cldbrne_mmr(self, species_ndx, bin_ndx, mmr)
154195
integer, intent(in) :: bin_ndx ! bin index
155196
real(r8), pointer :: mmr(:,:) ! mass mixing ratios (ncol,nlev)
156197

157-
call endrun('ERROR: bulk_aerosol_state_mod%get_cldbrne_mmr not yet implemented')
198+
! BAM has no cloud-borne aerosol pool — return zero array.
199+
mmr => self%zero_fld_
158200

159201
end subroutine get_cldbrne_mmr
160202

@@ -164,11 +206,28 @@ end subroutine get_cldbrne_mmr
164206
subroutine get_ambient_num(self, bin_ndx, num)
165207
class(bulk_aerosol_state), intent(in) :: self
166208
integer, intent(in) :: bin_ndx ! bin index
167-
real(r8), pointer :: num(:,:) ! number densities
209+
real(r8), pointer :: num(:,:) ! number mixing ratio (#/kg)
168210

169-
nullify(num)
211+
real(r8), pointer :: mmr(:,:)
212+
real(r8) :: ntm
213+
character(len=32) :: aname
214+
215+
! Derive number mixing ratio from mass: num = mmr * num_to_mass_aer (* bulk_scale for sulfate).
216+
! This matches the inline computation formerly in microp_aero.F90 and nucleate_ice_cam.F90.
217+
! Computed into per-object workspace (num_work_); callers must use or copy before the next call.
218+
219+
call rad_cnst_get_aer_mmr(self%list_idx_, bin_ndx, self%state, self%pbuf, mmr)
220+
call rad_aer_get_props(self%list_idx_, bin_ndx, num_to_mass_aer=ntm, aername=aname)
170221

171-
call endrun('ERROR: bulk_aerosol_state_mod%get_ambient_num not yet implemented')
222+
! Apply bulk_scale to sulfate/volcanic aerosol
223+
select case ( to_lower( aname(:4) ) )
224+
case ('sulf', 'volc') ! both treated as 'sulfate' in aero_props%get type.
225+
self%num_work_(:,:) = mmr(:,:) * ntm * bulk_scale_mod_
226+
case default
227+
self%num_work_(:,:) = mmr(:,:) * ntm
228+
end select
229+
230+
num => self%num_work_
172231

173232
end subroutine get_ambient_num
174233

@@ -180,9 +239,8 @@ subroutine get_cldbrne_num(self, bin_ndx, num)
180239
integer, intent(in) :: bin_ndx ! bin index
181240
real(r8), pointer :: num(:,:)
182241

183-
nullify(num)
184-
185-
call endrun('ERROR: bulk_aerosol_state_mod%get_cldbrne_num not yet implemented')
242+
! BAM has no cloud-borne aerosol pool — return zero array.
243+
num => self%zero_fld_
186244

187245
end subroutine get_cldbrne_num
188246

@@ -211,7 +269,9 @@ subroutine icenuc_size_wght_arr(self, bin_ndx, ncol, nlev, species_type, use_pre
211269
logical, intent(in) :: use_preexisting_ice ! pre-existing ice flag
212270
real(r8), intent(out) :: wght(:,:)
213271

214-
call endrun('ERROR: bulk_aerosol_state_mod%icenuc_size_wght_arr not yet implemented')
272+
! Empirical 1/25 scaling factor for BAM ice nucleation number densities.
273+
! This was previously hardcoded inline in nucleate_ice_cam.F90:633.
274+
wght(:ncol,:nlev) = 1._r8 / 25._r8
215275

216276
end subroutine icenuc_size_wght_arr
217277

@@ -227,7 +287,8 @@ subroutine icenuc_size_wght_val(self, bin_ndx, col_ndx, lyr_ndx, species_type, u
227287
logical, intent(in) :: use_preexisting_ice ! pre-existing ice flag
228288
real(r8), intent(out) :: wght
229289

230-
call endrun('ERROR: bulk_aerosol_state_mod%icenuc_size_wght_val not yet implemented')
290+
! Empirical 1/25 scaling factor for BAM ice nucleation number densities.
291+
wght = 1._r8 / 25._r8
231292

232293
end subroutine icenuc_size_wght_val
233294

@@ -247,7 +308,20 @@ subroutine icenuc_type_wght(self, bin_ndx, ncol, nlev, species_type, aero_props,
247308
logical, optional, intent(in) :: cloud_borne ! if TRUE cloud-borne aerosols are used
248309
! otherwise ambient aerosols are used
249310

250-
call endrun('ERROR: bulk_aerosol_state_mod%icenuc_type_wght not yet implemented')
311+
character(len=32) :: bin_spectype
312+
313+
! BAM has exactly 1 species per bin. The type weight is 1.0 when the queried
314+
! species type matches the bin's species, 0.0 otherwise. This avoids the
315+
! base class computation (which reads MMR just to compute mass/totalmass = 1.0).
316+
317+
call aero_props%species_type(bin_ndx, 1, bin_spectype)
318+
319+
if (trim(bin_spectype) == trim(species_type) .or. &
320+
(species_type == 'sulfate_strat' .and. bin_spectype == 'sulfate')) then
321+
wght(:ncol,:nlev) = 1._r8
322+
else
323+
wght(:ncol,:nlev) = 0._r8
324+
end if
251325

252326
end subroutine icenuc_type_wght
253327

@@ -264,7 +338,8 @@ subroutine update_bin( self, bin_ndx, col_ndx, lyr_ndx, delmmr_sum, delnum_sum,
264338
real(r8),intent(in) :: dtime ! time step size (sec)
265339
real(r8),intent(inout) :: tend(:,:,:) ! tendency
266340

267-
call endrun('ERROR: bulk_aerosol_state_mod%update_bin not yet implemented')
341+
! No-op for BAM: ice nucleation does not produce aerosol tendencies
342+
! (no interstitial-to-cloud-borne transfer for bulk aerosols).
268343

269344
end subroutine update_bin
270345

@@ -419,4 +494,81 @@ function wgtpct(self, ncol, nlev) result(wtp)
419494

420495
end function wgtpct
421496

497+
!------------------------------------------------------------------------------
498+
! Set the BAM sulfate scaling factor (bulk_scale namelist parameter).
499+
! Called once from microp_aero_init; read-only thereafter.
500+
!------------------------------------------------------------------------------
501+
subroutine bulk_aerosol_state_set_bulk_scale(scale)
502+
real(r8), intent(in) :: scale
503+
bulk_scale_mod_ = scale
504+
end subroutine bulk_aerosol_state_set_bulk_scale
505+
506+
! NOTE on bit-for-bit: The base-class nuclice_get_numdens computes:
507+
! size_wght * type_wght * num_col(#/kg) * rho * per_cm3
508+
! where for BAM: num_col = mmr * ntm [* bulk_scale], size_wght = 1/25, type_wght = 1.0
509+
! giving: (1/25) * 1.0 * (mmr * ntm) * rho * 1e-6
510+
!
511+
! The original inline BAM code (nucleate_ice_cam.F90, removed) computed:
512+
! naer2 = aer_mmr * rho * ntm (mmr * rho first, then * ntm)
513+
! dust_num = naer2 / 25 * 1e-6
514+
! giving: (mmr * rho * ntm) / 25 * 1e-6
515+
!
516+
! These differ only in floating-point operation order (associativity).
517+
! If this causes non-b4b results during testing, uncomment the procedure
518+
! binding in the type definition and this override to match the exact
519+
! original operation order.
520+
subroutine nuclice_get_numdens_bam(self, aero_props, use_preexisting_ice, &
521+
ncol, nlev, rho, dust_num_col, sulf_num_col, soot_num_col, sulf_num_tot_col)
522+
523+
class(bulk_aerosol_state), intent(in) :: self
524+
class(aerosol_properties), intent(in) :: aero_props
525+
logical, intent(in) :: use_preexisting_ice
526+
integer, intent(in) :: ncol
527+
integer, intent(in) :: nlev
528+
real(r8), intent(in) :: rho(:,:)
529+
real(r8), intent(out) :: dust_num_col(:,:)
530+
real(r8), intent(out) :: sulf_num_col(:,:)
531+
real(r8), intent(out) :: soot_num_col(:,:)
532+
real(r8), intent(out) :: sulf_num_tot_col(:,:)
533+
534+
real(r8), pointer :: aer_mmr(:,:)
535+
real(r8) :: ntm, maerosol, naer2
536+
character(len=32) :: spectype
537+
integer :: m, i, k
538+
real(r8), parameter :: per_cm3 = 1.e-6_r8
539+
540+
dust_num_col(:,:) = 0._r8
541+
sulf_num_col(:,:) = 0._r8
542+
soot_num_col(:,:) = 0._r8
543+
sulf_num_tot_col(:,:) = 0._r8
544+
545+
do m = 1, aero_props%nbins()
546+
call aero_props%species_type(m, 1, spectype)
547+
call self%get_ambient_mmr(species_ndx=1, bin_ndx=m, mmr=aer_mmr)
548+
call rad_aer_get_props(self%list_idx_, m, num_to_mass_aer=ntm)
549+
550+
! Exact original operation order: mmr * rho * ntm, then / 25 * per_cm3
551+
do k = 1, nlev
552+
do i = 1, ncol
553+
maerosol = aer_mmr(i,k) * rho(i,k)
554+
if (spectype == 'sulfate') then
555+
naer2 = maerosol * ntm * bulk_scale_mod_
556+
else
557+
naer2 = maerosol * ntm
558+
end if
559+
select case (trim(spectype))
560+
case ('dust')
561+
dust_num_col(i,k) = dust_num_col(i,k) + naer2 / 25._r8 * per_cm3
562+
case ('sulfate')
563+
sulf_num_col(i,k) = sulf_num_col(i,k) + naer2 / 25._r8 * per_cm3
564+
sulf_num_tot_col(i,k) = sulf_num_tot_col(i,k) + naer2 / 25._r8 * per_cm3
565+
case ('black-c')
566+
soot_num_col(i,k) = soot_num_col(i,k) + naer2 / 25._r8 * per_cm3
567+
end select
568+
end do
569+
end do
570+
end do
571+
572+
end subroutine nuclice_get_numdens_bam
573+
422574
end module bulk_aerosol_state_mod

src/physics/cam/microp_aero.F90

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ module microp_aero
4343

4444
use ndrop, only: ndrop_init, dropmixnuc
4545
use ndrop_bam, only: ndrop_bam_init, ndrop_bam_run, ndrop_bam_ccn
46+
use bulk_aerosol_state_mod, only: bulk_aerosol_state_set_bulk_scale
4647

4748
use hetfrz_classnuc_cam, only: hetfrz_classnuc_cam_readnl, hetfrz_classnuc_cam_register, hetfrz_classnuc_cam_init, &
4849
hetfrz_classnuc_cam_calc
@@ -345,6 +346,10 @@ subroutine microp_aero_init(phys_state,pbuf2d)
345346

346347
call ndrop_bam_init()
347348

349+
! Set BAM sulfate scaling factor for the aerosol state interface.
350+
! This enables bulk_aerosol_state%get_ambient_num to apply bulk_scale.
351+
call bulk_aerosol_state_set_bulk_scale(bulk_scale)
352+
348353
! Set module-level props object for BAM (used by nucleate_ice_cam)
349354
aero_props_obj => aero_props_bulk
350355

@@ -362,6 +367,8 @@ subroutine microp_aero_init(phys_state,pbuf2d)
362367
if (associated(aero_props_obj)) then
363368
call nucleate_ice_cam_init(mincld, bulk_scale, pbuf2d, aero_props=aero_props_obj)
364369
else
370+
! this path is used for aquaplanet compsets with two-moment microphysics
371+
! where nucleatei still runs Meyers nucleation deposition even without aerosol.
365372
call nucleate_ice_cam_init(mincld, bulk_scale, pbuf2d)
366373
end if
367374
if (use_hetfrz_classnuc) then

0 commit comments

Comments
 (0)