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}