"""
``esm-runscripts`` Core Plugins for dealing with Fortran Namelists.
Provides plugins for loading, modifying, deleting, and writing Fortran
Namelists as part of the ``esm-runscripts`` recipe. All plugins are found under
the class Namelist as static methods. A deprecated class ``namelist`` (small "n") is
provided, which warns you when it is used.
"""
import logging
import os
import sys
import warnings
import f90nml
import six
[docs]class Namelist:
"""Methods for dealing with FORTRAN namelists"""
[docs] @staticmethod
def nmls_load(mconfig):
"""
Loads Fortran namelists into the configuration dictionary.
User Information
----------------
To associate namelists with a specific model, you should have a section
in your configuration that lists the namelists::
fesom:
namelists:
- "namelist.config"
- "namelist.oce"
- "namelist.ice"
- "namelist.diag"
Programmer Information
----------------------
The namelists are represented by f90nml Namelist objects, and are
stored under::
mconfig["namelists"]["namelist.echam"]``
This would point to the ECHAM namelist as a f90nml object, which
closely resembles a dictionary.
The actual namelists to load are listed in the raw configuration as a
list of strings::
mconfig['namelists'] = ['nml1', 'nml2', 'nml3', ...]
Namelists are assumed to have been copied to
``mconfig["thisrun_config_dir"]``, and are loaded from there.
If the ``mconfig`` has a key ``"namelist_case"`` equal to "uppercase",
the uppercase attribute of the f90nml representation of the namelist is
set to ``True``.
Parameters
----------
mconfig : dict
The model (e.g. ECHAM, FESOM, NEMO or OIFS) configuration
Returns
-------
mconfig : dict
The modified configuration.
"""
nmls = mconfig.get("namelists", [])
mconfig["namelists"] = dict.fromkeys(nmls)
for nml in nmls:
if os.path.isfile(os.path.join(mconfig["thisrun_config_dir"], nml)):
logging.debug("Loading %s", nml)
mconfig["namelists"][nml] = f90nml.read(
os.path.join(mconfig["thisrun_config_dir"], nml)
)
else:
mconfig["namelists"][nml] = f90nml.namelist.Namelist()
if mconfig.get("namelist_case") == "uppercase":
mconfig["namelists"][nml].uppercase = True
return mconfig
[docs] @staticmethod
def nmls_remove(mconfig):
"""
Removes an element from a namelist chapter.
User Information
----------------
In the configuration file, assume you have::
echam:
namelist_changes:
namelist.echam:
radctl:
co2vmr: "remove_from_namelist"
In this case, the entry co2vmr would be deleted from the radctl section
of namelist.echam.
Programmer Information
----------------------
IDEA(PG): Maybe we can provide examples of how these functions are used
in the code?
Parameters
----------
mconfig : dict
The model (e.g. ECHAM, FESOM, NEMO or OIFS) configuration
Returns
-------
mconfig : dict
The modified configuration.
"""
namelist_changes = mconfig.get("namelist_changes", {})
namelist_removes = []
for namelist in list(namelist_changes):
changes = namelist_changes[namelist]
logging.debug("Determining remove entires for %s", namelist)
logging.debug("All changes: %s", changes)
for change_chapter in list(changes):
change_entries = changes[change_chapter]
for key in list(change_entries):
value = change_entries[key]
if value == "remove_from_namelist":
namelist_removes.append((namelist, change_chapter, key))
del namelist_changes[namelist][change_chapter][key]
for remove in namelist_removes:
namelist, change_chapter, key = remove
logging.debug("Removing from %s: %s, %s", namelist, change_chapter, key)
if key in mconfig["namelists"][namelist][change_chapter]:
del mconfig["namelists"][namelist][change_chapter][key]
return mconfig
[docs] @staticmethod
def nmls_modify(mconfig):
"""
Performs namelist changes.
User Information
----------------
In the configuration file, you should have a section as::
echam:
namelist_changes:
namelist.echam:
radctl:
co2vmr: 1200e-6
This would change the value of the echam namelist (namelist.echam),
subsection radctl, entry co2vmr to the value 1200e-6.
Programmer Information
----------------------
IDEA(PG): Maybe we can provide examples of how these functions are used
in the code?
Note
----
Actual changes are performed by the f90nml package patch fuction. See
here: https://tinyurl.com/y4ydz363
Parameters
----------
mconfig : dict
The model (e.g. ECHAM, FESOM, NEMO or OIFS) configuration
Returns
-------
mconfig : dict
The modified configuration.
"""
namelist_changes = mconfig.get("namelist_changes", {})
for namelist, changes in six.iteritems(namelist_changes):
mconfig["namelists"][namelist].patch(changes)
return mconfig
[docs] @staticmethod
def apply_echam_disturbance(config):
"""
Applies a disturbance to the DYNCTL chapter of the echam namelist via the enstdif
Relevant configuration entries:
* disturbance_years (list of int): Which year to apply the disturbance
* distrubance (float): Value to apply. Default can be found in echam.yaml
"""
if "echam" in config["general"]["valid_model_names"]:
# Get the echam namelist:
nml = config["echam"]["namelists"]["namelist.echam"]
# Get the current dynctl chapter or make a new empty one:
dynctl = nml.get("dynctl", f90nml.namelist.Namelist())
# Determine which years the user wants to have disturbed:
if os.path.isfile(
config["general"]["experiment_scripts_dir"] + "/disturb_years.dat"
):
with open(
config["general"]["experiment_scripts_dir"] + "/disturb_years.dat"
) as f:
disturbance_file = [
int(line.strip()) for line in f.readlines() if line.strip()
]
if config["general"]["verbose"]:
print(disturbance_file)
else:
disturbance_file = None
if config["general"]["verbose"]:
print("WARNING: "
+ config["general"]["experiment_scripts_dir"]
+ "/disturb_years.dat",
"was not found",
)
disturbance_years = disturbance_file or config["echam"].get(
"disturbance_years", []
)
current_year = config["general"]["current_date"].year
if current_year in disturbance_years:
print("-------------------------------------------------------")
print("")
print(" > Applying disturbance in echam namelist!")
print("")
print("-------------------------------------------------------")
dynctl["enstdif"] = config["echam"].get("disturbance", 1.000001)
nml["dynctl"] = dynctl
else:
if config["general"]["verbose"]:
print("Not applying disturbance in echam namelist.")
print(
"Current year",
current_year,
"disturbance_years",
disturbance_years,
)
return config
[docs] @staticmethod
def nmls_finalize(mconfig, verbose):
"""
Writes namelists to disk after all modifications have finished.
User Information
----------------
Part of the main log output will be a section specifing the actual
namelists that have been used for your simulation, including all
relevant additions, removals, or changes.
Programmer Information
----------------------
A copy of the f90nml object representations of the namelists is stored
under the dictionary key "namelist_objs", as a dictionary of
("namelist_name", f90nml_objfect) key/value pairs.
Warning
-------
Removing this step from your recipe might result in a broken run,
as the namelists will not be present in their desired form! Even if
your model runs, it might not contain all user-required changes.
Parameters
----------
mconfig : dict
The model (e.g. ECHAM, FESOM, NEMO or OIFS) configuration
Returns
-------
mconfig : dict
The modified configuration.
"""
all_nmls = {}
for nml_name, nml_obj in six.iteritems(mconfig.get("namelists", {})):
with open(
os.path.join(mconfig["thisrun_config_dir"], nml_name), "w"
) as nml_file:
nml_obj.write(nml_file)
all_nmls[nml_name] = nml_obj # PG: or a string representation?
mconfig["namelist_objs"] = all_nmls
if verbose:
mconfig = Namelist.nmls_output(mconfig)
return mconfig
[docs] @staticmethod
def nmls_output(mconfig):
all_nmls = {}
for nml_name, nml_obj in six.iteritems(mconfig.get("namelists", {})):
all_nmls[nml_name] = nml_obj # PG: or a string representation?
for nml_name, nml in all_nmls.items():
message = f'\nFinal Contents of {nml_name}:'
six.print_(message)
six.print_(len(message) * '-')
nml.write(sys.stdout)
print('-' * 80)
print(f'::: end of the contents of {nml_name}\n')
return mconfig
[docs] @staticmethod
def nmls_output_all(config):
six.print_(
"\n" "- Namelists modified according to experiment specifications..."
)
for model in config["general"]["valid_model_names"]:
config[model] = nmls_output(config[model], config["general"]["verbose"])
return config
[docs]class namelist(Namelist):
"""Legacy class name. Please use Namelist instead!"""
def __init__(self, *args, **kwargs):
warnings.warn(
"Please change your code to use Namelist!",
DeprecationWarning,
stacklevel=2,
)
super(namelist, self).__init__(*args, **kwargs)