Skip to content

Commit

Permalink
Added functions to read and write a nuopc.runconfig file.
Browse files Browse the repository at this point in the history
  • Loading branch information
micaeljtoliveira committed Dec 18, 2023
1 parent fe53652 commit a95377d
Show file tree
Hide file tree
Showing 2 changed files with 186 additions and 0 deletions.
117 changes: 117 additions & 0 deletions om3utils/nuopc_config.py
Original file line number Diff line number Diff line change
@@ -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"
)
69 changes: 69 additions & 0 deletions tests/test_nuopc_config.py
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit a95377d

Please sign in to comment.