@@ -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+
422574end module bulk_aerosol_state_mod
0 commit comments