#!/usr/bin/env python3
r"""
Generates a list of species that differ between versions, that
can be printed on the GEOS-Chem wiki.
Example
-------
.. code-block:: bash
$ conda activate gcpy_env
$ python gcpy.benchmark.modules.benchmark_species_changes \
--ref-label "14.4.0" \
--ref-log "gcc_14.4.0/14.4.0.log" \
--dev-label "14.5.0" \
--dev-log "gcc_14.5.0/14.5.0.log" \
--spcdb-files ["gcc_14.4.0/species_database.yml", \
"gcc_14.5.0/species_database.yml"], \
--output-file "wiki_tables.txt"
"""
from os.path import exists
import argparse
import pandas as pd
from gcpy.util import read_species_metadata, verify_variable_type
[docs]
def read_one_log_file(log_file):
"""
Parses the GEOS-Chem Classic log file (plain text) with
timing information and returns a dictionary with the results.
Parameters
----------
log_file : str
GEOS-Chem log file.
Returns
-------
species : dict
Dictionary with species metadata.
"""
keep_line = False
species = {}
keys = []
# Make sure file exists
if not exists(log_file):
raise FileNotFoundError(f"Could not find {log_file}!")
# Read the log file and just keep species information
with open(log_file, encoding="utf-8") as ifile:
for line in list(ifile):
line = line.strip("\n")
# Set a flag to denote the start of species info
if "SPECIES NAMES AND INDICES" in line:
keep_line = True
continue
# Append species info lines into a list
if keep_line:
# Break out of the loop after the species info section
if "==========" in line:
keep_line = False
break
# Skip hard rule and/or empty lines
if "----------" in line or len(line) == 0:
continue
# Get the list of columns
if "Name" in line:
keys = line.split()
continue
# Add each species
substr = line.split()
for idx in range(1, 7):
substr[idx] = not "-" in substr[idx]
species[substr[0]] = dict(zip(keys[1:7], substr[1:7]))
return species
[docs]
def append_keys(species, species_database, keys):
"""
Copies dictionary keys from the species database
to the existing dictionary.
Parameters
----------
species : dict
Dictionary w/ GEOS-Chem metadata.
species_database : dict
GEOS-Chem species database.
keys : list
Keys in species_database to append to species.
Returns
-------
species : dict
Updated dictionary with appended keys.
"""
for key in keys:
if key in species_database:
if "Formula" in key or "FullName" in key:
species[key] = species_database[key]
else:
species[key] = bool(species_database[key])
else:
species[key] = False
return species
[docs]
def bool_to_str(val):
"""
Converts a boolean True value to an "X" for printing
in the wiki table.
Parameters
----------
val : bool
Boolean value to test.
"""
string = ""
if val:
string = "X"
return string
[docs]
def write_wiki_row(species, ofile):
"""
Prints metadata for a given GEOS-Chem species.
Parameters
----------
species : pd.Series
GEOS-Chem species metadata.
ofile : file
File object for output file.
"""
line = "|-valign='top'\n"
line += f"|{species.name}\n"
line += f"|{species['Formula']}\n"
line += f"|{species['FullName']}\n"
line += f"|{bool_to_str(species['Is_Advected'])}\n"
line += f"|{bool_to_str(species['DryDepId'])}\n"
line += f"|{bool_to_str(species['Is_Gas'])}\n"
line += f"|{bool_to_str(species['PhotolId'])}\n"
line += f"|{bool_to_str(species['WetDepId'])}\n"
line += " "
print(line, file=ofile)
[docs]
def create_table(keys, species, ofile):
"""
Creates a wiki table containing selected species.
Parameters
----------
keys : list
Names of species to include in table.
species : pd.DataFrame
Species metadata.
ofile : io.TextIOWrapper
Output file handle.
"""
write_wiki_table_header(ofile)
for key in keys:
write_wiki_row(species[key], ofile)
write_wiki_table_footer(ofile)
print(" ", file=ofile)
[docs]
def check_for_species_changes(species, ref, dev):
"""
Prints a list of species with attributes that have changed
between the Ref and Dev versions.
Parameters
----------
species : list
List of species in both Ref and Dev.
ref : pd.DataFrame
Species metadata for the Ref version.
dev : pd.DataFrame
Species metadata for the Dev version.
"""
changed = {}
keys = dev.index.tolist()
# Search for species with changed attributes between versions
for spc in species:
for key in keys:
if ref[spc][key] != dev[spc][key]:
result = {key: {"Ref": ref[spc][key], "Dev": dev[spc][key]}}
if spc in changed:
changed[spc].update(result)
else:
changed[spc] = result
# Print results
print("Species with changes (also check advected species)")
for (key, value) in changed.items():
print(key, value)
[docs]
def make_benchmark_species_changes_wiki_tables(
ref_label,
ref_log,
dev_label,
dev_log,
spcdb_files,
output_file,
):
"""
Creates tables of species that have been added and removed
between Ref and Dev versions.
Parameters
----------
ref_label : str
Label for the Ref version.
ref_log : str
Path to log file for the Ref version.
dev_label : str
Label for the Dev version.
dev_log : str
Path to log file for the Dev version.
spcdb_files : list
Paths to Ref & Dev species_database.yml files.
output_file : str
Path to file with generated wiki tables.
"""
verify_variable_type(ref_label, str)
verify_variable_type(ref_log, str)
verify_variable_type(dev_label, str)
verify_variable_type(dev_log, str)
verify_variable_type(spcdb_files, list)
# Read the species database files in the Ref & Dev rundirs, and
# return a dict containing metadata for the union of species.
# We'll need properties such as mol. wt. for unit conversions, etc.
species_database = read_species_metadata(spcdb_files, quiet=True)
# Get species metadata for the Ref and Dev versions
ref = get_species_metadata(ref_log, species_database)
dev = get_species_metadata(dev_log, species_database)
# Get list of species in Ref, Dev and both Ref & Dev
in_ref = set(list(ref.columns))
in_dev = set(list(dev.columns))
in_both = sorted(list(in_dev & in_ref))
# Lists of species that were added and removed
added = sorted(list(in_dev - in_ref))
removed = sorted(list(in_ref - in_dev))
# Also print list of species in both Ref & Dev
# to make the table manually
check_for_species_changes(in_both, ref, dev)
# Create the wiki tables
with open(output_file, "w", encoding="utf-8") as ofile:
print("=== Species added ===\n", file=ofile)
print(
f"Species added between versions {ref_label} and {dev_label}:\n",
file=ofile
)
create_table(added, dev, ofile)
print("=== Species removed===\n", file=ofile)
print(
f"Species removed between versions {ref_label} and {dev_label}:\n",
file=ofile
)
create_table(removed, ref, ofile)
[docs]
def main():
"""
Parses command-line arguments.
"""
parser = argparse.ArgumentParser(
description="benchmark_species_changes.py"
)
parser.add_argument(
"--ref-label",
metavar="REF_LABEL",
type=str,
required=True,
default="GCC_ref",
help="Label for the Ref version"
)
parser.add_argument(
"--ref-log",
metavar="REF_LOG",
type=str,
required=True,
help="Log file from the Ref version"
)
parser.add_argument(
"--dev-label",
metavar="DEV_LABEL",
type=str,
required=True,
default="GCC_dev",
help="Label for the Dev version"
)
parser.add_argument(
"--dev-log",
metavar="DEV_LOG",
type=str,
required=True,
help="Log file from the Dev version"
)
parser.add_argument(
"--spcdb-files",
metavar="SPCDB_FILES",
type=list,
required=True,
default="./",
help="Paths to the Ref & Dev species_database.yml files"
)
parser.add_argument(
"-o", "--output-file",
metavar="OUTPUT_FILE",
type=str,
required=True,
default="wiki_tables.txt",
help="File where the wiki table will be written"
)
args = parser.parse_args()
make_benchmark_species_changes_wiki_tables(
args.ref_label,
args.ref_log,
args.dev_label,
args.dev_log,
args.spcdb_files,
args.output_file
)
if __name__ == '__main__':
main()