From 1d049991c7c7bbddebff3b52b5ac8820d0f5e816 Mon Sep 17 00:00:00 2001 From: Spencer Clark Date: Mon, 22 Mar 2021 09:39:57 -0400 Subject: [PATCH] Add getters and setters for variables used to override surface radiative fluxes (#244) This PR is tied to https://github.com/VulcanClimateModeling/fv3gfs-fortran/pull/158. It adds getters and setters for four fields in the fortran model: - `Statein%adjsfcdlw_override` (the downward longwave radiative flux at the surface seen by the land surface model) - `Statein%adjsfcdsw_override` (the downward shortwave radiative flux at the surface seen by the land surface model) - `Statein%adjsfcnsw_override` (the net shortwave radiative flux at the surface seen by the land surface model). - `Radtend%sfalb` (the average surface albedo) The first three are only valid if `gfs_physics_nml.override_surface_radiative_fluxes` is set to `.true.` in the fortran namelist (if it is not set to `.true.`, memory will not be allocated for them). We add a flag to the wrapper to be able to dynamically check this; if it is not set and a user asks to get or set one of these variables, then an informative error will be raised (rather than a model segmentation fault). The test infrastructure for the setters is modified to check this. In adding the new boolean flag, it was discovered that there was a bug in the implementation of earlier boolean flags (https://github.com/VulcanClimateModeling/fv3gfs-wrapper/pull/244#discussion_r597034750). We fixed this, and added more comprehensive tests of the flags, which did not exist previously. In addition we added a flag for the physics timestep, `dt_atmos`, using the preexisting `get_physics_timestep_subroutine` subroutine. Finally, to check that the fortran diagnostics for the radiative fluxes were updated as expected, we added an additional set of tests (which basically are an automated way of doing what we did in [this notebook](https://github.com/VulcanClimateModeling/explore/blob/master/spencerc/2021-03-04-predicting-radiative-fluxes-online/2021-03-11-verify-diagnostics.ipynb)). We also add a test that ensures that modifying the surface radiative fluxes modifies the temperature after a timestep (indicating that the modifications are being felt by the land surface model). --- HISTORY.md | 20 ++++ fill_templates.py | 44 ++++--- fv3gfs/wrapper/_properties.py | 9 ++ fv3gfs/wrapper/_restart/io.py | 8 +- fv3gfs/wrapper/flagstruct_properties.json | 6 + fv3gfs/wrapper/physics_properties.json | 28 +++++ lib/external | 2 +- templates/_wrapper.pyx | 23 ++++ templates/flagstruct_data.F90 | 5 +- tests/test_all.py | 27 +++-- tests/test_diagnostics.py | 5 +- tests/test_flags.py | 67 +++++++++++ tests/test_get_time.py | 30 +---- tests/test_getters.py | 36 ++++-- ..._overrides_for_surface_radiative_fluxes.py | 108 ++++++++++++++++++ tests/test_setters.py | 62 +++++++++- tests/test_tracer_metadata.py | 30 +---- tests/util.py | 22 +++- 18 files changed, 424 insertions(+), 108 deletions(-) create mode 100644 tests/test_flags.py create mode 100644 tests/test_overrides_for_surface_radiative_fluxes.py diff --git a/HISTORY.md b/HISTORY.md index 24566dc3..41fe78cc 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,26 @@ History ======= +Unreleased +---------- + +Minor changes: +- Added getters and setters for `Statein%adjsfcdlw_override`, + `Statein%adjsfcdsw_override`, and `Statein%adjsfcnsw_override`. These + correspond to the time adjusted total sky downward longwave radiative flux at + the surface, the time adjusted total sky downward shortwave radiative flux at + the surface, and the time adjusted total sky net shortwave radiative flux at + the surface, respectively. Note they are only available if the + `gfs_physics_nml.override_surface_radiative_fluxes` namelist parameter is set + to `.true.`. +- Added getter and setter for `Radtend%sfalb`, the surface diffused shortwave + albedo. +- Added flags for the physics timestep, `dt_atmos`, and namelist flag for + overriding the surface radiative fluxes, `override_surface_radiative_fluxes`, + to the `Flags` class. +- Fixed a bug in the implementation of boolean flags that prevented them from + working properly; to date the only flag this impacted was `do_adiabatic_init`. + v0.6.0 (2021-01-27) ------------------- diff --git a/fill_templates.py b/fill_templates.py index e61bf135..a73296ce 100644 --- a/fill_templates.py +++ b/fill_templates.py @@ -23,6 +23,16 @@ ) SETUP_DIR = os.path.dirname(os.path.abspath(__file__)) PROPERTIES_DIR = os.path.join(SETUP_DIR, "fv3gfs/wrapper") +FORTRAN_TO_C_AND_CYTHON_TYPES = { + "integer": {"type_c": "c_int", "type_cython": "int"}, + "real": {"type_c": "c_double", "type_cython": "REAL_t"}, + "logical": {"type_c": "c_int", "type_cython": "bint"}, +} +OVERRIDES_FOR_SURFACE_RADIATIVE_FLUXES = [ + "override_for_time_adjusted_total_sky_downward_longwave_flux_at_surface", + "override_for_time_adjusted_total_sky_downward_shortwave_flux_at_surface", + "override_for_time_adjusted_total_sky_net_shortwave_flux_at_surface", +] def get_dim_range_string(dim_list): @@ -30,6 +40,21 @@ def get_dim_range_string(dim_list): return ", ".join(reversed(token_list)) # Fortran order is opposite of Python +def assign_types_to_flags(flag_data): + flag_properties = [] + for flag in flag_data: + type_fortran = flag["type_fortran"] + if type_fortran in FORTRAN_TO_C_AND_CYTHON_TYPES: + flag.update(FORTRAN_TO_C_AND_CYTHON_TYPES[type_fortran]) + else: + unexpected_type = flag["type_fortran"] + raise NotImplementedError( + f"unexpected value for type_fortran: {unexpected_type}" + ) + flag_properties.append(flag) + return flag_properties + + if __name__ == "__main__": requested_templates = sys.argv[1:] @@ -51,7 +76,6 @@ def get_dim_range_string(dim_list): physics_2d_properties = [] physics_3d_properties = [] dynamics_properties = [] - flagstruct_properties = [] for properties in physics_data: if len(properties["dims"]) == 2: @@ -65,22 +89,7 @@ def get_dim_range_string(dim_list): properties["dim_colons"] = ", ".join(":" for dim in properties["dims"]) dynamics_properties.append(properties) - for flag in flagstruct_data: - if flag["type_fortran"] == "integer": - flag["type_c"] = "c_int" - flag["type_cython"] = "int" - elif flag["type_fortran"] == "real": - flag["type_c"] = "c_double" - flag["type_cython"] = "REAL_t" - elif flag["type_fortran"] == "logical": - flag["type_c"] = "c_bool" - flag["type_cython"] = "bint" - else: - unexpected_type = flag["type_fortran"] - raise NotImplementedError( - f"unexpected value for type_fortran: {unexpected_type}" - ) - flagstruct_properties.append(flag) + flagstruct_properties = assign_types_to_flags(flagstruct_data) if len(requested_templates) == 0: requested_templates = all_templates @@ -94,6 +103,7 @@ def get_dim_range_string(dim_list): physics_3d_properties=physics_3d_properties, dynamics_properties=dynamics_properties, flagstruct_properties=flagstruct_properties, + overriding_fluxes=OVERRIDES_FOR_SURFACE_RADIATIVE_FLUXES, ) with open(out_filename, "w") as f: f.write(result) diff --git a/fv3gfs/wrapper/_properties.py b/fv3gfs/wrapper/_properties.py index af42148c..a103f619 100644 --- a/fv3gfs/wrapper/_properties.py +++ b/fv3gfs/wrapper/_properties.py @@ -9,6 +9,15 @@ with open(os.path.join(DIR, "physics_properties.json"), "r") as f: PHYSICS_PROPERTIES = json.load(f) +with open(os.path.join(DIR, "flagstruct_properties.json"), "r") as f: + FLAGSTRUCT_PROPERTIES = json.load(f) + +OVERRIDES_FOR_SURFACE_RADIATIVE_FLUXES = [ + "override_for_time_adjusted_total_sky_downward_longwave_flux_at_surface", + "override_for_time_adjusted_total_sky_downward_shortwave_flux_at_surface", + "override_for_time_adjusted_total_sky_net_shortwave_flux_at_surface", +] + DIM_NAMES = { properties["name"]: properties["dims"] for properties in DYNAMICS_PROPERTIES + PHYSICS_PROPERTIES diff --git a/fv3gfs/wrapper/_restart/io.py b/fv3gfs/wrapper/_restart/io.py index 05e8284d..82f09509 100644 --- a/fv3gfs/wrapper/_restart/io.py +++ b/fv3gfs/wrapper/_restart/io.py @@ -1,5 +1,9 @@ from .._wrapper import get_tracer_metadata -from .._properties import DYNAMICS_PROPERTIES, PHYSICS_PROPERTIES +from .._properties import ( + DYNAMICS_PROPERTIES, + PHYSICS_PROPERTIES, + OVERRIDES_FOR_SURFACE_RADIATIVE_FLUXES, +) # these variables are found not to be needed for smooth restarts # later we could represent this as a key in the dynamics/physics properties @@ -7,7 +11,7 @@ "convective_cloud_fraction", "convective_cloud_top_pressure", "convective_cloud_bottom_pressure", -] +] + OVERRIDES_FOR_SURFACE_RADIATIVE_FLUXES def get_restart_names(): diff --git a/fv3gfs/wrapper/flagstruct_properties.json b/fv3gfs/wrapper/flagstruct_properties.json index 5dcd9eb8..6789838c 100644 --- a/fv3gfs/wrapper/flagstruct_properties.json +++ b/fv3gfs/wrapper/flagstruct_properties.json @@ -28,5 +28,11 @@ "fortran_name" : "do_adiabatic_init", "location" : "do_adiabatic_init", "type_fortran": "logical" + }, + { + "name": "override_surface_radiative_fluxes", + "fortran_name" : "override_surface_radiative_fluxes", + "location" : "IPD_Control", + "type_fortran": "logical" } ] diff --git a/fv3gfs/wrapper/physics_properties.json b/fv3gfs/wrapper/physics_properties.json index 930dce5b..61c538b7 100644 --- a/fv3gfs/wrapper/physics_properties.json +++ b/fv3gfs/wrapper/physics_properties.json @@ -112,6 +112,13 @@ "container": "Radtend", "dims": ["y", "x"] }, + { + "name": "surface_diffused_shortwave_albedo", + "fortran_name": "sfalb", + "units": "", + "container": "Radtend", + "dims": ["y", "x"] + }, { "name": "total_sky_downward_shortwave_flux_at_top_of_atmosphere", "fortran_name": "topfsw", @@ -455,5 +462,26 @@ "units": "unknown", "container": "Sfcprop", "dims": ["z_soil", "y", "x"] + }, + { + "name": "override_for_time_adjusted_total_sky_downward_longwave_flux_at_surface", + "fortran_name": "adjsfcdlw_override", + "units": "W/m^2", + "container": "Statein", + "dims": ["y", "x"] + }, + { + "name": "override_for_time_adjusted_total_sky_downward_shortwave_flux_at_surface", + "fortran_name": "adjsfcdsw_override", + "units": "W/m^2", + "container": "Statein", + "dims": ["y", "x"] + }, + { + "name": "override_for_time_adjusted_total_sky_net_shortwave_flux_at_surface", + "fortran_name": "adjsfcnsw_override", + "units": "W/m^2", + "container": "Statein", + "dims": ["y", "x"] } ] diff --git a/lib/external b/lib/external index d95144d4..94b4ce61 160000 --- a/lib/external +++ b/lib/external @@ -1 +1 @@ -Subproject commit d95144d446cf369c803be9e0f93bf7891821a086 +Subproject commit 94b4ce61be175e31ab22907e9079e47c310254ba diff --git a/templates/_wrapper.pyx b/templates/_wrapper.pyx index cc0cce28..5bd85e3e 100644 --- a/templates/_wrapper.pyx +++ b/templates/_wrapper.pyx @@ -199,8 +199,16 @@ cdef int set_2d_quantity(name, REAL_t[:, ::1] array) except -1: {% endif %} {% endfor %} {% for item in physics_2d_properties %} + {% if item.name in overriding_fluxes %} + elif name == '{{ item.name }}': + if flags.override_surface_radiative_fluxes: + set_{{ item.fortran_name }}{% if "fortran_subname" in item %}_{{ item.fortran_subname }}{% endif %}(&array[0, 0]) + else: + raise fv3gfs.util.InvalidQuantityError('Overriding surface fluxes can only be set if gfs_physics_nml.override_surface_radiative_fluxes is set to .true.') + {% else %} elif name == '{{ item.name }}': set_{{ item.fortran_name }}{% if "fortran_subname" in item %}_{{ item.fortran_subname }}{% endif %}(&array[0, 0]) + {% endif %} {% endfor %} else: raise ValueError(f'no setter available for {name}') @@ -261,10 +269,20 @@ def get_state(names, dict state=None, allocator=None): state['time'] = get_time() {% for item in physics_2d_properties %} + {% if item.name in overriding_fluxes %} + if '{{ item.name }}' in input_names_set: + if flags.override_surface_radiative_fluxes: + quantity = _get_quantity(state, "{{ item.name }}", allocator, {{ item.dims | safe }}, "{{ item.units }}", dtype=real_type) + with fv3gfs.util.recv_buffer(quantity.np.empty, quantity.view[:]) as array_2d: + get_{{ item.fortran_name }}{% if "fortran_subname" in item %}_{{ item.fortran_subname }}{% endif %}(&array_2d[0, 0]) + else: + raise fv3gfs.util.InvalidQuantityError('Overriding surface fluxes can only be accessed if gfs_physics_nml.override_surface_radiative_fluxes is set to .true.') + {% else %} if '{{ item.name }}' in input_names_set: quantity = _get_quantity(state, "{{ item.name }}", allocator, {{ item.dims | safe }}, "{{ item.units }}", dtype=real_type) with fv3gfs.util.recv_buffer(quantity.np.empty, quantity.view[:]) as array_2d: get_{{ item.fortran_name }}{% if "fortran_subname" in item %}_{{ item.fortran_subname }}{% endif %}(&array_2d[0, 0]) + {% endif %} {% endfor %} {% for item in physics_3d_properties %} @@ -372,6 +390,11 @@ class Flags: get_{{item.fortran_name}}(&{{item.name}}) return {{item.name}} {% endfor %} + @property + def dt_atmos(self): + cdef int dt_atmos + get_physics_timestep_subroutine(&dt_atmos) + return dt_atmos flags = Flags() diff --git a/templates/flagstruct_data.F90 b/templates/flagstruct_data.F90 index 6c33a7d9..e85f7c35 100644 --- a/templates/flagstruct_data.F90 +++ b/templates/flagstruct_data.F90 @@ -1,6 +1,7 @@ module flagstruct_data_mod use atmosphere_mod, only: Atm, mytile +use atmos_model_mod, only: IPD_Control use fv_nwp_nudge_mod, only: do_adiabatic_init use iso_c_binding @@ -11,7 +12,7 @@ module flagstruct_data_mod {% for item in flagstruct_properties %} {% if item.fortran_name == "do_adiabatic_init" %} subroutine get_do_adiabatic_init(do_adiabatic_init_out) bind(c) - logical(c_bool), intent(out) :: do_adiabatic_init_out + logical(c_int), intent(out) :: do_adiabatic_init_out do_adiabatic_init_out = do_adiabatic_init end subroutine get_do_adiabatic_init {% else %} @@ -21,6 +22,8 @@ subroutine get_{{ item.fortran_name }}({{ item.fortran_name }}_out) bind(c) {{ item.fortran_name }}_out = Atm(mytile)%flagstruct%{{ item.fortran_name }} {% elif item.location == "Atm" %} {{ item.fortran_name }}_out = Atm(mytile)%{{ item.fortran_name }} + {% elif item.location == "IPD_Control" %} + {{ item.fortran_name }}_out = IPD_Control%{{ item.fortran_name }} {% endif %} end subroutine get_{{ item.fortran_name }} {% endif %} diff --git a/tests/test_all.py b/tests/test_all.py index dc3b7d30..30da1b10 100644 --- a/tests/test_all.py +++ b/tests/test_all.py @@ -1,9 +1,7 @@ import unittest from mpi4py import MPI from util import run_unittest_script -import os -base_dir = os.path.dirname(os.path.realpath(__file__)) # The packages we import will import MPI, causing an MPI init, but we don't actually # want to use MPI under this script. We have to finalize so mpirun will work on @@ -13,25 +11,34 @@ class UsingMPITests(unittest.TestCase): def test_getters(self): - run_unittest_script(os.path.join(base_dir, "test_getters.py")) + run_unittest_script("test_getters.py") - def test_setters(self): - run_unittest_script(os.path.join(base_dir, "test_setters.py")) + def test_setters_default(self): + run_unittest_script("test_setters.py", "false") + + def test_setters_while_overriding_surface_radiative_fluxes(self): + run_unittest_script("test_setters.py", "true") + + def test_overrides_for_surface_radiative_fluxes_modify_diagnostics(self): + run_unittest_script("test_overrides_for_surface_radiative_fluxes.py") def test_diagnostics(self): - run_unittest_script(os.path.join(base_dir, "test_diagnostics.py")) + run_unittest_script("test_diagnostics.py") def test_tracer_metadata(self): - run_unittest_script(os.path.join(base_dir, "test_tracer_metadata.py")) + run_unittest_script("test_tracer_metadata.py") def test_get_time_julian(self): - run_unittest_script(os.path.join(base_dir, "test_get_time.py"), "julian") + run_unittest_script("test_get_time.py", "julian") def test_get_time_thirty_day(self): - run_unittest_script(os.path.join(base_dir, "test_get_time.py"), "thirty_day") + run_unittest_script("test_get_time.py", "thirty_day") def test_get_time_noleap(self): - run_unittest_script(os.path.join(base_dir, "test_get_time.py"), "noleap") + run_unittest_script("test_get_time.py", "noleap") + + def test_flags(self): + run_unittest_script("test_flags.py") if __name__ == "__main__": diff --git a/tests/test_diagnostics.py b/tests/test_diagnostics.py index 1656f96e..54b8ae23 100644 --- a/tests/test_diagnostics.py +++ b/tests/test_diagnostics.py @@ -4,7 +4,7 @@ import fv3gfs.util from mpi4py import MPI -from util import main +from util import get_default_config, main test_dir = os.path.dirname(os.path.abspath(__file__)) @@ -48,4 +48,5 @@ def test_get_diagnostic_data(self): if __name__ == "__main__": - main(test_dir) + config = get_default_config() + main(test_dir, config) diff --git a/tests/test_flags.py b/tests/test_flags.py new file mode 100644 index 00000000..0ad62308 --- /dev/null +++ b/tests/test_flags.py @@ -0,0 +1,67 @@ +import unittest +import os +import numpy as np +import fv3gfs.wrapper +import fv3gfs.util +from fv3gfs.wrapper._properties import FLAGSTRUCT_PROPERTIES +from mpi4py import MPI + +from util import get_default_config, generate_data_dict, main + +test_dir = os.path.dirname(os.path.abspath(__file__)) +FORTRAN_TO_PYTHON_TYPE = {"integer": int, "real": float, "logical": bool} + + +class FlagsTest(unittest.TestCase): + def __init__(self, *args, **kwargs): + super(FlagsTest, self).__init__(*args, **kwargs) + self.flagstruct_data = generate_data_dict(FLAGSTRUCT_PROPERTIES) + self.mpi_comm = MPI.COMM_WORLD + + def setUp(self): + pass + + def tearDown(self): + self.mpi_comm.barrier() + + def test_flagstruct_properties_present_in_metadata(self): + """Test that some small subset of flagstruct names are in the data dictionary""" + for name in ["do_adiabatic_init", "override_surface_radiative_fluxes"]: + self.assertIn(name, self.flagstruct_data.keys()) + + def test_get_all_flagstruct_properties(self): + self._get_all_properties_helper(self.flagstruct_data) + + def _get_all_properties_helper(self, properties): + for name, data in properties.items(): + with self.subTest(name): + result = getattr(fv3gfs.wrapper.flags, name) + expected_type = FORTRAN_TO_PYTHON_TYPE[data["type_fortran"]] + self.assertIsInstance(result, expected_type) + + def test_ptop(self): + """Test that getting a real flag produces its expected result.""" + result = fv3gfs.wrapper.flags.ptop + expected = 64.247 + np.testing.assert_allclose(result, expected) + + def test_n_split(self): + """Test that getting an integer flag produces its expected result.""" + result = fv3gfs.wrapper.flags.n_split + expected = 6 + self.assertEqual(result, expected) + + def test_override_surface_radiative_fluxes(self): + """Test that getting a boolean flag produces its expected result.""" + result = fv3gfs.wrapper.flags.override_surface_radiative_fluxes + self.assertFalse(result) + + def test_dt_atmos(self): + result = fv3gfs.wrapper.flags.dt_atmos + expected = 900 + self.assertEqual(result, expected) + + +if __name__ == "__main__": + config = get_default_config() + main(test_dir, config) diff --git a/tests/test_get_time.py b/tests/test_get_time.py index bde72f9b..243a04bf 100644 --- a/tests/test_get_time.py +++ b/tests/test_get_time.py @@ -11,14 +11,11 @@ """ import sys import unittest -import yaml import os -import shutil import cftime -import fv3config import fv3gfs.wrapper from mpi4py import MPI -from util import redirect_stdout +from util import get_default_config, main test_dir = os.path.dirname(os.path.abspath(__file__)) @@ -64,28 +61,7 @@ def set_calendar_type(): if __name__ == "__main__": - rank = MPI.COMM_WORLD.Get_rank() calendar = set_calendar_type() - config = yaml.safe_load(open(os.path.join(test_dir, "default_config.yml"), "r")) + config = get_default_config() config["namelist"]["coupler_nml"]["calendar"] = calendar - rundir = os.path.join(test_dir, "rundir") - if rank == 0: - if os.path.isdir(rundir): - shutil.rmtree(rundir) - fv3config.write_run_directory(config, rundir) - MPI.COMM_WORLD.barrier() - original_path = os.getcwd() - os.chdir(rundir) - try: - with redirect_stdout(os.devnull): - fv3gfs.wrapper.initialize() - MPI.COMM_WORLD.barrier() - if rank != 0: - kwargs = {"verbosity": 0} - else: - kwargs = {"verbosity": 2} - unittest.main(**kwargs) - finally: - os.chdir(original_path) - if rank == 0: - shutil.rmtree(rundir) + main(test_dir, config) diff --git a/tests/test_getters.py b/tests/test_getters.py index a8c4b59d..8c1a3783 100644 --- a/tests/test_getters.py +++ b/tests/test_getters.py @@ -1,29 +1,31 @@ import unittest import os import numpy as np -import yaml import fv3gfs.wrapper import fv3gfs.util -from fv3gfs.wrapper._properties import DYNAMICS_PROPERTIES, PHYSICS_PROPERTIES +from fv3gfs.wrapper._properties import ( + DYNAMICS_PROPERTIES, + PHYSICS_PROPERTIES, + OVERRIDES_FOR_SURFACE_RADIATIVE_FLUXES, +) from mpi4py import MPI +from util import get_current_config, get_default_config, generate_data_dict, main -from util import main test_dir = os.path.dirname(os.path.abspath(__file__)) MM_PER_M = 1000 - - -def get_config(): - with open("fv3config.yml") as f: - return yaml.safe_load(f) +DEFAULT_PHYSICS_PROPERTIES = [] +for entry in PHYSICS_PROPERTIES: + if entry["name"] not in OVERRIDES_FOR_SURFACE_RADIATIVE_FLUXES: + DEFAULT_PHYSICS_PROPERTIES.append(entry) class GetterTests(unittest.TestCase): def __init__(self, *args, **kwargs): super(GetterTests, self).__init__(*args, **kwargs) self.tracer_data = fv3gfs.wrapper.get_tracer_metadata() - self.dynamics_data = {entry["name"]: entry for entry in DYNAMICS_PROPERTIES} - self.physics_data = {entry["name"]: entry for entry in PHYSICS_PROPERTIES} + self.dynamics_data = generate_data_dict(DYNAMICS_PROPERTIES) + self.physics_data = generate_data_dict(DEFAULT_PHYSICS_PROPERTIES) self.mpi_comm = MPI.COMM_WORLD def setUp(self): @@ -91,7 +93,7 @@ def test_get_surface_precipitation_rate(self): ) total_precip = state["total_precipitation"] precip_rate = state["surface_precipitation_rate"] - config = get_config() + config = get_current_config() dt = config["namelist"]["coupler_nml"]["dt_atmos"] np.testing.assert_allclose( MM_PER_M * total_precip.view[:] / dt, precip_rate.view[:] @@ -156,6 +158,15 @@ def _get_names_helper(self, name_list): self.assertIn(name, state) self.assertEqual(len(name_list), len(state.keys())) + def _get_unallocated_name_helper(self, name): + with self.assertRaisesRegex(fv3gfs.util.InvalidQuantityError, "Overriding"): + fv3gfs.wrapper.get_state(names=[name]) + + def test_unallocated_physics_properties(self): + for name in OVERRIDES_FOR_SURFACE_RADIATIVE_FLUXES: + with self.subTest(name): + self._get_unallocated_name_helper(name) + class TracerMetadataTests(unittest.TestCase): def test_tracer_index_is_one_based(self): @@ -198,4 +209,5 @@ def test_all_tracers_present(self): if __name__ == "__main__": - main(test_dir) + config = get_default_config() + main(test_dir, config) diff --git a/tests/test_overrides_for_surface_radiative_fluxes.py b/tests/test_overrides_for_surface_radiative_fluxes.py new file mode 100644 index 00000000..98a96109 --- /dev/null +++ b/tests/test_overrides_for_surface_radiative_fluxes.py @@ -0,0 +1,108 @@ +import unittest +import os +from copy import deepcopy +from fv3gfs.wrapper._properties import OVERRIDES_FOR_SURFACE_RADIATIVE_FLUXES +import numpy as np +import fv3gfs.wrapper +import fv3gfs.util +from mpi4py import MPI +from util import get_default_config, main + + +test_dir = os.path.dirname(os.path.abspath(__file__)) +( + DOWNWARD_LONGWAVE, + DOWNWARD_SHORTWAVE, + NET_SHORTWAVE, +) = OVERRIDES_FOR_SURFACE_RADIATIVE_FLUXES + + +def override_surface_radiative_fluxes_with_random_values(): + old_state = fv3gfs.wrapper.get_state(names=OVERRIDES_FOR_SURFACE_RADIATIVE_FLUXES) + replace_state = deepcopy(old_state) + for name, quantity in replace_state.items(): + quantity.view[:] = np.random.uniform(size=quantity.extent) + fv3gfs.wrapper.set_state(replace_state) + return replace_state + + +def get_state_single_variable(name): + return fv3gfs.wrapper.get_state([name])[name].view[:] + + +class OverridingSurfaceRadiativeFluxTests(unittest.TestCase): + def __init__(self, *args, **kwargs): + super(OverridingSurfaceRadiativeFluxTests, self).__init__(*args, **kwargs) + + def setUp(self): + pass + + def tearDown(self): + MPI.COMM_WORLD.barrier() + + def test_resetting_to_checkpoint_allows_for_exact_restart(self): + checkpoint_state = fv3gfs.wrapper.get_state(fv3gfs.wrapper.get_restart_names()) + + # Run the model forward a timestep and save the temperature. + fv3gfs.wrapper.step() + expected = get_state_single_variable("air_temperature") + + # Restore state to original checkpoint; step the model forward again. + # Check that the temperature is identical as after the first time we + # took a step. + fv3gfs.wrapper.set_state(checkpoint_state) + fv3gfs.wrapper.step() + result = get_state_single_variable("air_temperature") + np.testing.assert_equal(result, expected) + + def test_overriding_fluxes_changes_model_state(self): + checkpoint_state = fv3gfs.wrapper.get_state(fv3gfs.wrapper.get_restart_names()) + + fv3gfs.wrapper.step() + temperature_with_default_override = get_state_single_variable("air_temperature") + + # Restore state to original checkpoint; modify the radiative fluxes; + # step the model again. + fv3gfs.wrapper.set_state(checkpoint_state) + override_surface_radiative_fluxes_with_random_values() + fv3gfs.wrapper.step() + temperature_with_random_override = get_state_single_variable("air_temperature") + + # We expect these states to differ. + assert not np.array_equal( + temperature_with_default_override, temperature_with_random_override + ) + + def test_overriding_fluxes_are_propagated_to_diagnostics(self): + replace_state = override_surface_radiative_fluxes_with_random_values() + + # We need to step the model to fill the diagnostics buckets. + fv3gfs.wrapper.step() + + timestep = fv3gfs.wrapper.flags.dt_atmos + expected_DSWRFI = replace_state[DOWNWARD_SHORTWAVE].view[:] + expected_DLWRFI = replace_state[DOWNWARD_LONGWAVE].view[:] + expected_USWRFI = ( + replace_state[DOWNWARD_SHORTWAVE].view[:] + - replace_state[NET_SHORTWAVE].view[:] + ) + + result_DSWRF = fv3gfs.wrapper.get_diagnostic_by_name("DSWRF").view[:] + result_DLWRF = fv3gfs.wrapper.get_diagnostic_by_name("DLWRF").view[:] + result_USWRF = fv3gfs.wrapper.get_diagnostic_by_name("USWRF").view[:] + result_DSWRFI = fv3gfs.wrapper.get_diagnostic_by_name("DSWRFI").view[:] + result_DLWRFI = fv3gfs.wrapper.get_diagnostic_by_name("DLWRFI").view[:] + result_USWRFI = fv3gfs.wrapper.get_diagnostic_by_name("USWRFI").view[:] + + np.testing.assert_allclose(result_DSWRF, timestep * expected_DSWRFI) + np.testing.assert_allclose(result_DLWRF, timestep * expected_DLWRFI) + np.testing.assert_allclose(result_USWRF, timestep * expected_USWRFI) + np.testing.assert_allclose(result_DSWRFI, expected_DSWRFI) + np.testing.assert_allclose(result_DLWRFI, expected_DLWRFI) + np.testing.assert_allclose(result_USWRFI, expected_USWRFI) + + +if __name__ == "__main__": + config = get_default_config() + config["namelist"]["gfs_physics_nml"]["override_surface_radiative_fluxes"] = True + main(test_dir, config) diff --git a/tests/test_setters.py b/tests/test_setters.py index b5d1f051..c3ef28ed 100644 --- a/tests/test_setters.py +++ b/tests/test_setters.py @@ -1,22 +1,35 @@ import unittest import os +import sys from copy import deepcopy import numpy as np import fv3gfs.wrapper -from fv3gfs.wrapper._properties import DYNAMICS_PROPERTIES, PHYSICS_PROPERTIES +from fv3gfs.wrapper._properties import ( + DYNAMICS_PROPERTIES, + PHYSICS_PROPERTIES, + OVERRIDES_FOR_SURFACE_RADIATIVE_FLUXES, +) import fv3gfs.util from mpi4py import MPI -from util import main +from util import get_current_config, get_default_config, generate_data_dict, main + test_dir = os.path.dirname(os.path.abspath(__file__)) +DEFAULT_PHYSICS_PROPERTIES = [] +for entry in PHYSICS_PROPERTIES: + if entry["name"] not in OVERRIDES_FOR_SURFACE_RADIATIVE_FLUXES: + DEFAULT_PHYSICS_PROPERTIES.append(entry) class SetterTests(unittest.TestCase): def __init__(self, *args, **kwargs): super(SetterTests, self).__init__(*args, **kwargs) self.tracer_data = fv3gfs.wrapper.get_tracer_metadata() - self.dynamics_data = {entry["name"]: entry for entry in DYNAMICS_PROPERTIES} - self.physics_data = {entry["name"]: entry for entry in PHYSICS_PROPERTIES} + self.dynamics_data = generate_data_dict(DYNAMICS_PROPERTIES) + if fv3gfs.wrapper.flags.override_surface_radiative_fluxes: + self.physics_data = generate_data_dict(PHYSICS_PROPERTIES) + else: + self.physics_data = generate_data_dict(DEFAULT_PHYSICS_PROPERTIES) def setUp(self): pass @@ -168,6 +181,45 @@ def _check_gotten_state(self, state, name_list): def assert_values_equal(self, quantity1, quantity2): self.assertTrue(quantity1.np.all(quantity1.view[:] == quantity2.view[:])) + def _set_unallocated_override_for_radiative_surface_flux(self, name): + config = get_current_config() + sizer = fv3gfs.util.SubtileGridSizer.from_namelist(config["namelist"]) + factory = fv3gfs.util.QuantityFactory(sizer, np) + quantity = factory.zeros(["x", "y"], units="W/m**2") + with self.assertRaisesRegex(fv3gfs.util.InvalidQuantityError, "Overriding"): + fv3gfs.wrapper.set_state({name: quantity}) + + def test_set_unallocated_override_for_radiative_surface_flux(self): + if fv3gfs.wrapper.flags.override_surface_radiative_fluxes: + self.skipTest("Memory is allocated for the overriding fluxes in this case.") + for name in OVERRIDES_FOR_SURFACE_RADIATIVE_FLUXES: + with self.subTest(name): + self._set_unallocated_override_for_radiative_surface_flux(name) + + +def get_override_surface_radiative_fluxes(): + """A crude way of parameterizing the setter tests for different values of + gfs_physics_nml.override_surface_radiative_fluxes. + + See https://stackoverflow.com/questions/11380413/python-unittest-passing-arguments. + """ + if len(sys.argv) != 2: + raise ValueError( + "test_setters.py requires a single argument " + "be passed through the command line, indicating the value of " + "the gfs_physics_nml.override_surface_radiative_fluxes flag " + "('true' or 'false')." + ) + override_surface_radiative_fluxes = sys.argv.pop().lower() + + # Convert string argument to bool. + return override_surface_radiative_fluxes == "true" + if __name__ == "__main__": - main(test_dir) + config = get_default_config() + override_surface_radiative_fluxes = get_override_surface_radiative_fluxes() + config["namelist"]["gfs_physics_nml"][ + "override_surface_radiative_fluxes" + ] = override_surface_radiative_fluxes + main(test_dir, config) diff --git a/tests/test_tracer_metadata.py b/tests/test_tracer_metadata.py index bbffa3f3..39189743 100644 --- a/tests/test_tracer_metadata.py +++ b/tests/test_tracer_metadata.py @@ -1,11 +1,7 @@ import unittest import os -import shutil -import yaml -from mpi4py import MPI -import fv3config import fv3gfs.wrapper -from util import redirect_stdout +from util import get_default_config, main test_dir = os.path.dirname(os.path.abspath(__file__)) rundir = os.path.join(test_dir, "rundir") @@ -81,8 +77,7 @@ def test_all_tracers_in_restart_names(self): if __name__ == "__main__": - with open(os.path.join(test_dir, "default_config.yml"), "r") as f: - config = yaml.safe_load(f) + config = get_default_config() config[ "initial_conditions" ] = "gs://vcm-fv3config/data/initial_conditions/c12_restart_initial_conditions/v1.0" @@ -92,23 +87,4 @@ def test_all_tracers_in_restart_names(self): config["namelist"]["fv_core_nml"]["mountain"] = True config["namelist"]["fv_core_nml"]["warm_start"] = True config["namelist"]["fv_core_nml"]["na_init"] = 0 - if MPI.COMM_WORLD.Get_rank() == 0: - if os.path.isdir(rundir): - shutil.rmtree(rundir) - fv3config.write_run_directory(config, rundir) - MPI.COMM_WORLD.barrier() - original_path = os.getcwd() - os.chdir(rundir) - try: - with redirect_stdout(os.devnull): - fv3gfs.wrapper.initialize() - MPI.COMM_WORLD.barrier() - if MPI.COMM_WORLD.Get_rank() != 0: - kwargs = {"verbosity": 0} - else: - kwargs = {"verbosity": 2} - unittest.main(**kwargs) - finally: - os.chdir(original_path) - if MPI.COMM_WORLD.Get_rank() == 0: - shutil.rmtree(rundir) + main(test_dir, config) diff --git a/tests/util.py b/tests/util.py index 2ec4f122..34e6cf97 100644 --- a/tests/util.py +++ b/tests/util.py @@ -13,9 +13,11 @@ libc = ctypes.CDLL(None) c_stdout = ctypes.c_void_p.in_dll(libc, "stdout") +base_dir = os.path.dirname(os.path.realpath(__file__)) -def run_unittest_script(filename, *args, n_processes=6): +def run_unittest_script(script_name, *args, n_processes=6): + filename = os.path.join(base_dir, script_name) python_args = ["python3", "-m", "mpi4py", filename] + list(args) subprocess.check_call(["mpirun", "-n", str(n_processes)] + python_args) @@ -74,10 +76,8 @@ def _redirect_stdout(self, to_file_descriptor): sys.stdout = io.TextIOWrapper(os.fdopen(self._stdout_file_descriptor, "wb")) -def main(test_dir): +def main(test_dir, config): rank = MPI.COMM_WORLD.Get_rank() - with open(os.path.join(test_dir, "default_config.yml"), "r") as f: - config = yaml.safe_load(f) rundir = os.path.join(test_dir, "rundir") if rank == 0: if os.path.isdir(rundir): @@ -99,3 +99,17 @@ def main(test_dir): os.chdir(original_path) if rank == 0: shutil.rmtree(rundir) + + +def get_default_config(): + with open(os.path.join(base_dir, "default_config.yml"), "r") as f: + return yaml.safe_load(f) + + +def get_current_config(): + with open("fv3config.yml") as f: + return yaml.safe_load(f) + + +def generate_data_dict(properties): + return {entry["name"]: entry for entry in properties}