Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 60 additions & 77 deletions cime_config/MOM_RPS/FType_MOM_params.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import os
import re
from collections import OrderedDict

from CIME.ParamGen.paramgen import ParamGen

from param_file_reader import read_MOM_params


class FType_MOM_params(ParamGen):
"""Encapsulates data and read/write methods for MOM6 case parameter files: MOM_input, user_nl."""
Expand All @@ -23,84 +24,43 @@ def from_MOM_input(cls, input_path):
Path to MOM_input file to read in and generate a FType_MOM_params object.
"""

_data = FType_MOM_params._read_MOM_input(input_path)
_data = read_MOM_params(input_path)
return FType_MOM_params(_data)

@staticmethod
def _read_MOM_input(input_path):
"""Reads in input files in MOM_input syntax. Note that this method may be used to
read in MOM_override and user_nl_mom too, since the syntax is the same, but
write methods for MOM_input and MOM_override are different."""

_data = OrderedDict()
with open(input_path, "r") as param_file:
within_comment_block = False
curr_module = "Global"
for line in param_file:
line = line.strip()
if len(line) > 1:
line_s = line.split()

# check if within comment block.
if (not within_comment_block) and line.strip()[0:2] == "/*":
within_comment_block = True

if within_comment_block and line.strip()[-2:] == "*/":
within_comment_block = False
continue

if (
not within_comment_block and line_s[0][0] != "!"
): # not a single comment line either
# check format:
if (curr_module == "Global") and line.strip()[-1] == "%":
curr_module = line.strip()[:-1]
elif curr_module != "Global" and line.strip()[0] == "%":
curr_module = "Global"
else:
# discard override keyword if provided:
if line_s[0] == "#override" and len(line_s) > 1:
line_s = line_s[1:]
line_j = " ".join(line_s)

# now parse the line:
if re.search("^\s*\w*\s*=\s*[^ \t\n\r\f\v!]+", line_j):
eq_ix = line_j.index("=")
varname = line_j[:eq_ix].strip()
val_str = line_j[eq_ix + 1 :].strip()
if "!" in val_str:
val_str = val_str.split("!")[
0
] # discard the comment in val str, if there is

# add this module if not added before:
if not curr_module in _data:
_data[curr_module] = dict()

# check if param already provided:
if varname in _data[curr_module]:
raise SystemExit(
"ERROR: "
+ varname
+ " listed more than once in "
+ input_path
)

# enter the parameter in the dictionary:
_data[curr_module][varname] = {"value": val_str}
else:
raise SystemExit(
"ERROR: Cannot parse the following line in user_nl_mom: "
+ line
)
def get_value(self, varname, module="Global"):
"""Returns the value of a parameter, or None if the parameter is not set.

# Check if there is unclosed block:
if within_comment_block:
raise SystemExit("ERROR: faulty comment block!")
if curr_module != "Global":
raise SystemExit("ERROR: faulty module block!")
The value is returned bare, like that of a case variable, so that $VAR and
${VAR} in templates expand it as they would a case variable: stripped of
enclosing quotes, and, if it is a logical, as the string "True" or "False",
however it is spelled in the parameter file (MOM6 also accepts T, .true.,
etc.)

return _data
Parameters
----------
varname: str
Name of the parameter.
module: str
Name of the parameter block that the parameter belongs to, if any.

Example
-------
>>> params = FType_MOM_params({"Global": {"A": {"value": '"x"'}, "B": {"value": ".false."}}})
>>> params.get_value("A"), params.get_value("B"), params.get_value("C")
('x', 'False', None)
>>> FType_MOM_params({"KPP": {"N_SMOOTH": {"value": "2"}}}).get_value("N_SMOOTH", "KPP")
'2'
"""
value = self._data.get(module, {}).get(varname, {}).get("value")
if value is None:
return None
value = str(value).strip()
if value[:1] in ("'", '"') and value.find(value[0], 1) == len(value) - 1:
value = value[1:-1]
logical = re.fullmatch(r"\.?(t|true|f|false)\.?", value, re.IGNORECASE)
if logical:
value = "True" if logical.group(1).lower() in ("t", "true") else "False"
return value

def write(
self, output_path, output_format, case=None, def_params=None, inst_suffix=""
Expand Down Expand Up @@ -183,10 +143,33 @@ def expand_func(varname):
def _write_MOM_override(self, output_path, def_params):

MOM_override_header = """/* WARNING: DO NOT EDIT this file! Any user changes made in files
in RUNDIR will be overriden. This file is automatically generated.
MOM6 parameter changes may ve made via SourceMods or user_nl_mom
in RUNDIR will be overridden. This file is automatically generated.
MOM6 parameter changes may be made via SourceMods or user_nl_mom
within CASEROOT.*/\n"""

# MOM6 reads a parameter only where it expects it, i.e., outside of any block
# or within a particular block, and ignores it elsewhere. So a parameter that
# MOM_input sets elsewhere than user_nl_mom does would be silently ignored.
def scope(module):
if module == "Global":
return "outside of any block"
return f"within the {module}% block"

for module in self._data:
for var, entry in self._data[module].items():
if var in def_params.data.get(module, {}):
continue
expected = [m for m in def_params.data if var in def_params.data[m]]
if expected:
fix = f"{var} = {entry['value']} {scope('Global')}"
if expected[0] != "Global":
fix = f"{expected[0]}%{var} = {entry['value']}"
raise SystemExit(
f"ERROR: {var} is set {scope(module)} in user_nl_mom, but MOM6 "
f"expects it {scope(expected[0])}, as in MOM_input, and so "
f"would ignore it. Set it as {fix} instead."
)

with open(os.path.join(output_path), "w") as MOM_override:

MOM_override.write(MOM_override_header)
Expand Down
37 changes: 27 additions & 10 deletions cime_config/MOM_RPS/FType_diag_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
int_setting,
REQUIRED_SETTINGS,
Stream,
StreamEdit,
STREAM_SETTINGS,
write_diag_table,
)
Expand All @@ -32,16 +33,16 @@ def resolve(unresolved_diag_table_path, resolved_diag_table_path, casename):
casename : str
The casename to be resolved.
"""
assert os.path.exists(unresolved_diag_table_path), (
"Unresolved diag_table file not found: " + unresolved_diag_table_path
)
assert os.path.exists(
unresolved_diag_table_path
), f"Unresolved diag_table file not found: {unresolved_diag_table_path}"

with open(resolved_diag_table_path, "w") as resolved_diag_table:
with open(unresolved_diag_table_path, "r") as diag_table_unresolved:
for line in diag_table_unresolved:
resolved_diag_table.write(line.replace(CASENAME, casename))

def write(self, output_path, case, MOM_input_final):
def write(self, output_path, case, MOM_input_final, changes=()):
"""Writes out the diag_table of a case.

Parameters
Expand All @@ -55,14 +56,14 @@ def write(self, output_path, case, MOM_input_final):
The MOM6 parameters of the case, i.e., MOM_input updated with
MOM_override. Consulted for expandable variables that are MOM6
parameters rather than case variables.
changes : list of (stream, key, operator, value), optional
The diag_table lines of user_nl_mom (see diag_table_streams.StreamEdit).
"""

def expand_func(varname):
val = case.get_value(varname)
if val is None:
val = (
MOM_input_final.data.get("Global", {}).get(varname, {}).get("value")
)
val = MOM_input_final.get_value(varname)
if val is None:
raise DiagTableError(
"Cannot determine the value of the variable {} appearing in "
Expand All @@ -71,13 +72,20 @@ def expand_func(varname):
)
return val

edits = OrderedDict() # the changes from user_nl_mom, by stream
for stream, key, operator, value in changes:
if stream not in edits:
edits[stream] = StreamEdit(stream)
edits[stream].add(key, operator, value)

# From the general template (diag_table.yaml), reduce a custom diag_table
# for this case, and turn its file entries into streams.
self.reduce(expand_func)
write_diag_table(list(self._streams().values()), output_path)
write_diag_table(list(self._streams(edits).values()), output_path)

def _streams(self):
"""Returns the streams of this case, keyed and ordered by stream name."""
def _streams(self, edits):
"""Returns the streams of this case, as changed by the StreamEdits of
user_nl_mom, keyed and ordered by stream name."""
assert self.reduced, "May only collect streams from a reduced diag_table."
defaults = self.data.get("FileDefaults") or {}
_check_defaults(defaults)
Expand All @@ -88,11 +96,20 @@ def _streams(self):
)
streams = OrderedDict()
for label, entry in self.data["Files"].items():
edit = edits.get(entry.get("name") or label)
if edit: # the settings of an edit apply as if given in the entry
entry = dict(entry, **edit.settings)
stream = _stream_from_entry(label, entry, defaults)
if stream.name in streams:
streams[stream.name].merge(stream)
else:
streams[stream.name] = stream
for name, edit in edits.items():
if name not in streams: # a stream that user_nl_mom adds
edit.check_new(list(streams))
streams[name] = _stream_from_entry(name, edit.settings, defaults)
streams[name].labels = []
edit.apply_fields(streams[name])
return streams


Expand Down
2 changes: 1 addition & 1 deletion cime_config/MOM_RPS/FType_input_data_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def write(self, output_path, case, MOM_input_final=None):
def expand_func(varname):
val = case.get_value(varname)
if val is None:
val = MOM_input_final.data["Global"][varname]["value"]
val = MOM_input_final.get_value(varname)
if val is None:
raise RuntimeError("Cannot determine the value of variable: " + varname)
return val
Expand Down
5 changes: 5 additions & 0 deletions cime_config/MOM_RPS/FType_input_nml.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,10 @@ def write(self, output_path, case):
# From the general template (input_nml.yaml), reduce a custom input.nml for this case
self.reduce(lambda varname: case.get_value(varname))

# Drop the variables whose guards are all false, rather than writing them as None
for group in self._data.values():
for var in [var for var, entry in group.items() if entry["values"] is None]:
del group[var]

# write the data in namelist format
self.write_nml(output_path)
Loading
Loading