diff --git a/bld/CLMBuildNamelist.pm b/bld/CLMBuildNamelist.pm
index fb44023cd5..019fa281d6 100755
--- a/bld/CLMBuildNamelist.pm
+++ b/bld/CLMBuildNamelist.pm
@@ -1581,6 +1581,7 @@ sub process_namelist_inline_logic {
setup_logic_luna($opts, $nl_flags, $definition, $defaults, $nl, $physv);
setup_logic_hillslope($opts, $nl_flags, $definition, $defaults, $nl);
setup_logic_o3_veg_stress_method($opts, $nl_flags, $definition, $defaults, $nl,$physv);
+ setup_logic_do3_streams($opts, $nl_flags, $definition, $defaults, $nl, $physv);
setup_logic_hydrstress($opts, $nl_flags, $definition, $defaults, $nl);
setup_logic_dynamic_roots($opts, $nl_flags, $definition, $defaults, $nl, $physv);
setup_logic_params_file($opts, $nl_flags, $definition, $defaults, $nl);
@@ -1718,6 +1719,11 @@ sub process_namelist_inline_logic {
##################################
setup_logic_cropcal_streams($opts, $nl_flags, $definition, $defaults, $nl);
+ ##################################
+ # namelist group: dO3_streams #
+ ##################################
+ setup_logic_do3_streams($opts, $nl_flags, $definition, $defaults, $nl);
+
##########################################
# namelist group: soil_moisture_streams #
##########################################
@@ -4223,6 +4229,31 @@ sub setup_logic_cropcal_streams {
#-------------------------------------------------------------------------------
+sub setup_logic_do3_streams {
+ # input file for creating diurnal ozone from >daily data
+ my ($opts, $nl_flags, $definition, $defaults, $nl, $physv) = @_;
+
+ add_default($opts, $nl_flags->{'inputdata_rootdir'}, $definition, $defaults, $nl, 'use_do3_streams');
+ if ( &value_is_true( $nl->get_value('use_do3_streams') ) ) {
+ if ($opts->{'driver'} ne "nuopc") {
+ $log->fatal_error("Cannot use do3_streams=.true. with MCT.");
+ }
+ add_default($opts, $nl_flags->{'inputdata_rootdir'}, $definition, $defaults, $nl, 'stream_fldfilename_do3',
+ 'hgrid'=>"360x720cru" );
+ add_default($opts, $nl_flags->{'inputdata_rootdir'}, $definition, $defaults, $nl, 'stream_meshfile_do3',
+ 'hgrid'=>"360x720cru" );
+ add_default($opts, $nl_flags->{'inputdata_rootdir'}, $definition, $defaults, $nl, 'do3_mapalgo',
+ 'hgrid'=>$nl_flags->{'res'} );
+ } else {
+ if ( defined($nl->get_value('stream_fldfilename_do3'))) {
+ $log->fatal_error("One of the do3 streams namelist items (stream_fldfilename_do3, " .
+ " is defined, but use_do3_streams option set to false");
+ }
+ }
+}
+
+#-------------------------------------------------------------------------------
+
sub setup_logic_soilwater_movement {
my ($opts, $nl_flags, $definition, $defaults, $nl) = @_;
@@ -4703,7 +4734,7 @@ sub write_output_files {
soil_resis_inparm bgc_shared canopyfluxes_inparm aerosol
clmu_inparm clm_soilstate_inparm clm_nitrogen clm_snowhydrology_inparm hillslope_hydrology_inparm hillslope_properties_inparm
cnprecision_inparm clm_glacier_behavior crop_inparm irrigation_inparm
- surfacealbedo_inparm water_tracers_inparm tillage_inparm);
+ surfacealbedo_inparm water_tracers_inparm tillage_inparm do3_streams);
#@groups = qw(clm_inparm clm_canopyhydrology_inparm clm_soilhydrology_inparm
# finidat_consistency_checks dynpft_consistency_checks);
diff --git a/bld/namelist_files/namelist_defaults_ctsm.xml b/bld/namelist_files/namelist_defaults_ctsm.xml
index cc407961ff..29e1c56606 100644
--- a/bld/namelist_files/namelist_defaults_ctsm.xml
+++ b/bld/namelist_files/namelist_defaults_ctsm.xml
@@ -1732,6 +1732,12 @@ lnd/clm2/surfdata_esmf/NEON/surfdata_1x1_NEON_TOOL_hist_78pfts_CMIP6_simyr2000_c
nn
nn
+
+.true.
+/glade/work/afoster/ozone_update/ozone_damage_files/converted/diurnal_factor_O3surface_ssp370_2015-2024_c20220502.nc
+share/meshes/fv0.9x1.25_141008_polemod_ESMFmesh.nc
+bilinear
+
1850
2100
diff --git a/bld/namelist_files/namelist_definition_ctsm.xml b/bld/namelist_files/namelist_definition_ctsm.xml
index 37c457141c..e4bc5f1343 100644
--- a/bld/namelist_files/namelist_definition_ctsm.xml
+++ b/bld/namelist_files/namelist_definition_ctsm.xml
@@ -1768,6 +1768,38 @@ Mapping method from LAI input file to the model resolution
copy = copy using the same indices
+
+
+
+
+
+Toggle to turn on reading in of diurnal ozone anomaly file. Used to convert >daily ozone to sub-daily
+
+
+
+Filename of input stream data for diurnal ozone anomaly
+
+
+
+Filename of mesh file for diurnal ozone anomaly file
+
+
+
+Mapping method from diurnal ozone input file to the model resolution
+ bilinear = bilinear interpolation
+ nn = nearest neighbor
+ nnoni = nearest neighbor on the "i" (longitude) axis
+ nnonj = nearest neighbor on the "j" (latitude) axis
+ spval = set to special value
+ copy = copy using the same indices
+
+
+
+
diff --git a/doc/design/design_doc_diurnal_ozone.md b/doc/design/design_doc_diurnal_ozone.md
new file mode 100644
index 0000000000..e340c4f062
--- /dev/null
+++ b/doc/design/design_doc_diurnal_ozone.md
@@ -0,0 +1,116 @@
+# Software Design Documentation
+
+Project Name
+
+
+**Date:**: 09/27/22
+
+**Written By**: Adrianna Foster (@adrifoster)
+
+## Introduction
+
+---------------------------------------
+
+As laid out in CTSM Issue [#270](https://github.com/ESCOMP/CTSM/issues/270) we want to downscale input ozone partial pressure (mol/mol) temporally if we receive a multi-day average input ozone (usually in DATM mode).
+
+We have the infrastructure laid out for CAM and DATM to inform CTSM which type of input we are receiving (i.e., 'multiday_average' or 'subdaily'). We should only apply this downscaling when receiving multiday average ozone.
+
+We have a gridded file provided by Louisa Emmons for a diurnal ozone variation factor. The data is additionally dimensioned by 'secs' (seconds of day), and depending on the model time of day, can be used as a multiplicative factor for converting multi-day average ozone to sub-daily ozone partial pressure.
+
+## Solutions
+
+---------------------------------------
+
+Read in the diurnal anomaly file as a streams file, being careful to not assume too much so that other files may be provided in the future. Create a diurnal ozone type that has an `Interp` method to downscale and interpolate multi-day average ozone data to sub-daily based on a factor attribute. Inside the existing `CalcOzoneUptake` routine, if we are using multi-day average ozone, interpolate/downscale the `forc_o3` array using the `Interp` subroutine.
+
+**Some alternate ideas**:
+
+1. have the existing `ozone_type` type have this diurnal ozone anomaly type as an attribute that gets initiated only if we are reading in multi-day average ozone.
+2. have the diurnal ozone type be fed into relevant `ozone_type` methods as arguments. We would only want to do this if other modules are going to use the diurnal ozone type. This has a possibility, but no clear plans are on the horizon for it.
+
+Consider going with the first solution for now.
+
+## Design Considerations
+
+---------------------------------------
+
+### Assumptions and Dependencies
+
+Right now we are assuming that the units of the input diurnal file are going to be in seconds, and that the file covers the whole day.
+
+There is no need to assume that the dimension will be 1-24 (i.e. on the hour) or that the dimensions be equal intervals.
+
+
+## Design and Architecture
+
+---------------------------------------
+
+### System diagram or flowchart
+
+#### Diurnal Factor Streams File
+
+We need to read in the diurnal ozone factor file provided by Louisa Emmons as a streams file. Some modifications are required on the file so that it can be accurately read and parsed by ESMF and to facilitate our chosen implementation strategy:
+
+1. Add a new 'time' variable (even though the data is not dimensioned by time, except for seconds of day) so that ESMF can parse it. The date is arbitrarily set to 2000-01-01, and the dimensions are set to 'UNLIMITED'.
+
+2. Shift the 'secs' array up 1800 seconds so that it provides the midpoint of the averaged time interval, rather than the beginning. This conversion is done to facilitate interpolation between the time-of-day dimensions.
+
+These file modifications can be seen in the Jupyter notebook /glade/u/home/afoster/Diurnal_ozone.ipynb.
+
+The file is read in using a new `src/share_esmf/diurnalOzoneStreamMod` module, with associated updates to `bld/CLMBuildNamelist.pm`, `bld/namelist_files/namelist_defaults_ctsm.xml` and `bld/namelist_files/namelist_definition_csvm.xml`
+
+Right now the new variable `use_do3_streams` is set in the `user_nl_clm` file. And the other namelist variables (`stream_fldfilename_do3`, `stream_meshfile_do3`, and `do3_mapalgo`) are inside the `lnd_in` file under a `do3_streams` namelist.
+
+**I'm not sure setting up the `use_do3_streams` namelist variable is the best way to go about this, since we essentially want this feature on whenever the ozone frequency is 'multiday_average'**
+
+Right now we are hard-coding the `stream_var_name`, and `stream_lev_dimname`. I think this is okay. Otherwise, we would want them to be namelist variables that get read in.
+
+Because the diurnal anomaly file is not dimensioned by time other than seconds of day, we just need to read it in and advance one time. This is all currently done in the `diurnalOzoneStreamMod`'s `read_O3_stream` subroutine. We set the date to an arbitrary date (the same as on the file, I'm not sure if this is necessary?).
+
+We also need to grab the `secs` array to do interpolation/downscaling with.
+
+Both are read from the file and then an instance of `diurnal_ozone_anom_type` is initialized and relevant arrays are set to the ozone factor and seconds data.
+
+### Diurnal Ozone Anomaly Type
+
+The module `DiurnalOzoneType` sets up a `diurnal_ozone_anom_type` which has only a few attributes and methods:
+
+**Attributes**:
+
+1. `ntimes` - size of time/seconds-of-day dimension (private)
+2. `o3_anomaly_grc` - o3 anomaly data, 2d, gridcells x ntimes
+3. `time_arr` - time/seconds of day array, 1d, ntimes
+
+**Methods**:
+
+1. `Init` - Initializes the anomaly data structures, calls `InitAllocate`
+2. `InitAllocate` - allocates arrays and sets them to nan
+3. `Interp` - Interpolates/downscales an input multi-day average ozone (`forc_o3`) using the o3 anomaly data and outputs a downscaled `forc_o3_down` array. See below for `Interp` algorithm/pseudo code
+
+### Implementation within OzoneMod
+
+We add an instance of the `diurnal_ozone_anom_type` as an attribute of the `ozone_base_type`.
+
+Within the `Init` method of the `ozone_base_type`, we read the `drv_flds_in` file to get the `atom_ozone_frequency_val`. If this value is `atm_ozone_frequency_multiday_average` then we initialize and read in the o3 anomaly data by calling the `read_O3_stream` described above. *Do we need an else decision here?*
+
+We also add an integer `atm_ozone_freq` as a new attribute, and set it in `Init`.
+
+Within the `CalcOzoneUptake` method, we check for the `atm_ozone_freq` flag and if it is `atm_ozone_frequency_multiday_average` we call the `Interp` method.
+
+### Algorithm and Pseudo code for Interp
+
+
+
+## Rollout Plan
+
+---------------------------------------
+Define the roll-out phases and tests you plan to do
+
+
+## Review Sign-off
+
+---------------------------------------
+
+* Reviewer(s):
+
+* Sign-off Completed on YYYY-MM-DD*
diff --git a/src/biogeophys/DiurnalOzoneType.F90 b/src/biogeophys/DiurnalOzoneType.F90
new file mode 100644
index 0000000000..bb8a327817
--- /dev/null
+++ b/src/biogeophys/DiurnalOzoneType.F90
@@ -0,0 +1,156 @@
+module DiurnalOzoneType
+
+ !-----------------------------------------------------------------------
+ ! !DESCRIPTION:
+ ! Sets up type for converting multi-day ozone input to sub-daily values using an input
+ ! ozone anomaly file
+ !
+ ! !USES:
+#include "shr_assert.h"
+ use shr_kind_mod , only : r8 => shr_kind_r8
+ use decompMod , only : bounds_type
+ use clm_varcon , only : spval
+ use clm_varctl , only : iulog
+ use abortutils , only : endrun
+
+ implicit none
+ save
+ private
+
+ ! !PUBLIC TYPES:
+ type, public :: diurnal_ozone_anom_type
+ private
+ ! Private data members
+ integer :: ntimes ! size of time dimension
+ real(r8), public, allocatable :: o3_anomaly_grc(:,:) ! o3 anomaly data [grc, ntimes]
+ real(r8), public, allocatable :: time_arr(:) ! time dimension (units = seconds of day)
+
+ contains
+ ! Public routines
+ procedure, public :: Init
+ procedure, private :: InitAllocate
+ procedure, public :: Interp
+
+ end type diurnal_ozone_anom_type
+
+ character(len=*), parameter, private :: sourcefile = &
+ __FILE__
+
+ contains
+
+ ! ========================================================================
+ ! Infrastructure routines (initialization, etc.)
+ ! ========================================================================
+
+ !-----------------------------------------------------------------------
+ subroutine Init(this, bounds, n)
+ !
+ ! DESCRIPTION:
+ ! Initialize ozone anomaly data structures
+ !
+ !
+ ! ARGUMENTS:
+ class(diurnal_ozone_anom_type), intent(inout) :: this
+ type(bounds_type), intent(in) :: bounds
+ integer, intent(in) :: n
+ !-----------------------------------------------------------------------
+
+ ! set ntimes from input
+ this%ntimes = n
+
+ ! allocate arrays
+ call this%InitAllocate(bounds)
+
+ end subroutine Init
+
+ !-----------------------------------------------------------------------
+ subroutine InitAllocate(this, bounds)
+ !
+ ! !DESCRIPTION:
+ ! Allocate module variables and data structures
+ !
+ ! !USES:
+ use shr_infnan_mod, only: nan => shr_infnan_nan, assignment(=)
+ !
+ ! !ARGUMENTS:
+ class(diurnal_ozone_anom_type), intent(inout) :: this
+ type(bounds_type), intent(in) :: bounds
+ !
+ ! LOCAL VARIABLES:
+ integer :: begg, endg
+ !---------------------------------------------------------------------
+
+ begg = bounds%begg; endg = bounds%endg
+
+ allocate(this%o3_anomaly_grc(begg:endg,1:this%ntimes)); this%o3_anomaly_grc(:,:) = nan
+ allocate(this%time_arr(1:this%ntimes)); this%time_arr(:) = nan
+
+ end subroutine InitAllocate
+
+ !-----------------------------------------------------------------------
+
+ subroutine Interp(this, bounds, forc_o3, forc_o3_down)
+ !
+ ! !DESCRIPTION:
+ ! Downscale/interpolate multi-day ozone data to subdaily
+ !
+ ! !USES:
+ use clm_time_manager , only : get_curr_date
+ use clm_varcon , only : secspday
+ !
+ ! !ARGUMENTS:
+ class(diurnal_ozone_anom_type), intent(in) :: this
+ type(bounds_type), intent(in) :: bounds ! bounds type
+ real(r8), intent(in) :: forc_o3(:) ! ozone partial pressure (mol/mol)
+ real(r8), intent(out) :: forc_o3_down(:) ! ozone partial pressure, downscaled (mol/mol)
+
+ !
+ ! LOCAL VARIABLES:
+ integer :: t ! looping index
+ integer :: yr ! year
+ integer :: mon ! month
+ integer :: day ! day of month
+ integer :: tod ! time of day (seconds past 0Z)
+ integer :: begg, endg ! bounds
+ integer :: t_prev ! previous time index
+ real(r8) :: tdiff_end
+ real(r8) :: tdiff_start
+ real(r8) :: tdiff
+ !-----------------------------------------------------------------------
+
+ begg = bounds%begg; endg = bounds%endg
+
+ ! Get current date/time - we really only need seconds
+ call get_curr_date(yr, mon, day, tod)
+
+ ! find the time interval we are in
+ do t = 1, this%ntimes
+ if (real(tod) <= this%time_arr(t)) then
+ exit
+ end if
+ end do
+
+ ! interpolate, checking for edge cases
+ if (t == 1) then
+ ! wrap around back
+ t_prev = this%ntimes
+ tdiff_end = secspday - this%time_arr(t_prev) + real(tod)
+ tdiff = this%time_arr(t) + secspday - this%time_arr(t_prev)
+ else
+ t_prev = t - 1
+ tdiff_end = real(tod) - this%time_arr(t_prev)
+ tdiff = this%time_arr(t) - this%time_arr(t_prev)
+ end if
+
+ tdiff_start = this%time_arr(t) - real(tod)
+
+ ! interpolate
+ forc_o3_down(begg:endg) = forc_o3(begg:endg)* &
+ ((this%o3_anomaly_grc(begg:endg, t_prev)*tdiff_start + &
+ this%o3_anomaly_grc(begg:endg, t_prev)*tdiff_end)/tdiff)
+
+ end subroutine Interp
+
+ !-----------------------------------------------------------------------
+
+ end module DiurnalOzoneType
diff --git a/src/biogeophys/OzoneMod.F90 b/src/biogeophys/OzoneMod.F90
index 61efdfcde0..6d2630d4ef 100644
--- a/src/biogeophys/OzoneMod.F90
+++ b/src/biogeophys/OzoneMod.F90
@@ -15,14 +15,18 @@ module OzoneMod
!
! !USES:
#include "shr_assert.h"
- use shr_kind_mod, only : r8 => shr_kind_r8
- use decompMod , only : bounds_type
- use clm_varcon , only : spval
- use clm_varctl , only : iulog
- use OzoneBaseMod, only : ozone_base_type
- use abortutils , only : endrun
- use PatchType , only : patch
- use pftconMod , only : pftcon
+ use shr_kind_mod , only : r8 => shr_kind_r8
+ use decompMod , only : bounds_type
+ use clm_varcon , only : spval
+ use clm_varctl , only : iulog
+ use OzoneBaseMod , only : ozone_base_type
+ use abortutils , only : endrun
+ use PatchType , only : patch
+ use pftconMod , only : pftcon
+ use shr_ozone_coupling_mod , only : atm_ozone_frequency_multiday_average, shr_ozone_coupling_readnl
+ use DiurnalOzoneType , only : diurnal_ozone_anom_type
+ use diurnalOzoneStreamMod , only : read_O3_stream
+ use controlMod , only : use_do3_streams
implicit none
save
@@ -33,9 +37,11 @@ module OzoneMod
private
! Private data members
integer :: stress_method ! Which ozone stress parameterization we're using in this run
+ integer :: atm_ozone_freq ! Which ozone input frequency are we receiving?
real(r8), pointer :: o3uptakesha_patch(:) ! ozone dose, shaded leaves (mmol O3/m^2)
real(r8), pointer :: o3uptakesun_patch(:) ! ozone dose, sunlit leaves (mmol O3/m^2)
+ real(r8), pointer :: o3force_grid(:) ! ozone forcing (mol/mol)
! NOTE(wjs, 2014-09-29) tlai_old_patch really belongs alongside tlai_patch in
! CanopyStateType. But there are problems with any way I can think to implement
@@ -53,9 +59,10 @@ module OzoneMod
! Then the setter method would also set tlai_old. This feels like the most robust
! solution, but we don't have any precedent for using getters and setters for data
! arrays.
- real(r8), pointer :: tlai_old_patch(:) ! tlai from last time step
+ real(r8), pointer :: tlai_old_patch(:) ! tlai from last time step
+ type(diurnal_ozone_anom_type) :: diurnalOzoneAnomInst
- contains
+ contains
! Public routines
procedure, public :: Init
procedure, public :: Restart
@@ -139,14 +146,28 @@ subroutine Init(this, bounds, o3_veg_stress_method)
! Initialize ozone data structure
!
! !USES:
- use clm_varctl , only : use_luna
+ use clm_varctl , only : use_luna
!
! !ARGUMENTS:
class(ozone_type), intent(inout) :: this
type(bounds_type), intent(in) :: bounds
character(len=*), intent(in) :: o3_veg_stress_method
+
+ ! Local variables
+ integer :: atm_ozone_frequency_val
!-----------------------------------------------------------------------
+ ! read what atm ozone frequency we have
+ call shr_ozone_coupling_readnl("drv_flds_in", atm_ozone_frequency_val)
+ this%atm_ozone_freq = atm_ozone_frequency_val
+
+ ! if we have multi-day average input ozone, we need to convert to sub-daily using
+ ! an input anomaly file
+ if (this%atm_ozone_freq == atm_ozone_frequency_multiday_average .and. use_do3_streams) then
+ ! initialize and read in data for diurnal O3 anomaly stream
+ call read_O3_stream(this%diurnalOzoneAnomInst, bounds)
+ end if
+
if (o3_veg_stress_method=='stress_lombardozzi2015') then
this%stress_method = stress_method_lombardozzi2015
else if (o3_veg_stress_method=='stress_falk') then
@@ -178,16 +199,20 @@ subroutine InitAllocate(this, bounds)
!
! !LOCAL VARIABLES:
integer :: begp, endp
+ integer :: begg, endg
!-----------------------------------------------------------------------
begp = bounds%begp
endp = bounds%endp
+ begg = bounds%begg
+ endg = bounds%endg
call this%InitAllocateBase(bounds)
allocate(this%o3uptakesha_patch(begp:endp)) ; this%o3uptakesha_patch(:) = nan
allocate(this%o3uptakesun_patch(begp:endp)) ; this%o3uptakesun_patch(:) = nan
allocate(this%tlai_old_patch(begp:endp)) ; this%tlai_old_patch(:) = nan
+ allocate(this%o3force_grid(begg:endg)) ; this%o3force_grid(:) = nan
end subroutine InitAllocate
@@ -206,12 +231,15 @@ subroutine InitHistory(this, bounds)
!
! !LOCAL VARIABLES:
integer :: begp, endp
+ integer :: begg, endg
character(len=*), parameter :: subname = 'InitHistory'
!-----------------------------------------------------------------------
begp = bounds%begp
endp = bounds%endp
+ begg = bounds%begg
+ endg = bounds%endg
this%o3uptakesun_patch(begp:endp) = spval
call hist_addfld1d (fname='O3UPTAKESUN', units='mmol/m^2', &
@@ -223,6 +251,11 @@ subroutine InitHistory(this, bounds)
avgflag='A', long_name='total ozone flux into shaded leaves', &
ptr_patch=this%o3uptakesha_patch)
+ this%o3force_grid(begg:endg) = spval
+ call hist_addfld1d (fname='FORCE_O3', units='mol/mol', &
+ avgflag='A', long_name='ozone partial pressure', &
+ ptr_lnd=this%o3force_grid)
+
if (this%stress_method==stress_method_lombardozzi2015) then
! For this and the following variables: how should we include leaf area in the
! patch averaging?
@@ -294,18 +327,22 @@ subroutine InitCold(this, bounds)
!
! !LOCAL VARIABLES:
integer :: begp, endp
+ integer :: begg, endg
character(len=*), parameter :: subname = 'InitCold'
!-----------------------------------------------------------------------
begp = bounds%begp
endp = bounds%endp
+ begg = bounds%begg
+ endg = bounds%endg
call this%InitColdBase(bounds)
this%o3uptakesha_patch(begp:endp) = 0._r8
this%o3uptakesun_patch(begp:endp) = 0._r8
this%tlai_old_patch(begp:endp) = 0._r8
+ this%o3force_grid(begg:endg) = 0._r8
end subroutine InitCold
@@ -346,6 +383,11 @@ subroutine Restart(this, bounds, ncid, flag)
long_name='ozone uptake for sunlit leaves', units='mmol m^-3', &
readvar=readvar, interpinic_flag='interp', data=this%o3uptakesun_patch)
+ call restartvar(ncid=ncid, flag=flag, varname='o3force', xtype=ncd_double, &
+ dim1name='gridcell', &
+ long_name='ozone forcing', units='mol mol^-1', &
+ readvar=readvar, interpinic_flag='interp', data=this%o3force_grid)
+
end subroutine Restart
! ========================================================================
@@ -374,10 +416,11 @@ subroutine CalcOzoneUptake(this, bounds, num_exposedvegp, filter_exposedvegp, &
real(r8) , intent(in) :: forc_o3( bounds%begg: ) ! ozone partial pressure (mol/mol)
!
! !LOCAL VARIABLES:
- integer :: fp ! filter index
- integer :: p ! patch index
- integer :: c ! column index
- integer :: g ! gridcell index
+ real(r8) :: forc_o3_down(bounds%begg:bounds%endg) ! downscaled ozone partial pressure (mol/mol)
+ integer :: fp ! filter index
+ integer :: p ! patch index
+ integer :: c ! column index
+ integer :: g ! gridcell index
character(len=*), parameter :: subname = 'CalcOzoneUptake'
!-----------------------------------------------------------------------
@@ -398,28 +441,36 @@ subroutine CalcOzoneUptake(this, bounds, num_exposedvegp, filter_exposedvegp, &
tlai_old => this%tlai_old_patch & ! Output: [real(r8) (:)] tlai from last time step
)
- do fp = 1, num_exposedvegp
- p = filter_exposedvegp(fp)
- c = patch%column(p)
- g = patch%gridcell(p)
-
- ! Ozone uptake for shaded leaves
- call CalcOzoneUptakeOnePoint( &
- forc_ozone=forc_o3(g), forc_pbot=forc_pbot(c), forc_th=forc_th(c), &
- rs=rssha(p), rb=rb(p), ram=ram(p), &
- tlai=tlai(p), tlai_old=tlai_old(p), pft_type=patch%itype(p), &
- o3uptake=o3uptakesha(p))
-
- ! Ozone uptake for sunlit leaves
- call CalcOzoneUptakeOnePoint( &
- forc_ozone=forc_o3(g), forc_pbot=forc_pbot(c), forc_th=forc_th(c), &
- rs=rssun(p), rb=rb(p), ram=ram(p), &
- tlai=tlai(p), tlai_old=tlai_old(p), pft_type=patch%itype(p), &
- o3uptake=o3uptakesun(p))
-
- tlai_old(p) = tlai(p)
-
- end do
+ if (this%atm_ozone_freq == atm_ozone_frequency_multiday_average .and. use_do3_streams) then
+ call this%diurnalOzoneAnomInst%Interp(bounds, forc_o3, forc_o3_down)
+ else
+ forc_o3_down(bounds%begg:bounds%endg) = forc_o3(bounds%begg:bounds%endg)
+ end if
+
+ this%o3force_grid(bounds%begg:bounds%endg) = forc_o3_down(bounds%begg:bounds%endg)
+
+ do fp = 1, num_exposedvegp
+ p = filter_exposedvegp(fp)
+ c = patch%column(p)
+ g = patch%gridcell(p)
+
+ ! Ozone uptake for shaded leaves
+ call CalcOzoneUptakeOnePoint( &
+ forc_ozone=forc_o3_down(g), forc_pbot=forc_pbot(c), forc_th=forc_th(c), &
+ rs=rssha(p), rb=rb(p), ram=ram(p), &
+ tlai=tlai(p), tlai_old=tlai_old(p), pft_type=patch%itype(p), &
+ o3uptake=o3uptakesha(p))
+
+ ! Ozone uptake for sunlit leaves
+ call CalcOzoneUptakeOnePoint( &
+ forc_ozone=forc_o3_down(g), forc_pbot=forc_pbot(c), forc_th=forc_th(c), &
+ rs=rssun(p), rb=rb(p), ram=ram(p), &
+ tlai=tlai(p), tlai_old=tlai_old(p), pft_type=patch%itype(p), &
+ o3uptake=o3uptakesun(p))
+
+ tlai_old(p) = tlai(p)
+
+ end do
end associate
diff --git a/src/cpl/share_esmf/diurnalOzoneStreamMod.F90 b/src/cpl/share_esmf/diurnalOzoneStreamMod.F90
new file mode 100644
index 0000000000..abbb8bac02
--- /dev/null
+++ b/src/cpl/share_esmf/diurnalOzoneStreamMod.F90
@@ -0,0 +1,204 @@
+module diurnalOzoneStreamMod
+
+#include "shr_assert.h"
+
+ !-----------------------------------------------------------------------
+ ! !DESCRIPTION:
+ ! Read in file to convert input ozone to sub-daily values from stream
+ !
+ ! !USES:
+ use ESMF
+ use dshr_strdata_mod , only : shr_strdata_type
+ use shr_kind_mod , only : r8 => shr_kind_r8, CL => shr_kind_CL, CS => shr_kind_CS
+ use shr_log_mod , only : errMsg => shr_log_errMsg
+ use spmdMod , only : masterproc, mpicom
+ use clm_varctl , only : iulog
+ use abortutils , only : endrun
+ use decompMod , only : bounds_type
+ use DiurnalOzoneType , only : diurnal_ozone_anom_type
+
+ !
+ ! PUBLIC TYPES:
+ implicit none
+ private
+
+ ! PUBLIC MEMBER FUNCTIONS:
+ public :: read_O3_stream ! read dataset for diurnal ozone anomaly
+
+ character(len=*), parameter :: sourcefile = &
+ __FILE__
+
+ !==============================================================================
+ contains
+
+ !==============================================================================
+
+ subroutine read_O3_stream(diurnalOzoneAnomInst, bounds)
+ !
+ ! Initialize data stream information for LAI.
+ !
+ ! USES:
+ use spmdMod , only : iam
+ use clm_nlUtilsMod , only : find_nlgroup_name
+ use shr_log_mod , only : errMsg => shr_log_errMsg
+ use shr_mpi_mod , only : shr_mpi_bcast
+ use controlMod , only : NLFilename
+ use lnd_comp_shr , only : mesh, model_clock
+ use dshr_strdata_mod , only : shr_strdata_init_from_inline, shr_strdata_print
+ use dshr_strdata_mod , only : shr_strdata_advance
+ use dshr_methods_mod , only : dshr_fldbun_getfldptr
+ use ncdio_pio , only : file_desc_t, ncd_pio_openfile, ncd_io
+
+ !
+ ! ARGUMENTS:
+ type(diurnal_ozone_anom_type), intent(inout) :: diurnalOzoneAnomInst ! instance of diurnal ozone anomaly type
+ type(bounds_type), intent(in) :: bounds ! bounds
+ !
+ ! !LOCAL VARIABLES:
+ type(shr_strdata_type) :: sdat_dO3 ! input data stream
+ type(file_desc_t) :: ncid ! netcdf file id
+ real(r8), pointer :: dataptr2d(:,:) ! first dimension is level, second is data on that level
+ real(r8), pointer :: secs(:) ! time-of-day (units = seconds) dimension on file
+ integer :: ig, g, j ! indices
+ integer :: nu_nml ! unit for namelist file
+ integer :: nml_error ! namelist i/o error flag
+ integer :: year ! year (0, ...) for nstep+1
+ integer :: mon ! month (1, ..., 12) for nstep+1
+ integer :: day ! day of month (1, ..., 31) for nstep+1
+ integer :: sec ! seconds into current date for nstep+1
+ integer :: mcdate ! current model date (yyyymmdd)
+ integer :: rc ! error code
+ integer :: nlevsec ! dimension of 'secs' variable
+ integer, allocatable :: g_to_ig(:) ! array matching gridcell index to data index
+ logical :: readvar
+ character(len=CL) :: stream_fldFileName_dO3 = ' ' ! diurnal ozone stream filename to read
+ character(len=CL) :: stream_meshfile_dO3 = ' ' ! diurnal ozone stream meshfile
+ character(len=CL) :: dO3_mapalgo = ' ' ! mapping alogrithm
+ character(len=CL) :: stream_lev_dimname = 'sec' ! name of vertical layer dimension
+ character(*), parameter :: stream_var_name = "O3_diurnal_factor" ! base string for field string
+ character(len=*), parameter :: subName = "('dO3_init')"
+
+ !-----------------------------------------------------------------------
+ !
+ ! namelist variables
+ !
+ namelist /dO3_streams/ &
+ dO3_mapalgo, &
+ stream_fldFileName_dO3, &
+ stream_meshfile_dO3
+
+ ! Read dO3_streams namelist
+ if (masterproc) then
+ open( newunit=nu_nml, file=trim(NLFilename), status='old', iostat=nml_error )
+ call find_nlgroup_name(nu_nml, 'do3_streams', status=nml_error)
+ if (nml_error == 0) then
+ read(nu_nml, nml=dO3_streams,iostat=nml_error)
+ if (nml_error /= 0) then
+ call endrun(subname // ':: ERROR reading do3_streams namelist')
+ end if
+ else
+ call endrun(subname // ':: ERROR finding do3_streams namelist')
+ end if
+ close(nu_nml)
+ endif
+
+ call shr_mpi_bcast(stream_fldFileName_dO3 , mpicom)
+ call shr_mpi_bcast(stream_meshfile_dO3 , mpicom)
+ call shr_mpi_bcast(dO3_mapalgo , mpicom)
+
+ if (masterproc) then
+ write(iulog,*)
+ write(iulog,'(a)') 'do3_stream settings:'
+ write(iulog,'(a,a)' ) ' stream_fldFileName_do3 = ',trim(stream_fldFileName_dO3)
+ write(iulog,'(a,a)' ) ' stream_meshfile_do3 = ',trim(stream_meshfile_dO3)
+ write(iulog,'(a,a)' ) ' stream varname = ',trim(stream_var_name)
+ write(iulog,*)
+ endif
+
+ ! Initialize the cdeps data type sdat_dO3
+ call shr_strdata_init_from_inline(sdat_dO3, &
+ my_task = iam, &
+ logunit = iulog, &
+ compname = 'LND', &
+ model_clock = model_clock, &
+ model_mesh = mesh, &
+ stream_meshfile = trim(stream_meshfile_dO3), &
+ stream_lev_dimname = trim(stream_lev_dimname), &
+ stream_mapalgo = trim(dO3_mapalgo), &
+ stream_filenames = (/trim(stream_fldfilename_dO3)/), &
+ stream_fldlistFile = (/trim(stream_var_name)/), &
+ stream_fldListModel = (/trim(stream_var_name)/), &
+ stream_yearFirst = 2000, &
+ stream_yearLast = 2000, &
+ stream_yearAlign = 1, &
+ stream_offset = 0, &
+ stream_taxmode = 'extend', &
+ stream_dtlimit = 1.0e30_r8, &
+ stream_tintalgo = 'linear', &
+ stream_name = 'do3 data', &
+ rc = rc)
+ if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU, line=__LINE__, file=__FILE__)) then
+ call ESMF_Finalize(endflag=ESMF_END_ABORT)
+ end if
+
+ ! Explicitly set current date to a hardcoded constant value
+ ! as in ch4FInundatedStreamType
+ year = 2000
+ mon = 12
+ day = 31
+ sec = 0
+ mcdate = year*10000 + mon*100 + day
+
+ call shr_strdata_advance(sdat_dO3, ymd=mcdate, tod=sec, logunit=iulog, istr='do3', rc=rc)
+ if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU, line=__LINE__, file=__FILE__)) then
+ call ESMF_Finalize(endflag=ESMF_END_ABORT)
+ end if
+
+ ! Map gridcell to 1->local_size
+ if ( .not. allocated(g_to_ig) )then
+ allocate (g_to_ig(bounds%begg:bounds%endg) )
+ ig = 0
+ do g = bounds%begg,bounds%endg
+ ig = ig+1
+ g_to_ig(g) = ig
+ end do
+ end if
+
+ ! Get pointer for stream data that is time and spatially interpolated to model time and grid
+ call dshr_fldbun_getFldPtr(sdat_dO3%pstrm(1)%fldbun_model, trim(stream_var_name), fldptr2=dataptr2d, rc=rc)
+ if (ESMF_LogFoundError(rcToCheck=rc, msg=ESMF_LOGERR_PASSTHRU, line=__LINE__, file=__FILE__)) then
+ call ESMF_Finalize(endflag=ESMF_END_ABORT)
+ end if
+
+ ! set the nlevsec size
+ nlevsec = size(dataptr2d, dim=1)
+
+ ! read in the seconds array as well
+ allocate(secs(nlevsec))
+ call ncd_pio_openfile(ncid, trim(stream_fldFileName_dO3), 0)
+ call ncd_io(ncid=ncid, varname=trim(stream_lev_dimname), flag='read', data=secs, readvar=readvar)
+ if (.not. readvar) then
+ call endrun(msg=' ERROR: secs NOT on diurnal ozone file'//errMsg(sourcefile, __LINE__))
+ end if
+
+ ! initialize arrays
+ call diurnalOzoneAnomInst%Init(bounds, nlevsec)
+
+ ! set the diurnal ozone anomaly
+ do g = bounds%begg, bounds%endg
+ ig = g_to_ig(g)
+ do j = 1, nlevsec
+ diurnalOzoneAnomInst%o3_anomaly_grc(g,j) = dataptr2d(j,ig)
+ end do
+ end do
+
+ ! set the seconds array
+ do j = 1, nlevsec
+ diurnalOzoneAnomInst%time_arr(j) = secs(j)
+ end do
+
+ end subroutine read_O3_stream
+
+!==============================================================================
+
+end module diurnalOzoneStreamMod
diff --git a/src/main/clm_varctl.F90 b/src/main/clm_varctl.F90
index 7d0b2b55ad..59d8aeafff 100644
--- a/src/main/clm_varctl.F90
+++ b/src/main/clm_varctl.F90
@@ -241,9 +241,15 @@ module clm_varctl
! atmospheric CO2 molar ratio (by volume) (umol/mol)
real(r8), public :: co2_ppmv = 355._r8 !
- ! ozone vegitation stress method, valid values: unset, stress_lombardozzi2015, stress_falk
+ ! ozone vegetation stress method, valid values: unset, stress_lombardozzi2015, stress_falk
character(len=64), public :: o3_veg_stress_method = 'unset'
-
+
+ ! o3_streams parameters
+ logical, public :: use_do3_streams = .true.
+ character(len=fname_len), public :: stream_fldfilename_do3
+ character(len=fname_len), public :: stream_meshfile_do3
+ character(len=fname_len), public :: do3_mapalgo
+
real(r8), public :: o3_ppbv = 100._r8
! number of wavelength bands used in SNICAR snow albedo calculation
diff --git a/src/main/controlMod.F90 b/src/main/controlMod.F90
index 46d9e9958a..a7787f4257 100644
--- a/src/main/controlMod.F90
+++ b/src/main/controlMod.F90
@@ -244,9 +244,10 @@ subroutine control_init(dtime)
use_fates_tree_damage, &
fates_history_dimlevel
- ! Ozone vegetation stress method
- namelist / clm_inparm / o3_veg_stress_method
-
+ ! Ozone vegetation stress method and streams file
+ namelist / clm_inparm / o3_veg_stress_method, use_do3_streams, &
+ stream_fldfilename_do3, stream_meshfile_do3, do3_mapalgo
+
! CLM 5.0 nitrogen flags
namelist /clm_inparm/ use_flexibleCN, use_luna
@@ -987,6 +988,10 @@ subroutine control_print ()
write(iulog,*) ' use_grainproduct = ', use_grainproduct
write(iulog,*) ' crop_residue_removal_frac = ', crop_residue_removal_frac
write(iulog,*) ' o3_veg_stress_method = ', o3_veg_stress_method
+ write(iulog,*) ' use_do3_streams = ', use_do3_streams
+ write(iulog,*) ' stream_fldfilename_do3 = ', stream_fldfilename_do3
+ write(iulog,*) ' stream_meshfile_do3 = ', stream_meshfile_do3
+ write(iulog,*) ' do3_mapalgo = ', do3_mapalgo
write(iulog,*) ' use_snicar_frc = ', use_snicar_frc
write(iulog,*) ' snicar_use_aerosol = ',snicar_use_aerosol
write(iulog,*) ' use_vancouver = ', use_vancouver