Skip to content

Commit 420b269

Browse files
jimmielinnusbaume
andauthored
Allow reading fields with a constituent dimension (#401)
Tag name (required for release branches): sima0_06_001 Originator(s): @jimmielin Description (include the issue title, and the keyword ['closes', 'fixes', 'resolves'] followed by the issue number): ## Background Certain fields written in the CAM snapshot (e.g., `cam_in%cflx`) have a constituent dimension. Reading such snapshot fields is necessary for testing vertical diffusion which needs this field. Presently, the registry is configured to accept the definition of fields with a `number_of_ccpp_constituents` dimension but the initial value read infrastructure does not accept it with the error: `cflx already has an initial_value, using it now. It also cannot be read from file: cflx has unsupported dimension, number_of_ccpp_constituents.` This PR enables the reading of 2-D variables with the dimension `horizontal_dimension number_of_ccpp_constituents` with the ability to trivially expand it to 3-D fields in the future. ## Details * Because the CAM snapshot output constituent indices do not match the indices in the SIMA model, companion PR in CAM (ESCOMP/CAM#1336) will output `cam_in%cflx` as `cam_in_cflx_<constituent name>` (i.e., `cam_in_cflx_Q`, `cam_in_cflx_CLDICE`, ...) * A new interface `read_constituent_dimensioned_field` will read fields with a constituent dimension, reading variables one-by-one with a name constructed from a "base name" (`cam_in_cflx`) + "constituent short name" (`_Q`, `_CLDICE`, ...) * Because CAM is not aware of standard names, constituent names are mapped from standard names to short names (Q, CLDICE, ...) through the registry.xml initial value definitions. * Unit tests have been updated Tested by vertical diffusion work in progress code in SIMA. Describe any changes made to build system: * `write_init_files.py` updated infrastructure to allow constituent dimension input Describe any changes made to the namelist: N/A List any changes to the defaults for the input datasets (e.g. boundary datasets): * Snapshot format update - see ESCOMP/CAM#1336. This update is backwards compatible. List all files eliminated and why: N/A List all files added and what they do: ``` A test/unit/python/sample_files/phys_vars_init_check_constituent_dim.F90 A test/unit/python/sample_files/physics_inputs_constituent_dim.F90 A test/unit/python/sample_files/physics_types_simple_constituent_dim.F90 A test/unit/python/sample_files/physics_types_simple_constituent_dim.meta A test/unit/python/sample_files/write_init_files/phys_vars_init_check_constituent_dim.F90 A test/unit/python/sample_files/write_init_files/physics_inputs_constituent_dim.F90 A test/unit/python/sample_files/write_init_files/simple_reg_constituent_dim.xml A test/unit/python/sample_files/write_init_files/temp_adjust_constituent_dim.F90 A test/unit/python/sample_files/write_init_files/temp_adjust_constituent_dim.meta - new unit test (test_simple_constituent_dimensioned_var_write_init) for constituent-dimensioned input variable A test/unit/python/sample_files/physics_types_simple_initial_value.F90 A test/unit/python/sample_files/physics_types_simple_initial_value.meta - fix missing files from previously introduced test ``` List all existing files that have been modified, and describe the changes: (Helpful git command: `git diff --name-status development...<your_branch_name>`) ``` M src/data/write_init_files.py - add checks for number_of_ccpp_constituents as alternate last dimension - if number_of_ccpp_constituents is a dimension, call read_constituent_dimensioned_field instead of read_field - rearrange location of const_props for use by above feature. M src/physics/utils/physics_data.F90 - add interface read_constituent_dimensioned_field to read variables which have number_of_ccpp_constituents as dimension. M test/unit/python/test_write_init_files.py - new unit test (test_simple_constituent_dimensioned_var_write_init) for constituent-dimensioned input variable M test/unit/python/sample_files/write_init_files/physics_inputs_4D.F90 M test/unit/python/sample_files/write_init_files/physics_inputs_bvd.F90 M test/unit/python/sample_files/write_init_files/physics_inputs_cnst.F90 M test/unit/python/sample_files/write_init_files/physics_inputs_ddt.F90 M test/unit/python/sample_files/write_init_files/physics_inputs_ddt2.F90 M test/unit/python/sample_files/write_init_files/physics_inputs_ddt_array.F90 M test/unit/python/sample_files/write_init_files/physics_inputs_host_var.F90 M test/unit/python/sample_files/write_init_files/physics_inputs_initial_value.F90 M test/unit/python/sample_files/write_init_files/physics_inputs_mf.F90 M test/unit/python/sample_files/write_init_files/physics_inputs_no_horiz.F90 M test/unit/python/sample_files/write_init_files/physics_inputs_noreq.F90 M test/unit/python/sample_files/write_init_files/physics_inputs_param.F90 M test/unit/python/sample_files/write_init_files/physics_inputs_protect.F90 M test/unit/python/sample_files/write_init_files/physics_inputs_scalar.F90 M test/unit/python/sample_files/write_init_files/physics_inputs_simple.F90 - updated sample files due to rearranging of const_props to top of subroutine. M test/unit/python/sample_files/write_init_files/simple_build_cache_template.xml M test/unit/python/sample_files/write_init_files/simple_reg.xml - fix typo and update hash for build cache template. M test/unit/python/sample_files/write_init_files/simple_reg_initial_value.xml - fix file name in xml. - fix allocatable should be allocatable (not pointer) if array has initial_value. ``` If there are new failures (compared to the `test/existing-test-failures.txt` file), have them OK'd by the gatekeeper, note them here, and add them to the file. If there are baseline differences, include the test and the reason for the diff. What is the nature of the change? Roundoff? derecho/intel/aux_sima: NLFAIL chemistry_nl - making baseline for #405 derecho/gnu/aux_sima: NLFAIL chemistry_nl - making baseline for #405 If this changes climate describe any run(s) done to evaluate the new climate in enough detail that it(they) could be reproduced: CAM-SIMA date used for the baseline comparison tests if different than latest: --------- Co-authored-by: Jesse Nusbaumer <nusbaume@ucar.edu>
1 parent c88ee96 commit 420b269

31 files changed

+1631
-67
lines changed

src/data/registry.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,8 +530,10 @@
530530
<dimensions>horizontal_dimension vertical_layer_dimension</dimensions>
531531
<ic_file_input_names>cp_or_cv_dycore</ic_file_input_names>
532532
</variable>
533+
533534
<!-- Constituent Variables -->
534535
<!-- These are only used to set possible IC file input names, as the constituents object handles allocation. -->
536+
<!-- Note: the first IC file input name should correspond to the short name of the constituent used in CAM, in order to facilitate reading constituent-dimensioned input fields. -->
535537
<variable local_name="q"
536538
standard_name="water_vapor_mixing_ratio_wrt_moist_air_and_condensed_water"
537539
units="kg kg-1" type="real" constituent="true">

src/data/write_init_files.py

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -739,14 +739,21 @@ def get_dimension_info(hvar):
739739
- The local variable name of the vertical dimension (or None)
740740
- True if <hvar> has one dimension which is a horizontal dimension or
741741
if <hvar> has two dimensions (horizontal and vertical)
742+
- Flag if any dimensions are number_of_ccpp_constituents and needs
743+
reading separate variables by constituent and reassembled into
744+
host model indices.
742745
"""
743746
vdim_name = None
744747
legal_dims = False
745748
fail_reason = ""
749+
746750
dims = hvar.get_dimensions()
747751
levnm = hvar.has_vertical_dimension()
752+
has_constituent_dim = any('number_of_ccpp_constituents' in dim for dim in dims)
753+
748754
# <hvar> is only 'legal' for 2 or 3 dimensional fields (i.e., 1 or 2
749-
# dimensional variables). The second dimension must be vertical.
755+
# dimensional variables).
756+
# The second dimension must either be vertical or number of constituents.
750757
# XXgoldyXX: If we ever need to read scalars, it would have to be
751758
# done using global attributes, not 'infld'.
752759
ldims = len(dims)
@@ -758,7 +765,17 @@ def get_dimension_info(hvar):
758765
fail_reason += f"{suff}{lname} has no horizontal dimension"
759766
suff = "; "
760767
# end if
761-
if (ldims > 2) or ((ldims > 1) and (not levnm)):
768+
769+
if has_constituent_dim:
770+
# A special case where any dimensions include number_of_ccpp_constituents,
771+
# in this case the variable needs to be suffixed by an underscore plus the constituent name
772+
# and read and reassembled separately into host model constituent indices
773+
# based on constituent name.
774+
# This case will be handled separately.
775+
legal_dims = True
776+
elif (ldims > 2) or ((ldims > 1) and (not levnm)):
777+
# The regular case where the second dimension must be vertical,
778+
# and higher dimensions are unsupported.
762779
legal_dims = False
763780
unsupp = []
764781
for dim in dims:
@@ -785,6 +802,7 @@ def get_dimension_info(hvar):
785802
# end if
786803
suff = "; "
787804
# end if
805+
788806
if legal_dims and levnm:
789807
# <hvar> should be legal, find the correct local name for the
790808
# vertical dimension
@@ -808,7 +826,8 @@ def get_dimension_info(hvar):
808826
raise ValueError(f"Vertical dimension, '{levnm}', not found")
809827
# end if
810828
# end if
811-
return vdim_name, legal_dims, fail_reason
829+
830+
return vdim_name, legal_dims, fail_reason, has_constituent_dim
812831

813832
def write_phys_read_subroutine(outfile, host_dict, host_vars, host_imports,
814833
phys_check_fname_str, constituent_set,
@@ -850,7 +869,7 @@ def write_phys_read_subroutine(outfile, host_dict, host_vars, host_imports,
850869
call_string_key = f"case ('{var_stdname}')"
851870

852871
# Extract vertical level variable:
853-
levnm, call_read_field, reason = get_dimension_info(hvar)
872+
levnm, call_read_field, reason, has_constituent_read = get_dimension_info(hvar)
854873
if hvar.get_prop_value('protected'):
855874
call_read_field = False
856875
if reason:
@@ -860,14 +879,23 @@ def write_phys_read_subroutine(outfile, host_dict, host_vars, host_imports,
860879
# end if
861880
lvar = hvar.get_prop_value('local_name')
862881
reason += f"{suff}{lvar} is a protected variable"
882+
# end if
883+
863884
# Set "read_field" call string:
864885
if call_read_field:
865-
# Replace vertical dimension with local name
866-
call_str = "call read_field(file, " + \
867-
f"'{var_stdname}', input_var_names(:,name_idx), "
886+
if has_constituent_read:
887+
# Special case for constituent-dimension variables.
888+
call_str = f"call read_constituent_dimensioned_field(const_props, file, '{var_stdname}', input_var_names(:,name_idx), "
889+
else:
890+
# Replace vertical dimension with local name
891+
call_str = "call read_field(file, " + \
892+
f"'{var_stdname}', input_var_names(:,name_idx), "
893+
# end if
894+
868895
if levnm is not None:
869896
call_str += f"'{levnm}', "
870897
# end if
898+
871899
err_on_not_found_string = ""
872900
if var_stdname in vars_init_value:
873901
# if initial value is available, do not throw error when not found in initial condition file.
@@ -903,7 +931,8 @@ def write_phys_read_subroutine(outfile, host_dict, host_vars, host_imports,
903931
["shr_kind_mod", ["SHR_KIND_CS, SHR_KIND_CL, SHR_KIND_CX"]],
904932
["physics_data", ["read_field", "find_input_name_idx",
905933
"no_exist_idx", "init_mark_idx",
906-
"prot_no_init_idx", "const_idx"]],
934+
"prot_no_init_idx", "const_idx",
935+
"read_constituent_dimensioned_field"]],
907936
["cam_ccpp_cap", ["ccpp_physics_suite_variables",
908937
"cam_constituents_array",
909938
"cam_model_const_properties"]],
@@ -967,8 +996,13 @@ def write_phys_read_subroutine(outfile, host_dict, host_vars, host_imports,
967996
outfile.write("logical :: use_init_variables", 2)
968997
outfile.blank_line()
969998

999+
# Prepare constituent properties pointer for later usage:
1000+
outfile.comment("Get constituent properties pointer:", 2)
1001+
outfile.write("const_props => cam_model_const_properties()", 2)
1002+
outfile.blank_line()
1003+
9701004
# Initialize variables:
971-
outfile.comment("Initalize missing and non-initialized variables strings:",
1005+
outfile.comment("Initialize missing and non-initialized variables strings:",
9721006
2)
9731007
outfile.write("missing_required_vars = ' '", 2)
9741008
outfile.write("protected_non_init_vars = ' '", 2)
@@ -1103,7 +1137,6 @@ def write_phys_read_subroutine(outfile, host_dict, host_vars, host_imports,
11031137
# Read in constituent data
11041138
outfile.comment("Read in constituent variables if not using init variables", 2)
11051139
outfile.write("field_data_ptr => cam_constituents_array()", 2)
1106-
outfile.write("const_props => cam_model_const_properties()", 2)
11071140
outfile.blank_line()
11081141
outfile.comment("Iterate over all registered constituents", 2)
11091142
outfile.write("do constituent_idx = 1, size(const_props)", 2)
@@ -1138,7 +1171,7 @@ def write_phys_read_subroutine(outfile, host_dict, host_vars, host_imports,
11381171
outfile.write("call const_props(constituent_idx)%minimum(constituent_min_value, constituent_errflg, constituent_errmsg)", 5)
11391172
outfile.write("field_data_ptr(:,:,constituent_idx) = constituent_min_value", 5)
11401173
outfile.write("if (masterproc) then", 5)
1141-
outfile.write("write(iulog,*) 'Constituent ', trim(std_name), ' default value not configured. Setting to 0.'", 6)
1174+
outfile.write("write(iulog,*) 'Constituent ', trim(std_name), ' default value not configured. Setting to min value of ', constituent_min_value", 6)
11421175
outfile.write("end if", 5)
11431176
outfile.write("end if", 4)
11441177
outfile.write("end if", 3)
@@ -1191,7 +1224,12 @@ def write_phys_check_subroutine(outfile, host_dict, host_vars, host_imports,
11911224
call_string_key = f"case ('{var_stdname}')"
11921225

11931226
# Extract vertical level variable:
1194-
levnm, call_check_field, reason = get_dimension_info(hvar)
1227+
levnm, call_check_field, reason, has_constituent_read = get_dimension_info(hvar)
1228+
1229+
# If this is a constituent-indexed field, do not check it for now.
1230+
if has_constituent_read:
1231+
continue
1232+
# end if
11951233

11961234
# Set "check_field" call string:
11971235
if call_check_field:

src/physics/utils/physics_data.F90

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ module physics_data
88

99
public :: find_input_name_idx
1010
public :: read_field
11+
public :: read_constituent_dimensioned_field
1112
public :: check_field
1213

1314
!Non-standard variable indices:
@@ -26,6 +27,10 @@ module physics_data
2627
module procedure check_field_3d
2728
end interface check_field
2829

30+
interface read_constituent_dimensioned_field
31+
module procedure read_constituent_dimensioned_field_2d
32+
end interface read_constituent_dimensioned_field
33+
2934
!==============================================================================
3035
CONTAINS
3136
!==============================================================================
@@ -325,6 +330,198 @@ subroutine read_field_3d(file, std_name, var_names, vcoord_name, &
325330
end if
326331
end subroutine read_field_3d
327332

333+
subroutine read_constituent_dimensioned_field_2d(const_props, file, std_name, base_var_names, timestep, field_array, error_on_not_found)
334+
use shr_assert_mod, only: shr_assert_in_domain
335+
use shr_sys_mod, only: shr_sys_flush
336+
use pio, only: file_desc_t, var_desc_t
337+
use spmd_utils, only: masterproc
338+
use cam_pio_utils, only: cam_pio_find_var
339+
use cam_abortutils, only: endrun, check_allocate
340+
use cam_logfile, only: iulog
341+
use cam_field_read, only: cam_read_field
342+
use ccpp_constituent_prop_mod, only: ccpp_constituent_prop_ptr_t
343+
use phys_vars_init_check, only: mark_as_read_from_file
344+
use phys_vars_init_check, only: phys_var_stdnames, input_var_names, phys_var_num
345+
346+
! Dummy arguments
347+
type(ccpp_constituent_prop_ptr_t), intent(in) :: const_props(:) ! Constituent properties
348+
type(file_desc_t), intent(inout) :: file !Parallel I/O (PIO) file type.
349+
character(len=*), intent(in) :: std_name ! Standard name of base variable.
350+
character(len=*), intent(in) :: base_var_names(:) ! "Base" name(s) used to construct variable name (base_constname)
351+
integer, intent(in) :: timestep ! Timestep to read [count]
352+
real(kind_phys), intent(inout) :: field_array(:,:) ! Output field array (ncol, pcnst)
353+
logical, optional, intent(in) :: error_on_not_found ! Flag to error and exit if not found
354+
355+
! Local variables
356+
logical :: var_found
357+
character(len=128) :: constituent_name
358+
character(len=256) :: file_var_name
359+
character(len=256) :: found_name
360+
character(len=512) :: missing_vars
361+
type(var_desc_t) :: vardesc
362+
real(kind_phys), allocatable :: buffer(:)
363+
integer :: const_idx, base_idx
364+
integer :: ierr
365+
logical :: error_on_not_found_local
366+
logical :: any_missing
367+
368+
! For construction of constituent short name mapping
369+
character(len=128), allocatable :: constituent_short_names(:)
370+
character(len=128) :: constituent_std_name
371+
integer :: n
372+
integer :: const_input_idx
373+
374+
character(len=256) :: errmsg
375+
376+
character(len=*), parameter :: subname = 'read_constituent_dimensioned_field: '
377+
378+
if (present(error_on_not_found)) then
379+
error_on_not_found_local = error_on_not_found
380+
else
381+
error_on_not_found_local = .true.
382+
end if
383+
384+
! Initialize tracking variables
385+
any_missing = .false.
386+
missing_vars = ''
387+
388+
! Allocate temporary buffer
389+
allocate(buffer(size(field_array, 1)), stat=ierr, errmsg=errmsg)
390+
call check_allocate(ierr, subname, 'buffer', errmsg=errmsg)
391+
392+
!REMOVECAM:
393+
! Because the constituent properties pointer contains standard names, and not input constituent names
394+
! (e.g., Q, CLDLIQ, ...) which are used in the input file names,
395+
! we have to construct a mapping of the standard names to the short input IC file names
396+
! When CAM is retired and only standard names are used for constituents, this mapping can be removed.
397+
allocate(constituent_short_names(size(const_props)), stat=ierr, errmsg=errmsg)
398+
call check_allocate(ierr, subname, 'constituent_short_names', errmsg=errmsg)
399+
400+
const_shortmap_loop: do const_idx = 1, size(const_props)
401+
! Get constituent standard name.
402+
call const_props(const_idx)%standard_name(constituent_std_name)
403+
404+
! Check if constituent standard name is in the registry to look up its IC name
405+
! n.b. this assumes that the first IC name specified in the registry for this constituent
406+
! is the short name
407+
const_input_idx = -1
408+
phys_inputvar_loop: do n = 1, phys_var_num
409+
if (trim(phys_var_stdnames(n)) == trim(constituent_std_name)) then
410+
const_input_idx = n
411+
exit phys_inputvar_loop
412+
end if
413+
end do phys_inputvar_loop
414+
415+
if (const_input_idx > 0) then
416+
! Use the first entry from the input_var_names -- assumed to be short name.
417+
constituent_short_names(const_idx) = trim(input_var_names(1, const_input_idx))
418+
else
419+
! Use the standard name itself if not found in registry.
420+
constituent_short_names(const_idx) = trim(constituent_std_name)
421+
end if
422+
end do const_shortmap_loop
423+
!END REMOVECAM
424+
425+
! Loop through all possible base names to find correct base name.
426+
! Note this assumes that the same base name is used for all constituents.
427+
! i.e., there cannot be something like cam_in_cflx_Q & cflx_CLDLIQ in one file.
428+
base_idx_loop: do base_idx = 1, size(base_var_names)
429+
! Loop through all constituents
430+
const_idx_loop: do const_idx = 1, size(const_props)
431+
! Get constituent short name
432+
constituent_name = constituent_short_names(const_idx)
433+
434+
! Create file variable name: <base_var_name>_<constituent_name>
435+
file_var_name = trim(base_var_names(base_idx)) // '_' // trim(constituent_name)
436+
437+
! Try to find variable in file
438+
var_found = .false.
439+
call cam_pio_find_var(file, [file_var_name], found_name, vardesc, var_found)
440+
441+
if(var_found) then
442+
exit base_idx_loop
443+
endif
444+
end do const_idx_loop
445+
end do base_idx_loop
446+
447+
if(.not. var_found .and. error_on_not_found_local) then
448+
call endrun(subname//'Required constituent-dimensioned variables not found: No match for ' // trim(std_name))
449+
end if
450+
451+
! Once base_idx is identified, use it in the actual constituent loop:
452+
const_read_loop: do const_idx = 1, size(const_props)
453+
! Get constituent short name
454+
constituent_name = constituent_short_names(const_idx)
455+
456+
! Create file variable name: <base_var_name>_<constituent_name>
457+
file_var_name = trim(base_var_names(base_idx)) // '_' // trim(constituent_name)
458+
459+
! Try to find variable in file
460+
var_found = .false.
461+
call cam_pio_find_var(file, [file_var_name], found_name, vardesc, var_found)
462+
463+
if (var_found) then
464+
! Read the variable
465+
if (masterproc) then
466+
write(iulog, *) 'Reading constituent-dimensioned input field, ', trim(found_name)
467+
call shr_sys_flush(iulog)
468+
end if
469+
470+
call cam_read_field(found_name, file, buffer, var_found, timelevel=timestep)
471+
472+
if (var_found) then
473+
! Copy to correct constituent index in field array
474+
field_array(:, const_idx) = buffer(:)
475+
476+
! Check for NaN values
477+
call shr_assert_in_domain(field_array(:, const_idx), is_nan=.false., &
478+
varname=trim(found_name), &
479+
msg=subname//'NaN found in '//trim(found_name))
480+
else
481+
! Failed to read even though variable was found
482+
any_missing = .true.
483+
if (len_trim(missing_vars) > 0) then
484+
missing_vars = trim(missing_vars) // ', ' // trim(file_var_name)
485+
else
486+
missing_vars = trim(file_var_name)
487+
end if
488+
end if
489+
else
490+
! Variable not found in file
491+
any_missing = .true.
492+
if (len_trim(missing_vars) > 0) then
493+
missing_vars = trim(missing_vars) // ', ' // trim(file_var_name)
494+
else
495+
missing_vars = trim(file_var_name)
496+
end if
497+
498+
if (.not. error_on_not_found_local) then
499+
! Use default value (already set at initialization)
500+
501+
if (masterproc) then
502+
write(iulog, *) 'Constituent-dimensioned field ', trim(file_var_name), &
503+
' not found, using default value for constituent ', trim(constituent_name)
504+
call shr_sys_flush(iulog)
505+
end if
506+
end if
507+
end if
508+
end do const_read_loop
509+
510+
! Check if we should fail due to missing variables
511+
if (any_missing .and. error_on_not_found_local) then
512+
call endrun(subname//'Required constituent-dimensioned variables not found: ' // trim(missing_vars) // &
513+
'Make sure the constituent short name is the first in the <ic_file_input_names> list in the registry.')
514+
end if
515+
516+
! Mark the base variable as read from file (only if no errors)
517+
call mark_as_read_from_file(std_name)
518+
519+
! Clean up
520+
deallocate(constituent_short_names)
521+
deallocate(buffer)
522+
523+
end subroutine read_constituent_dimensioned_field_2d
524+
328525
subroutine check_field_2d(file, var_names, timestep, current_value, &
329526
stdname, min_difference, min_relative_value, is_first, diff_found)
330527
use pio, only: file_desc_t, var_desc_t

0 commit comments

Comments
 (0)