From a95377d81fcd8aff07600df9c4a675a461a983a4 Mon Sep 17 00:00:00 2001 From: Micael Oliveira Date: Wed, 13 Dec 2023 16:00:16 +1100 Subject: [PATCH] Added functions to read and write a nuopc.runconfig file. --- om3utils/nuopc_config.py | 117 +++++++++++++++++++++++++++++++++++++ tests/test_nuopc_config.py | 69 ++++++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 om3utils/nuopc_config.py create mode 100644 tests/test_nuopc_config.py diff --git a/om3utils/nuopc_config.py b/om3utils/nuopc_config.py new file mode 100644 index 0000000..7f65729 --- /dev/null +++ b/om3utils/nuopc_config.py @@ -0,0 +1,117 @@ +"""NUOPC configuration""" + +from pathlib import Path +import re + + +def _convert_from_string(value: str): + """Tries to convert a string to the most appropriate type. Leaves the string unchanged if not conversion succeeds. + + :param value: value to convert. + """ + # Start by trying to convert from a Fortran logical to a Python bool + if value.lower() == ".true.": + return True + elif value.lower() == ".false.": + return False + # Next try to convert to integer or float + for conversion in [ + lambda: int(value), + lambda: float(value), + lambda: float(value.replace("D", "e")), + ]: + try: + out = conversion() + except ValueError: + continue + return out + # None of the above succeeded, so just return the string + return value + + +def _convert_to_string(value) -> str: + """Converts values to a string. + + :param value: value to convert. + """ + if isinstance(value, bool): + return ".true." if value else ".false." + elif isinstance(value, float): + return "{:e}".format(value).replace("e", "D") + else: + return str(value) + + +def read_nuopc_config(file_name: str) -> dict: + """Read a NUOPC config file. + + :param file_name: File name. + """ + fname = Path(file_name) + if not fname.is_file(): + raise FileNotFoundError(f"File not found: {fname.as_posix()}") + + label_value_pattern = re.compile(r"\s*(\w+)\s*:\s*(.+)\s*") + table_start_pattern = re.compile(r"\s*(\w+)\s*::\s*") + table_end_pattern = re.compile(r"\s*::\s*") + assignment_pattern = re.compile(r"\s*(\w+)\s*=\s*(\S+)\s*") + + config = {} + with open(fname, "r") as stream: + reading_table = False + label = None + table = None + for line in stream: + line = re.sub(r"(#).*", "", line) + if line.strip(): + if reading_table: + if re.match(table_end_pattern, line): + config[label] = table + reading_table = False + else: + match = re.match(assignment_pattern, line) + if match: + table[match.group(1)] = _convert_from_string(match.group(2)) + else: + raise ValueError( + f"Line: {line} in file {file_name} is not a valid NUOPC configuration specification" + ) + + elif re.match(table_start_pattern, line): + reading_table = True + match = re.match(label_value_pattern, line) + label = match.group(1) + table = {} + + elif re.match(label_value_pattern, line): + match = re.match(label_value_pattern, line) + if len(match.group(2).split()) > 1: + config[match.group(1)] = [ + _convert_from_string(string) + for string in match.group(2).split() + ] + else: + config[match.group(1)] = _convert_from_string(match.group(2)) + + return config + + +def write_nuopc_config(config: dict, file: Path): + """Write a NUOPC config dictionary as a Resource File. + + :param config: Dictionary holding the NUOPC configuration to write + :param file: File to write to. + """ + with open(file, "w") as stream: + for key, item in config.items(): + if isinstance(item, dict): + stream.write(key + "::\n") + for label, value in item.items(): + stream.write( + " " + label + " = " + _convert_to_string(value) + "\n" + ) + stream.write("::\n\n") + else: + stream.write( + key + ": " + " ".join(map(_convert_to_string, item)) + "\n" + ) diff --git a/tests/test_nuopc_config.py b/tests/test_nuopc_config.py new file mode 100644 index 0000000..e19d8e6 --- /dev/null +++ b/tests/test_nuopc_config.py @@ -0,0 +1,69 @@ +import pytest +import filecmp + +from utils import MockFile +from om3utils.nuopc_config import read_nuopc_config, write_nuopc_config + + +@pytest.fixture() +def simple_nuopc_config(): + return dict( + DRIVER_attributes={ + "Verbosity": "off", + "cime_model": "cesm", + "logFilePostFix": ".log", + "pio_blocksize": -1, + "pio_rearr_comm_enable_hs_comp2io": True, + "pio_rearr_comm_enable_hs_io2comp": False, + "reprosum_diffmax": -1.0e-8, + "wv_sat_table_spacing": 1.0, + "wv_sat_transition_start": 20.0, + }, + COMPONENTS=["atm", "ocn"], + ALLCOMP_attributes={ + "ATM_model": "datm", + "GLC_model": "sglc", + "OCN_model": "mom", + "ocn2glc_levels": "1:10:19:26:30:33:35", + }, + ) + + +@pytest.fixture() +def simple_nuopc_config_file(tmp_path): + file = tmp_path / "simple_config_file" + resource_file_str = """DRIVER_attributes:: + Verbosity = off + cime_model = cesm + logFilePostFix = .log + pio_blocksize = -1 + pio_rearr_comm_enable_hs_comp2io = .true. + pio_rearr_comm_enable_hs_io2comp = .false. + reprosum_diffmax = -1.000000D-08 + wv_sat_table_spacing = 1.000000D+00 + wv_sat_transition_start = 2.000000D+01 +:: + +COMPONENTS: atm ocn +ALLCOMP_attributes:: + ATM_model = datm + GLC_model = sglc + OCN_model = mom + ocn2glc_levels = 1:10:19:26:30:33:35 +:: + +""" + return MockFile(file, resource_file_str) + + +def test_read_nuopc_config(tmp_path, simple_nuopc_config, simple_nuopc_config_file): + config_from_file = read_nuopc_config(file_name=simple_nuopc_config_file.file) + + assert config_from_file == simple_nuopc_config + + +def test_write_nuopc_config(tmp_path, simple_nuopc_config, simple_nuopc_config_file): + file = tmp_path / "config_file" + write_nuopc_config(simple_nuopc_config, file) + + assert filecmp.cmp(file, simple_nuopc_config_file.file)