From 84835f4aa910b2429f3787b004a9e6481758b5e4 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Wed, 29 Apr 2026 15:07:02 -0600 Subject: [PATCH 1/2] Per #3274, add support for new wind config variables for tcrmw --- docs/Users_Guide/glossary.rst | 35 +++++++ docs/Users_Guide/wrappers.rst | 91 +++++++++++++++++++ .../wrappers/tcrmw/test_tcrmw_wrapper.py | 22 +++++ metplus/wrappers/tcrmw_wrapper.py | 15 +++ parm/met_config/TCRMWConfig_wrapped | 25 +++++ .../met_tool_wrapper/TCRMW/TCRMW.conf | 8 ++ 6 files changed, 196 insertions(+) diff --git a/docs/Users_Guide/glossary.rst b/docs/Users_Guide/glossary.rst index 66aeca9f9a..2620a1545b 100644 --- a/docs/Users_Guide/glossary.rst +++ b/docs/Users_Guide/glossary.rst @@ -15092,3 +15092,38 @@ METplus Configuration Glossary Sets obs_perc_value for the nth obs.field in the MET configuration file for EnsembleStat. | *Used by:* EnsembleStat + + TC_RMW_COMPUTE_TANGENTIAL_AND_RADIAL_WINDS + Specify the value for 'compute_tangential_and_radial_winds' in the MET configuration file for TCRMW. + + | *Used by:* TCRMW + + TC_RMW_U_WIND_FIELD_NAME + Specify the value for 'u_wind_field_name' in the MET configuration file for TCRMW. + + | *Used by:* TCRMW + + TC_RMW_V_WIND_FIELD_NAME + Specify the value for 'v_wind_field_name' in the MET configuration file for TCRMW. + + | *Used by:* TCRMW + + TC_RMW_TANGENTIAL_VELOCITY_FIELD_NAME + Specify the value for 'tangential_velocity_field_name' in the MET configuration file for TCRMW. + + | *Used by:* TCRMW + + TC_RMW_TANGENTIAL_VELOCITY_LONG_FIELD_NAME + Specify the value for 'tangential_velocity_long_field_name' in the MET configuration file for TCRMW. + + | *Used by:* TCRMW + + TC_RMW_RADIAL_VELOCITY_FIELD_NAME + Specify the value for 'radial_velocity_field_name' in the MET configuration file for TCRMW. + + | *Used by:* TCRMW + + TC_RMW_RADIAL_VELOCITY_LONG_FIELD_NAME + Specify the value for 'radial_velocity_long_field_name' in the MET configuration file for TCRMW. + + | *Used by:* TCRMW diff --git a/docs/Users_Guide/wrappers.rst b/docs/Users_Guide/wrappers.rst index 8bf6142b0b..c61bc8e134 100644 --- a/docs/Users_Guide/wrappers.rst +++ b/docs/Users_Guide/wrappers.rst @@ -13007,6 +13007,13 @@ METplus Configuration | :term:`TC_RMW_VALID_INCLUDE_LIST` | :term:`TC_RMW_VALID_EXCLUDE_LIST` | :term:`TC_RMW_VALID_HOUR_LIST` +| :term:`TC_RMW_COMPUTE_TANGENTIAL_AND_RADIAL_WINDS` +| :term:`TC_RMW_U_WIND_FIELD_NAME` +| :term:`TC_RMW_V_WIND_FIELD_NAME` +| :term:`TC_RMW_TANGENTIAL_VELOCITY_FIELD_NAME` +| :term:`TC_RMW_TANGENTIAL_VELOCITY_LONG_FIELD_NAME` +| :term:`TC_RMW_RADIAL_VELOCITY_FIELD_NAME` +| :term:`TC_RMW_RADIAL_VELOCITY_LONG_FIELD_NAME` | :term:`TC_RMW_SKIP_IF_OUTPUT_EXISTS` | :term:`TC_RMW_DESC` | :term:`MODEL` @@ -13277,6 +13284,90 @@ ${METPLUS_RMW_SCALE} * - :term:`TC_RMW_SCALE` - rmw_scale +${METPLUS_COMPUTE_TANGENTIAL_AND_RADIAL_WINDS} +"""""""""""""""""""""""""""""""""""""""""""""" + +.. list-table:: + :widths: 5 5 + :header-rows: 1 + + * - METplus Config(s) + - MET Config File + * - :term:`TC_RMW_COMPUTE_TANGENTIAL_AND_RADIAL_WINDS` + - compute_tangential_and_radial_winds + +${METPLUS_U_WIND_FIELD_NAME} +"""""""""""""""""""""""""""" + +.. list-table:: + :widths: 5 5 + :header-rows: 1 + + * - METplus Config(s) + - MET Config File + * - :term:`TC_RMW_U_WIND_FIELD_NAME` + - u_wind_field_name + +${METPLUS_V_WIND_FIELD_NAME} +"""""""""""""""""""""""""""" + +.. list-table:: + :widths: 5 5 + :header-rows: 1 + + * - METplus Config(s) + - MET Config File + * - :term:`TC_RMW_V_WIND_FIELD_NAME` + - v_wind_field_name + +${METPLUS_TANGENTIAL_VELOCITY_FIELD_NAME} +""""""""""""""""""""""""""""""""""""""""" + +.. list-table:: + :widths: 5 5 + :header-rows: 1 + + * - METplus Config(s) + - MET Config File + * - :term:`TC_RMW_TANGENTIAL_VELOCITY_FIELD_NAME` + - tangential_velocity_field_name + +${METPLUS_TANGENTIAL_VELOCITY_LONG_FIELD_NAME} +"""""""""""""""""""""""""""""""""""""""""""""" + +.. list-table:: + :widths: 5 5 + :header-rows: 1 + + * - METplus Config(s) + - MET Config File + * - :term:`TC_RMW_TANGENTIAL_VELOCITY_LONG_FIELD_NAME` + - tangential_velocity_long_field_name + +${METPLUS_RADIAL_VELOCITY_FIELD_NAME} +""""""""""""""""""""""""""""""""""""" + +.. list-table:: + :widths: 5 5 + :header-rows: 1 + + * - METplus Config(s) + - MET Config File + * - :term:`TC_RMW_RADIAL_VELOCITY_FIELD_NAME` + - radial_velocity_field_name + +${METPLUS_RADIAL_VELOCITY_LONG_FIELD_NAME} +"""""""""""""""""""""""""""""""""""""""""" + +.. list-table:: + :widths: 5 5 + :header-rows: 1 + + * - METplus Config(s) + - MET Config File + * - :term:`TC_RMW_RADIAL_VELOCITY_LONG_FIELD_NAME` + - radial_velocity_long_field_name + ${METPLUS_MET_CONFIG_OVERRIDES} """"""""""""""""""""""""""""""" diff --git a/internal/tests/pytests/wrappers/tcrmw/test_tcrmw_wrapper.py b/internal/tests/pytests/wrappers/tcrmw/test_tcrmw_wrapper.py index 0e24b953e2..e39cbc8173 100644 --- a/internal/tests/pytests/wrappers/tcrmw/test_tcrmw_wrapper.py +++ b/internal/tests/pytests/wrappers/tcrmw/test_tcrmw_wrapper.py @@ -110,6 +110,7 @@ def set_minimum_config_settings(config): 'censor_val = [12000, 5000];}' ) }), + ({'TC_RMW_N_RANGE': '10', }, {'METPLUS_N_RANGE': 'n_range = 10;'}), @@ -122,6 +123,27 @@ def set_minimum_config_settings(config): ({'TC_RMW_RMW_SCALE': '15', }, {'METPLUS_RMW_SCALE': 'rmw_scale = 15.0;'}), + ({'TC_RMW_COMPUTE_TANGENTIAL_AND_RADIAL_WINDS': 'true', }, + {'METPLUS_COMPUTE_TANGENTIAL_AND_RADIAL_WINDS': 'compute_tangential_and_radial_winds = TRUE;'}), + + ({'TC_RMW_U_WIND_FIELD_NAME': 'UGRD', }, + {'METPLUS_U_WIND_FIELD_NAME': 'u_wind_field_name = "UGRD";'}), + + ({'TC_RMW_V_WIND_FIELD_NAME': 'VGRD', }, + {'METPLUS_V_WIND_FIELD_NAME': 'v_wind_field_name = "VGRD";'}), + + ({'TC_RMW_TANGENTIAL_VELOCITY_FIELD_NAME': 'VT', }, + {'METPLUS_TANGENTIAL_VELOCITY_FIELD_NAME': 'tangential_velocity_field_name = "VT";'}), + + ({'TC_RMW_TANGENTIAL_VELOCITY_LONG_FIELD_NAME': 'Tangential Velocity', }, + {'METPLUS_TANGENTIAL_VELOCITY_LONG_FIELD_NAME': 'tangential_velocity_long_field_name = "Tangential Velocity";'}), + + ({'TC_RMW_RADIAL_VELOCITY_FIELD_NAME': 'VR', }, + {'METPLUS_RADIAL_VELOCITY_FIELD_NAME': 'radial_velocity_field_name = "VR";'}), + + ({'TC_RMW_RADIAL_VELOCITY_LONG_FIELD_NAME': 'Radial Velocity', }, + {'METPLUS_RADIAL_VELOCITY_LONG_FIELD_NAME': 'radial_velocity_long_field_name = "Radial Velocity";'}), + ] ) @pytest.mark.wrapper diff --git a/metplus/wrappers/tcrmw_wrapper.py b/metplus/wrappers/tcrmw_wrapper.py index 2cdf2d279f..43a1cc7333 100755 --- a/metplus/wrappers/tcrmw_wrapper.py +++ b/metplus/wrappers/tcrmw_wrapper.py @@ -46,6 +46,13 @@ class TCRMWWrapper(RuntimeFreqWrapper): 'METPLUS_N_AZIMUTH', 'METPLUS_DELTA_RANGE_KM', 'METPLUS_RMW_SCALE', + 'METPLUS_COMPUTE_TANGENTIAL_AND_RADIAL_WINDS', + 'METPLUS_U_WIND_FIELD_NAME', + 'METPLUS_V_WIND_FIELD_NAME', + 'METPLUS_TANGENTIAL_VELOCITY_FIELD_NAME', + 'METPLUS_TANGENTIAL_VELOCITY_LONG_FIELD_NAME', + 'METPLUS_RADIAL_VELOCITY_FIELD_NAME', + 'METPLUS_RADIAL_VELOCITY_LONG_FIELD_NAME', ] def __init__(self, config, instance=None): @@ -133,6 +140,14 @@ def create_c_dict(self): 'TC_RMW_VALID_HOUR', ]) + self.add_met_config(name='compute_tangential_and_radial_winds', data_type='bool') + self.add_met_config(name='u_wind_field_name', data_type='string') + self.add_met_config(name='v_wind_field_name', data_type='string') + self.add_met_config(name='tangential_velocity_field_name', data_type='string') + self.add_met_config(name='tangential_velocity_long_field_name', data_type='string') + self.add_met_config(name='radial_velocity_field_name', data_type='string') + self.add_met_config(name='radial_velocity_long_field_name', data_type='string') + c_dict['VAR_LIST_TEMP'] = parse_var_list(self.config, data_type='FCST', met_tool=self.app_name, diff --git a/parm/met_config/TCRMWConfig_wrapped b/parm/met_config/TCRMWConfig_wrapped index 9df3ddec8f..25fafab240 100644 --- a/parm/met_config/TCRMWConfig_wrapped +++ b/parm/met_config/TCRMWConfig_wrapped @@ -81,6 +81,31 @@ ${METPLUS_DELTA_RANGE_KM} //rmw_scale = ${METPLUS_RMW_SCALE} +// +// Optionally convert u/v winds to tangential/radial winds +// + +//compute_tangential_and_radial_winds = +${METPLUS_COMPUTE_TANGENTIAL_AND_RADIAL_WINDS} + +//u_wind_field_name = +${METPLUS_U_WIND_FIELD_NAME} + +//v_wind_field_name = +${METPLUS_V_WIND_FIELD_NAME} + +//tangential_velocity_field_name = +${METPLUS_TANGENTIAL_VELOCITY_FIELD_NAME} + +//tangential_velocity_long_field_name = +${METPLUS_TANGENTIAL_VELOCITY_LONG_FIELD_NAME} + +//radial_velocity_field_name = +${METPLUS_RADIAL_VELOCITY_FIELD_NAME} + +//radial_velocity_long_field_name = +${METPLUS_RADIAL_VELOCITY_LONG_FIELD_NAME} + //////////////////////////////////////////////////////////////////////////////// //version = "V10.0"; diff --git a/parm/use_cases/met_tool_wrapper/TCRMW/TCRMW.conf b/parm/use_cases/met_tool_wrapper/TCRMW/TCRMW.conf index 60b2e494db..3e118c961c 100644 --- a/parm/use_cases/met_tool_wrapper/TCRMW/TCRMW.conf +++ b/parm/use_cases/met_tool_wrapper/TCRMW/TCRMW.conf @@ -95,3 +95,11 @@ TC_RMW_CYCLONE = 14 #TC_RMW_VALID_INCLUDE_LIST = #TC_RMW_VALID_EXCLUDE_LIST = #TC_RMW_VALID_HOUR_LIST = + +#TC_RMW_COMPUTE_TANGENTIAL_AND_RADIAL_WINDS = +#TC_RMW_U_WIND_FIELD_NAME = +#TC_RMW_V_WIND_FIELD_NAME = +#TC_RMW_TANGENTIAL_VELOCITY_FIELD_NAME = +#TC_RMW_TANGENTIAL_VELOCITY_LONG_FIELD_NAME = +#TC_RMW_RADIAL_VELOCITY_FIELD_NAME = +#TC_RMW_RADIAL_VELOCITY_LONG_FIELD_NAME = \ No newline at end of file From 7f7f9bc74f59e175d203e524bcf448214335e587 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:26:33 -0600 Subject: [PATCH 2/2] create TCBase wrapper to add common functionality of TC wrappers --- metplus/wrappers/__init__.py | 1 + metplus/wrappers/tc_base_wrapper.py | 76 +++++++++++++++++++++++++++++ metplus/wrappers/tc_diag_wrapper.py | 50 ++----------------- metplus/wrappers/tcrmw_wrapper.py | 56 ++------------------- 4 files changed, 85 insertions(+), 98 deletions(-) create mode 100644 metplus/wrappers/tc_base_wrapper.py diff --git a/metplus/wrappers/__init__.py b/metplus/wrappers/__init__.py index ae8a1c3aae..d598ea09d7 100644 --- a/metplus/wrappers/__init__.py +++ b/metplus/wrappers/__init__.py @@ -12,6 +12,7 @@ from .reformat_gridded_wrapper import ReformatGriddedWrapper from .reformat_point_wrapper import ReformatPointWrapper from .compare_gridded_wrapper import CompareGriddedWrapper +from .tc_base_wrapper import TCBaseWrapper # import RegridDataPlane wrapper because it is used by other wrappers from .regrid_data_plane_wrapper import RegridDataPlaneWrapper diff --git a/metplus/wrappers/tc_base_wrapper.py b/metplus/wrappers/tc_base_wrapper.py new file mode 100644 index 0000000000..49261f6426 --- /dev/null +++ b/metplus/wrappers/tc_base_wrapper.py @@ -0,0 +1,76 @@ +""" +Program Name: tc_base_wrapper.py +Contact(s): George McCabe +""" + +from . import RuntimeFreqWrapper + +from ..util import get_lead_sequence, sub_var_list +from ..util.time_util import ti_get_hours_from_relativedelta + +'''!@namespace TCBaseWrapper +@brief parent class for the tropical cyclone wrappers +@endcode +''' + +class TCBaseWrapper(RuntimeFreqWrapper): + + def __init__(self, config, instance=None): + super().__init__(config, instance=instance) + + def add_met_config_tc_wind(self): + self.add_met_config(name='compute_tangential_and_radial_winds', data_type='bool') + self.add_met_config(name='u_wind_field_name', data_type='string') + self.add_met_config(name='v_wind_field_name', data_type='string') + self.add_met_config(name='tangential_velocity_field_name', data_type='string') + self.add_met_config(name='tangential_velocity_long_field_name', data_type='string') + self.add_met_config(name='radial_velocity_field_name', data_type='string') + self.add_met_config(name='radial_velocity_long_field_name', data_type='string') + + def _set_lead_list(self, time_info, lead_seq=None): + self.env_var_dict['METPLUS_LEAD_LIST'] = '' + + if lead_seq is None: + lead_seq = get_lead_sequence(self.config, time_info) + + # set LEAD_LIST to list of forecast leads used + if lead_seq == [0]: + return + + lead_list = [] + for lead in lead_seq: + lead_hours = ( + ti_get_hours_from_relativedelta(lead, valid_time=time_info['valid']) + ) + lead_list.append(f'"{str(lead_hours).zfill(2)}"') + + self.env_var_dict['METPLUS_LEAD_LIST'] = f"lead = [{', '.join(lead_list)}];" + + def _set_data_field(self, time_info): + """!Get list of fields from config to process. Build list of field info + that are formatted to be read by the MET config file. Set DATA_FIELD + item of c_dict with the formatted list of fields. + Args: + @param time_info time dictionary to use for string substitution + @returns True if field list could be built, False if not. + """ + field_list = sub_var_list(self.c_dict['VAR_LIST_TEMP'], time_info) + if not field_list: + self.log_error("Could not get field information from config.") + return False + + all_fields = [] + for field in field_list: + field_list = self.get_field_info(d_type='FCST', + v_name=field['fcst_name'], + v_level=field['fcst_level'], + ) + if field_list is None: + self.log_error(f'Could not get field info from {field}') + return False + + all_fields.extend(field_list) + + data_field = ','.join(all_fields) + self.env_var_dict['METPLUS_DATA_FIELD'] = f'field = [{data_field}];' + return True diff --git a/metplus/wrappers/tc_diag_wrapper.py b/metplus/wrappers/tc_diag_wrapper.py index cef52e6a66..4c1653e630 100755 --- a/metplus/wrappers/tc_diag_wrapper.py +++ b/metplus/wrappers/tc_diag_wrapper.py @@ -13,7 +13,7 @@ import os from ..util import time_util -from . import RuntimeFreqWrapper +from . import TCBaseWrapper from ..util import do_string_sub, get_lead_sequence from ..util import parse_var_list, sub_var_list, getlist from ..util import find_indices_in_config_section @@ -25,7 +25,7 @@ ''' -class TCDiagWrapper(RuntimeFreqWrapper): +class TCDiagWrapper(TCBaseWrapper): RUNTIME_FREQ_DEFAULT = 'RUN_ONCE_PER_INIT_OR_VALID' RUNTIME_FREQ_SUPPORTED = ['RUN_ONCE_PER_INIT_OR_VALID'] @@ -190,18 +190,7 @@ def create_c_dict(self): self.handle_regrid(c_dict, set_to_grid=False) - self.add_met_config(name='compute_tangential_and_radial_winds', - data_type='bool') - self.add_met_config(name='u_wind_field_name', data_type='string') - self.add_met_config(name='v_wind_field_name', data_type='string') - self.add_met_config(name='tangential_velocity_field_name', - data_type='string') - self.add_met_config(name='tangential_velocity_long_field_name', - data_type='string') - self.add_met_config(name='radial_velocity_field_name', - data_type='string') - self.add_met_config(name='radial_velocity_long_field_name', - data_type='string') + self.add_met_config_tc_wind() self.add_met_config(name='vortex_removal', data_type='bool') @@ -314,11 +303,11 @@ def run_at_time_once(self, time_info): return # get field information to set in MET config - if not self.set_data_field(time_info): + if not self._set_data_field(time_info): return # set forecast lead list for MET config - self.set_lead_list(time_info) + self._set_lead_list(time_info) # get other configurations for command self.set_command_line_arguments(time_info) @@ -328,35 +317,6 @@ def run_at_time_once(self, time_info): self.build() - def set_data_field(self, time_info): - """!Get list of fields from config to process. Build list of field info - that are formatted to be read by the MET config file. Set DATA_FIELD - item of c_dict with the formatted list of fields. - Args: - @param time_info time dictionary to use for string substitution - @returns True if field list could be built, False if not. - """ - field_list = sub_var_list(self.c_dict['VAR_LIST_TEMP'], time_info) - if not field_list: - self.log_error("Could not get field information from config.") - return False - - all_fields = [] - for field in field_list: - field_list = self.get_field_info(d_type='FCST', - v_name=field['fcst_name'], - v_level=field['fcst_level'], - ) - if field_list is None: - return False - - all_fields.extend(field_list) - - data_field = ','.join(all_fields) - self.env_var_dict['METPLUS_DATA_FIELD'] = f'field = [{data_field}];' - - return True - def find_input_files(self, time_info): """!Get DECK file and list of input data files and set c_dict items. Args: diff --git a/metplus/wrappers/tcrmw_wrapper.py b/metplus/wrappers/tcrmw_wrapper.py index 43a1cc7333..c4ead096a5 100755 --- a/metplus/wrappers/tcrmw_wrapper.py +++ b/metplus/wrappers/tcrmw_wrapper.py @@ -15,7 +15,7 @@ from ..util import ti_calculate, ti_get_hours_from_relativedelta from ..util import do_string_sub, get_lead_sequence from ..util import parse_var_list, sub_var_list -from . import RuntimeFreqWrapper +from . import TCBaseWrapper '''!@namespace TCRMWWrapper @brief Wraps the TC-RMW tool @@ -23,7 +23,7 @@ ''' -class TCRMWWrapper(RuntimeFreqWrapper): +class TCRMWWrapper(TCBaseWrapper): RUNTIME_FREQ_DEFAULT = 'RUN_ONCE_PER_INIT_OR_VALID' RUNTIME_FREQ_SUPPORTED = ['RUN_ONCE_PER_INIT_OR_VALID'] @@ -140,13 +140,7 @@ def create_c_dict(self): 'TC_RMW_VALID_HOUR', ]) - self.add_met_config(name='compute_tangential_and_radial_winds', data_type='bool') - self.add_met_config(name='u_wind_field_name', data_type='string') - self.add_met_config(name='v_wind_field_name', data_type='string') - self.add_met_config(name='tangential_velocity_field_name', data_type='string') - self.add_met_config(name='tangential_velocity_long_field_name', data_type='string') - self.add_met_config(name='radial_velocity_field_name', data_type='string') - self.add_met_config(name='radial_velocity_long_field_name', data_type='string') + self.add_met_config_tc_wind() c_dict['VAR_LIST_TEMP'] = parse_var_list(self.config, data_type='FCST', @@ -250,50 +244,6 @@ def find_input_files(self, time_info): return time_info - def _set_data_field(self, time_info): - """!Get list of fields from config to process. Build list of field info - that are formatted to be read by the MET config file. Set DATA_FIELD - item of c_dict with the formatted list of fields. - Args: - @param time_info time dictionary to use for string substitution - @returns True if field list could be built, False if not. - """ - field_list = sub_var_list(self.c_dict['VAR_LIST_TEMP'], time_info) - if not field_list: - self.log_error("Could not get field information from config.") - return False - - all_fields = [] - for field in field_list: - field_list = self.get_field_info(d_type='FCST', - v_name=field['fcst_name'], - v_level=field['fcst_level'], - ) - if field_list is None: - self.log_error(f'Could not get field info from {field}') - return False - - all_fields.extend(field_list) - - data_field = ','.join(all_fields) - self.env_var_dict['METPLUS_DATA_FIELD'] = f'field = [{data_field}];' - return True - - def _set_lead_list(self, time_info, lead_seq): - # set LEAD_LIST to list of forecast leads used - if lead_seq == [0]: - return - - lead_list = [] - for lead in lead_seq: - lead_hours = ( - ti_get_hours_from_relativedelta(lead, - valid_time=time_info['valid']) - ) - lead_list.append(f'"{str(lead_hours).zfill(2)}"') - - self.env_var_dict['METPLUS_LEAD_LIST'] = f"lead = [{', '.join(lead_list)}];" - def set_command_line_arguments(self, time_info): if self.c_dict['CONFIG_FILE']: config_file = do_string_sub(self.c_dict['CONFIG_FILE'],