Skip to content

Commit 0c8bb57

Browse files
author
Steve Goldhaber
committed
Ready for trial run
1 parent deca869 commit 0c8bb57

3 files changed

Lines changed: 166 additions & 87 deletions

File tree

tools/cmip_namelists/README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ variables, e.g., `nhtfrq`.
2121

2222
To run the script, first download a current version of the [CMIP7 data
2323
request
24-
spreadsheet](https://docs.google.com/spreadsheets/d/1XUdCTl1zKsWi_yTvMZqsMtBnnUPIhIeTPVSLHgL0Hdo) and the separate [CAM field request spreadsheet]()
24+
spreadsheet](https://docs.google.com/spreadsheets/d/1XUdCTl1zKsWi_yTvMZqsMtBnnUPIhIeTPVSLHgL0Hdo) and the separate [CAM field request spreadsheet](https://docs.google.com/spreadsheets/d/1-ohp9nwJ5BUKQ7qxubg_PD1bVMWMMz00bS-MQ-z30p8/edit?gid=0#gid=0)
2525
(File `==>` Download `==>` .csv). The script is then run as:
2626

2727
```
@@ -32,3 +32,9 @@ For the full interface (with options), see the help menu:
3232
```
3333
usage: cmip_diagnostic_namelists.py [--help]
3434
```
35+
36+
To use this script, the following workflow is recommended:
37+
1. Use `git rm` to remove existing usermods files (or use `--overwrite` and only remove files no longer in use).
38+
2. Run script to generate new usermods files
39+
3. Use `git add` to add any new usermods files
40+
4. Use `git commit -a` to commit all the new usermods files. Note that git will only show untracked directory names if none of the files in that directory are currently in the repository.

tools/cmip_namelists/cmip_diagnostic_namelists.py

Lines changed: 155 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,11 @@
3535
class Usermod():
3636
"""Class to hold information about a history usermod directory"""
3737

38-
def __init__(self, name, frequencies, dependencies=None,
39-
include_cosp=False, include_aerocom=False):
38+
def __init__(self, name, dirname, frequencies, usermods_dir,
39+
dependencies=None, include_cosp=False, include_aerocom=False):
4040
"""Initialize a history usermod section"""
4141
self.__name = name
42+
self.__dirname = os.path.normpath(os.path.join(usermods_dir, dirname))
4243
self.__freqset = set([x.strip() for x in frequencies.split(',')])
4344
if dependencies:
4445
self.__depends = [x.strip() for x in dependencies.split(',')]
@@ -50,27 +51,32 @@ def __init__(self, name, frequencies, dependencies=None,
5051

5152
def unique_frequencies(self, usermod_dict):
5253
"""Return the output frequencies not covered by this section's dependencies.
53-
It is an error if any dependency is not in usermod_dict.
54-
It is an error if any frequency in a dependency is not in this section's dependencies."""
54+
"""
5555
my_freqs = self.frequencies
5656
for dependency in self.dependencies:
57-
if dependency in usermod_dict:
58-
depmod = usermod_dict[dependency]
59-
if depmod.frequencies - self.frequencies:
60-
raise ValueError(f"Section, '{dependency}' has frequencies not in '{self.name}'")
61-
# end if
62-
my_freqs -= depmod.frequencies
63-
else:
64-
raise ValueError(f"Dependency, '{dependency}', from {self.name}, not found")
65-
# end if
57+
depmod = usermod_dict[dependency]
58+
my_freqs -= depmod.frequencies
6659
# end for
67-
return my_freqs
60+
return sorted(my_freqs, key=lambda x: _HIST_FILEORDER.index(x))
61+
62+
def namelist_file(self):
63+
"""Construct and return the namelist filename for this object"""
64+
return os.path.join(self.dirname, "user_nl_cam")
65+
66+
def include_file(self):
67+
"""Construct and return the include usermods filename for this object"""
68+
return os.path.join(self.dirname, "include_user_mods")
6869

6970
@property
7071
def name(self):
7172
"""Return the name for this object"""
7273
return self.__name
7374

75+
@property
76+
def dirname(self):
77+
"""Return the usermod subdirectory name for this object"""
78+
return self.__dirname
79+
7480
@property
7581
def frequencies(self):
7682
"""Return the frequencies for this object"""
@@ -127,13 +133,17 @@ def command_line(args):
127133
help="""Stop processing if any missing fields found.
128134
Default is to produce fieldlist files by ignoring any
129135
missing fields.""")
136+
parser.add_argument("--max-line", type=int, default=80,
137+
help="Maximum line length for namelist files")
130138
pargs = parser.parse_args(args)
131139
return (pargs.CMIP_file, pargs.CAM_file, pargs.usermods, pargs.cfgfile,
132-
pargs.overwrite, pargs.error_on_missing)
140+
pargs.overwrite, pargs.error_on_missing, pargs.max_line)
133141

134-
def read_config_file(filename):
142+
def read_config_file(filename, usermods_dir, overwrite):
135143
"""Read a fincl group configuration (ini-style) file.
136-
Returns a dictionary of usermods sections with the section name as the key"""
144+
Returns a dictionary of usermods sections with the section name as the key.
145+
If any errors are found, print and return None"""
146+
errors = False
137147
usermod_dict = {}
138148
config = configparser.ConfigParser()
139149
config.read(filename)
@@ -143,6 +153,7 @@ def read_config_file(filename):
143153
use_cosp = None
144154
include_aerocom = None
145155
frequencies = cfg_sect['frequencies']
156+
dirname = cfg_sect['usermod_dir']
146157
if 'include_usermods' in cfg_sect:
147158
depends = cfg_sect['include_usermods']
148159
# end if
@@ -152,29 +163,66 @@ def read_config_file(filename):
152163
if 'use_aercom' in cfg_sect:
153164
include_aerocom = cfg_sect['use_aercom']
154165
# end if
155-
if section in usermod_dict:
156-
raise ValueError("Duplicate section, '{section}'")
157-
# end if
158-
usermod_dict[section] = Usermod(section, frequencies,
166+
usermod_dict[section] = Usermod(section, dirname, frequencies,
167+
usermods_dir,
159168
dependencies=depends,
160169
include_cosp=use_cosp,
161170
include_aerocom=include_aerocom)
162171
# end for
163-
return usermod_dict
172+
# Check for errors
173+
for name, usermod in usermod_dict.items():
174+
if name in usermod_dict:
175+
print(f"Duplicate section, '{name}'")
176+
errors = True
177+
# end if
178+
if not overwrite:
179+
path = usermod.namelist_file()
180+
if os.path.exists(path):
181+
print(f"namelist file, '{path}' exists and overwrite = False")
182+
errors = True
183+
# end if
184+
# end if
185+
if not overwrite and usermod.dependencies:
186+
path = usermod.include_file()
187+
if os.path.exists(path):
188+
print(f"include file, '{path}' exists and overwrite = False")
189+
errors = True
190+
# end if
191+
# end if
192+
for depend in usermod.dependencies:
193+
if depend == usermod.name:
194+
print(f"Invalid dependency, {depend} in section {depend}")
195+
errors = True
196+
# end if
197+
# It is an error if any dependency is not in usermod_dict.
198+
if depend not in usermod_dict:
199+
print(f"Invalid dependency, '{depend}' not in config file")
200+
errors = True
201+
# end if
202+
# end for
203+
# Check frequencies
204+
if not usermod.frequencies:
205+
print(f"Section, '{name}', contains no output frequencies")
206+
errors = True
207+
elif if any([x not in _HIST_FILEORDER for x in usermod.frequencies]):
208+
unknown = list(set(usermod.frequencies) - set(_HIST_FILEORDER))
209+
freq = ', '.join(unknown)
210+
print(f"Section, '{section}', contains unknown frequencies: {freq}")
211+
errors = True
212+
else:
213+
for dependency in self.dependencies:
214+
depmod = usermod_dict[dependency]
215+
if depmod.frequencies - usermod.frequencies:
216+
print(f"Section, '{dependency}' has frequencies not in '{name}'")
217+
errors = True
218+
# end if
164219

165-
@contextlib.contextmanager
166-
def flex_open(filename=None, mode='w'):
167-
if filename and filename != '-':
168-
fh = open(filename, mode)
169-
else:
170-
fh = sys.stdout
220+
# end if
221+
# end for
222+
if errors:
223+
return None
171224
# end if
172-
173-
try:
174-
yield fh
175-
finally:
176-
if fh is not sys.stdout:
177-
fh.close()
225+
return usermod_dict
178226

179227
def read_fieldname_file(filename):
180228
"""Read a fieldname file and return all fieldnames as a list."""
@@ -260,10 +308,11 @@ def parse_spreadsheet(csvfile, model_name="atmos"):
260308
# end for
261309
return cmip_dict
262310

263-
def check_for_missing_fieldnames(masterlist, data_request):
311+
def check_for_missing_fieldnames(masterlist, data_request, request_name):
264312
"""Given a data request dictionary (<data_request>),
265313
check to see if any are not in <masterlist>.
266-
Return a list of missing fields names (an empty list means none).
314+
Return a set of missing fields names (an empty list means none).
315+
Print out any missing fields.
267316
Clean <data_request> to remove missing field entries (side effect)."""
268317
# Gather the set of all fields (combine different frequencies)
269318
all_reqfields = set()
@@ -281,65 +330,86 @@ def check_for_missing_fieldnames(masterlist, data_request):
281330
for field in sorted(missing):
282331
print(f" {field}")
283332
# end for
333+
print(f"These fields were found in the {request_name} data request spreadsheet")
284334
# end if
285335
return missing
286336

287-
def generate_namelist_entries(data_request, nl_filename, maxline=125, hist_files=_HIST_FILEORDER):
288-
"""Write the set of namelist entries represented by <data_request> to
289-
<nl_filename> (which may be standard output)."""
290-
lbreak = ''
291-
with flex_open(nl_filename, mode="w") as outfile:
292-
for index, freq in enumerate(hist_files):
293-
if freq in data_request:
294-
if freq == 'subhr':
295-
avgflag = 'I'
296-
else:
297-
avgflag = 'A'
298-
# end if
299-
# Write history file config info
300-
outfile.write(f"{lbreak}{_HIST_TITLES[freq]}\n")
301-
outfile.write(f"nhtfrq({index + 1}) = {_HIST_FRQCODES[freq]}\n")
302-
outfile.write(f"mfilt({index + 1}) = {_HIST_MFILT[freq]}\n")
303-
outfile.write(f"empty_htapes({index + 1}) = .true.\n")
304-
fields = data_request[freq]
305-
fldstring = ', '.join([f"{x}:{avgflag}" for x in fields])
306-
nlstr = f"fincl{index + 1} = {fldstring}"
307-
# Write the fincl string with appropriate line breaks
308-
begpos = 0
309-
strlen = len(nlstr)
310-
while begpos < strlen:
311-
endpos = strlen
312-
if endpos - begpos > maxline:
313-
endpos = nlstr[0:begpos + maxline].rfind(' ')
314-
if endpos < begpos:
315-
endpos = strlen
316-
# end if
337+
def generate_namelist_entries(data_request, usermod_config, maxline):
338+
"""Write the sets of namelist entries represented by <data_request> to
339+
the usermods files defined in <usermod_config>."""
340+
for usermod in usermod_config.values():
341+
lbreak = ''
342+
if not os.path.exists(usemod.dirname):
343+
os.makedirs(usemod.dirname)
344+
# end if
345+
with open(usermod.namelist_file(), mode="w") as outfile:
346+
for freq in usermod.unique_frequencies(usermod_config)
347+
if freq in data_request:
348+
# index is the fincl number for this frequency
349+
index = _HIST_FILEORDER.index(freq) + 1
350+
if freq == 'subhr':
351+
avgflag = 'I'
352+
else:
353+
avgflag = 'A'
317354
# end if
318-
outfile.write(f"{nlstr[begpos:endpos]}\n")
319-
begpos = endpos
320-
# end while
355+
# Write history file config info
356+
outfile.write(f"{lbreak}{_HIST_TITLES[freq]}\n")
357+
outfile.write(f"nhtfrq({index}) = {_HIST_FRQCODES[freq]}\n")
358+
outfile.write(f"mfilt({index}) = {_HIST_MFILT[freq]}\n")
359+
outfile.write(f"empty_htapes({index}) = .true.\n")
360+
fields = data_request[freq]
361+
fldstring = ', '.join([f"{x}:{avgflag}" for x in fields])
362+
nlstr = f"fincl{index} = {fldstring}"
363+
# Write the fincl string with appropriate line breaks
364+
begpos = 0
365+
strlen = len(nlstr)
366+
while begpos < strlen:
367+
endpos = strlen
368+
if endpos - begpos > maxline:
369+
endpos = nlstr[0:begpos + maxline].rfind(' ')
370+
if endpos < begpos:
371+
endpos = strlen
372+
# end if
373+
# end if
374+
outfile.write(f"{nlstr[begpos:endpos]}\n")
375+
begpos = endpos
376+
# end while
377+
# end if
378+
lbreak = '\n'
379+
# end for
321380
# end if
322-
lbreak = '\n'
323-
# end for
324-
# Now, write out combined namelist items
325-
# end with
326-
381+
# end with (open file)
382+
# Write the include usermods file (if any)
383+
if usermod.dependencies:
384+
with open(username.include_file, mode="w") as outfile:
385+
for depend in usermod.dependencies:
386+
depdir = os.path.basename(usermod_config[depend].dirname)
387+
outfile.write(f"{os.path.join(os.pardir, depdir)}\n")
388+
# end for
389+
# end for
390+
# end if
391+
# end for (sections)
327392

328393
###############################################################################
329394

330395
if __name__ == "__main__":
331396
arglist = command_line(sys.argv[1:])
332-
cmipfile, camfile, usermods_dir, configfile, overwrite, error = arglist
397+
cmipfile, camfile, usermods, configfile, overwrite, error, maxline = arglist
333398
# read configuration
334-
usermod_dict = read_config_file(configfile)
335-
all_fieldnames, cosp_fieldnames, aerocom_fieldnames = read_diagnostic_fieldnames()
336-
cmip7_request = parse_spreadsheet(cmipfile)
337-
missing7 = check_for_missing_fieldnames(all_fieldnames, cmip7_request, "CMIP7")
338-
cam_request = parse_spreadsheet(camfile)
339-
missingc = check_for_missing_fieldnames(all_fieldnames, cmip7_request, "CAM")
340-
if error and (missing7 or missingc):
341-
print("Missing fields found, not producing any namelist usermods files")
342-
# else:
343-
# generate_namelist_entries(data_request, , maxline=80)
399+
usermod_dict = read_config_file(configfile, usermods, overwrite)
400+
if usermod_dict:
401+
fieldnames = read_diagnostic_fieldnames()
402+
all_fieldnames, cosp_fieldnames, aerocom_fieldnames = fieldnames
403+
cmip7_request = parse_spreadsheet(cmipfile)
404+
missing7 = check_for_missing_fieldnames(all_fieldnames, cmip7_request,
405+
"CMIP7")
406+
cam_request = parse_spreadsheet(camfile)
407+
missingc = check_for_missing_fieldnames(all_fieldnames, cmip7_request,
408+
"CAM")
409+
if error and (missing7 or missingc):
410+
print("Missing fields found, not producing any namelist usermods files")
411+
else:
412+
generate_namelist_entries(data_request, usermod_dict, maxline)
413+
# end if
344414
# end if
345415
sys.exit(0)

tools/cmip_namelists/usermods_sets.cfg

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
## Frequencies and usermod_dir are required
44
## Frequencies are mon, day, 6hr, 3hr, 1hr, subhr
55

6-
## include_usermods is the name of a config section (default: none)
6+
## include_usermods is a comma-separated list of names of config sections
7+
## (default: none)
8+
## Each config section must be present in this file
9+
710
## COSP_on and use_aerocom default to False
811

912
[reduced]

0 commit comments

Comments
 (0)