diff --git a/CITATION.cff b/CITATION.cff index 947d0122..e8f8e725 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,7 +1,7 @@ cff-version: 1.2.0 message: If you use this software, please cite it as below. title: MusicBox -version: v2.5.0 +version: v2.5.5 authors: - family-names: Dawson given-names: Matthew diff --git a/pyproject.toml b/pyproject.toml index 6f7ae321..1f59b3d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,9 @@ dependencies = [ "colorlog", "pandas", "tqdm", - "netcdf4" + "netcdf4", + "matplotlib", + "mplcursors" ] [project.urls] diff --git a/src/acom_music_box/__init__.py b/src/acom_music_box/__init__.py index f3eed4ed..898a338d 100644 --- a/src/acom_music_box/__init__.py +++ b/src/acom_music_box/__init__.py @@ -4,7 +4,7 @@ This package contains modules for handling various aspects of a music box, including species, products, reactants, reactions, and more. """ -__version__ = "2.5.4" +__version__ = "2.5.5" from .utils import convert_time, convert_pressure, convert_temperature, convert_concentration from .model_options import BoxModelOptions diff --git a/src/acom_music_box/examples/configs/ts1/my_config.json b/src/acom_music_box/examples/configs/ts1/my_config.json index bbb1504c..1910ee32 100644 --- a/src/acom_music_box/examples/configs/ts1/my_config.json +++ b/src/acom_music_box/examples/configs/ts1/my_config.json @@ -411,6 +411,15 @@ }, "TEPOMUC": { "initial value [mol m-3]": 1.86716E-11 + }, + "O2": { + "initial value [mol m-3]": 8.368900382 + }, + "N2": { + "initial value [mol m-3]": 31.08448713 + }, + "H2O": { + "initial value [mol m-3]": 1.195557197 } }, "environmental conditions": { diff --git a/src/acom_music_box/main.py b/src/acom_music_box/main.py index f53010c1..6108dafb 100644 --- a/src/acom_music_box/main.py +++ b/src/acom_music_box/main.py @@ -6,6 +6,8 @@ import subprocess import sys import tempfile +import matplotlib.pyplot as plt +import mplcursors from acom_music_box import MusicBox, Examples, __version__ @@ -55,6 +57,13 @@ def parse_arguments(): type=str, help='Plot a comma-separated list of species if gnuplot is available (e.g., CONC.A,CONC.B).' ) + parser.add_argument( + '--plot-tool', + type=str, + choices=['gnuplot', 'matplotlib'], + default='matplotlib', + help='Choose plotting tool: gnuplot or matplotlib (default: matplotlib).' + ) return parser.parse_args() @@ -119,6 +128,33 @@ def plot_with_gnuplot(data, species_list): os.remove(data_file_path) +def plot_with_matplotlib(data, species_list): + # Prepare columns and data for plotting + indexed = data.set_index('time') + + fig, ax = plt.subplots() + indexed[species_list].plot(ax=ax) + + ax.set(xlabel='Time [s]', ylabel='Concentration [mol m-3]', title='Time vs Species') + + ax.spines[:].set_visible(False) + ax.spines['left'].set_visible(True) + ax.spines['bottom'].set_visible(True) + + ax.grid(alpha=0.5) + ax.legend() + + # Enable interactive data cursors with hover functionality + cursor = mplcursors.cursor(hover=True) + + # Customize the annotation format + @cursor.connect("add") + def on_add(sel): + sel.annotation.set_text(f'Time: {sel.target[0]:.2f}\nConcentration: {sel.target[1]:1.2e}') + + plt.show() + + def main(): start = datetime.datetime.now() @@ -159,8 +195,10 @@ def main(): print(result.to_csv(index=False)) if plot_species_list: - # Prepare data for plotting - plot_with_gnuplot(result, plot_species_list) + if args.plot_tool == 'gnuplot': + plot_with_gnuplot(result, plot_species_list) + elif args.plot_tool == 'matplotlib': + plot_with_matplotlib(result, plot_species_list) end = datetime.datetime.now() logger.info(f"End time: {end}") diff --git a/src/acom_music_box/tools/waccmToMusicBox.py b/src/acom_music_box/tools/waccmToMusicBox.py index 8b438792..db1f1f02 100644 --- a/src/acom_music_box/tools/waccmToMusicBox.py +++ b/src/acom_music_box/tools/waccmToMusicBox.py @@ -220,6 +220,17 @@ def readWACCM(waccmMusicaDict, latitude, longitude, return (musicaDict) +# Add molecular Nitrogen, Oxygen, and Argon to dictionary. +# varValues = already read from WACCM, contains (name, concentration, units) +# return varValues with N2, O2, and Ar added +def addStandardGases(varValues): + varValues["N2"] = ("N2", 0.78084, "mol/mol") # standard fraction by volume + varValues["O2"] = ("O2", 0.20946, "mol/mol") + varValues["Ar"] = ("Ar", 0.00934, "mol/mol") + + return (varValues) + + # set up indexes for the tuple musicaIndex = 0 valueIndex = 1 @@ -268,7 +279,7 @@ def writeInitCSV(initValues, filename): else: fp.write(",") - fp.write(key) + fp.write("{} [{}]".format(key, value[unitIndex])) fp.write("\n") # write the variable values @@ -404,9 +415,19 @@ def main(): if ("longitude" in myArgs): lon = safeFloat(myArgs.get("longitude")) + # get the requested (diagnostic) output + outputCSV = False + outputJSON = False + if ("output" in myArgs): + # parameter is like: output=CSV,JSON + outputFormats = myArgs.get("output").split(",") + outputFormats = [lowFormat.lower() for lowFormat in outputFormats] + outputCSV = "csv" in outputFormats + outputJSON = "json" in outputFormats + + # locate the WACCM output file when = datetime.datetime.strptime( f"{dateStr} {timeStr}", "%Y%m%d %H:%M") - waccmFilename = f"f.e22.beta02.FWSD.f09_f09_mg17.cesm2_2_beta02.forecast.001.cam.h3.{when.year:4d}-{when.month:02d}-{when.day:02}-00000.nc" # read and glean chemical species from WACCM and MUSICA @@ -425,16 +446,19 @@ def main(): lat, lon, when, waccmDir, waccmFilename) logger.info(f"Original WACCM varValues = {varValues}") + # add molecular Nitrogen, Oxygen, and Argon + varValues = addStandardGases(varValues) + # Perform any conversions needed, or derive variables. varValues = convertWaccm(varValues) logger.info(f"Converted WACCM varValues = {varValues}") - if (False): + if (outputCSV): # Write CSV file for MusicBox initial conditions. csvName = os.path.join(musicaDir, "initial_conditions.csv") writeInitCSV(varValues, csvName) - if (False): + if (outputJSON): # Write JSON file for MusicBox initial conditions. jsonName = os.path.join(musicaDir, "initial_config.json") writeInitJSON(varValues, jsonName)