Skip to content

Commit cf5fba6

Browse files
authored
Merge pull request #318 from NCAR/main
v2.7.0
2 parents e6536be + fabc69a commit cf5fba6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+744
-1033
lines changed

.github/workflows/CI_Tests.yml

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -58,23 +58,4 @@ jobs:
5858
music_box -e Analytical -o output.csv -vv --color-output
5959
waccmToMusicBox waccmDir="./sample_waccm_data" date="20240904" time="07:00" latitude=3.1 longitude=101.7
6060
61-
- name: Check for config.zip
62-
if: runner.os != 'Windows'
63-
run: |
64-
if [ -f "./config.zip" ]; then
65-
echo "config.zip created successfully"
66-
else
67-
echo "config.zip not found"
68-
exit 1
69-
fi
70-
71-
- name: Check for config.zip (Windows)
72-
if: runner.os == 'Windows'
73-
run: |
74-
if (Test-Path -Path "./config.zip") {
75-
Write-Output "config.zip created successfully"
76-
} else {
77-
Write-Output "config.zip not found"
78-
exit 1
79-
}
80-
shell: pwsh
61+
shell: pwsh
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: 'Close stale issues'
2+
on:
3+
schedule:
4+
# Run every day at 7PM UTC
5+
- cron: '0 19 * * *'
6+
jobs:
7+
close-stale:
8+
permissions:
9+
issues: write
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/stale@v9
13+
with:
14+
stale-issue-message: 'This issue is stale because it has been open 180 days with no activity. Remove stale label or comment or this will be closed in 14 days.'
15+
close-issue-message: 'This issue was closed because it has been stalled for 14 days with no activity.'
16+
days-before-stale: 180 # Mark as stale after 180 days of inactivity
17+
days-before-close: 14 # Close issue if no activity after 14 days of being stale
18+
days-before-pr-close: -1
19+
# Issues with this label are exempt from being checked if they are stale...
20+
exempt-issue-labels: Low Priority
21+
# Below are currently defaults, but given in case we decide to change
22+
operations-per-run: 30 # This setting specifies the maximum number of operations (such as marking issues as stale or closing them) that the action will perform in a single run
23+
stale-issue-label: Stale # This setting defines the label that will be applied to issues that are considered stale
24+
close-issue-reason: not_planned # This setting specifies the reason that will be given when closing stale issues

CITATION.cff

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ authors:
1919
given-names: Brendan
2020
- family-names: Drews
2121
given-names: Carl
22+
- family-names: Thind
23+
given-names: Montek
2224
- family-names: Kiran
2325
given-names: Aditya
2426
license: Apache-2.0

README.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Copyright (C) 2020 National Science Foundation - National Center for Atmospheric
1515

1616
# Installation
1717
```
18-
pip install acom_music_box
18+
pip install acom-music-box
1919
```
2020

2121
# Command line tool
@@ -33,34 +33,34 @@ Run an example. Notice that the output, in csv format, is printed to the termina
3333
music_box -e Chapman
3434
```
3535

36-
Output can be saved to a csv file and printed to the terminal.
36+
Output can be saved to a csv file (the default format) and printed to the terminal.
3737

3838
```
39-
music_box -e Chapman -o output.csv
39+
music_box -e Chapman -o output
4040
```
4141

42-
Output can be saved to a csv file and the terminal output can be suppressed by specifying the `--output-format`
42+
Output can be saved to a csv file by specifying the .csv extension for Comma-Separated Values.
4343

4444
```
45-
music_box --output-format csv -e Chapman -o output.csv
45+
music_box -e Chapman -o output.csv
4646
```
4747

48-
Output can be saved to a file as netcdf file when `--output-format` netcdf is passed
48+
Output can be saved to a file as netcdf file by specifying the .nc file extension.
4949

5050
```
51-
music_box --output-format netcdf -e Chapman -o output.nc
51+
music_box -e Chapman -o output.nc
5252
```
5353

54-
Output can be saved to a file in csv format when a filename is not specified. In this case a timestamped csv file is made
54+
Output can be saved to a file in csv format when a filename is not specified. In this case a timestamped csv file is made.
5555

5656
```
57-
music_box --output-format csv -e Chapman
57+
music_box -e Chapman
5858
```
5959

60-
Output can be saved to a file in netcdf format when a filename is not specified. In this case a timestamped netcdf file is made
60+
You may also specify multiple output files with different formats, using the file extension.
6161

6262
```
63-
music_box --output-format netcdf -e Chapman
63+
music_box -e Analytical -o results.csv -o results.nc
6464
```
6565

6666
You can also run your own configuration
@@ -95,7 +95,7 @@ music_box -h
9595
It is used like this
9696

9797
```
98-
music_box -e TS1 --output-format csv --plot O3 --plot-output-unit "ppb"
98+
music_box -e TS1 --plot O3 --plot-output-unit "ppb"
9999
```
100100

101101
### gnuplot

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ classifiers = ["License :: OSI Approved :: Apache Software License"]
2121
dynamic = ["version", "description"]
2222

2323
dependencies = [
24-
"musica==0.9.0",
24+
"musica==0.10.1",
2525
"xarray",
2626
"colorlog",
2727
"pandas",

src/acom_music_box/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
This package contains modules for handling various aspects of a music box,
55
including species, products, reactants, reactions, and more.
66
"""
7-
__version__ = "2.6.0"
7+
__version__ = "2.7.0"
88

99
from .utils import convert_time, convert_pressure, convert_temperature, convert_concentration
1010
from .model_options import BoxModelOptions

src/acom_music_box/conditions.py

Lines changed: 156 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from .utils import convert_pressure, convert_temperature, convert_concentration
22
import pandas as pd
3+
import numpy
34
import os
45

56
import logging
@@ -105,11 +106,94 @@ def from_UI_JSON(self, UI_JSON, species_list, reaction_list):
105106
species_concentrations,
106107
reaction_rates)
107108

109+
@classmethod
110+
def retrieve_initial_conditions_from_JSON(
111+
cls,
112+
path_to_json,
113+
json_object,
114+
reaction_types):
115+
"""
116+
Retrieves initial conditions from CSV file and JSON structures.
117+
If both are present, JSON values will override the CSV values.
118+
119+
This class method takes a path to a JSON file, a configuration JSON object,
120+
and a list of desired reaction types.
121+
122+
Args:
123+
path_to_json (str): The path to the JSON file containing the initial conditions and settings.
124+
json_object (dict): The configuration JSON object containing the initial conditions and settings.
125+
reaction_types: Use set like {"ENV", "CONC"} for species concentrations, {"EMIS", "PHOTO"} for reaction rates.
126+
127+
Returns:
128+
object: A dictionary of name:value pairs.
129+
"""
130+
131+
logger.debug(f"path_to_json: {path_to_json} reaction_types: {reaction_types}")
132+
133+
# look for that JSON section
134+
if (not 'initial conditions' in json_object):
135+
return ({})
136+
if (len(list(json_object['initial conditions'].keys())) == 0):
137+
return ({})
138+
139+
# retrieve initial conditions from CSV and JSON
140+
initial_csv = {}
141+
initial_data = {}
142+
143+
initCond = json_object['initial conditions']
144+
logger.debug(f"initCond: {initCond}")
145+
if 'filepaths' in initCond:
146+
file_paths = initCond['filepaths']
147+
148+
# loop through the CSV files
149+
for file_path in file_paths:
150+
# read initial conditions from CSV file
151+
initial_conditions_path = os.path.join(
152+
os.path.dirname(path_to_json), file_path)
153+
154+
file_initial_csv = Conditions.read_initial_conditions_from_file(
155+
initial_conditions_path, reaction_types)
156+
logger.debug(f"file_initial_csv = {file_initial_csv}")
157+
158+
# tranfer conditions from this file to the aggregated dictionary
159+
for one_csv in file_initial_csv:
160+
# give warning if one file CSV overrides a prior CSV
161+
if one_csv in initial_csv:
162+
logger.warning(
163+
"Value {}:{} in file {} will override prior value {}"
164+
.format(one_csv, file_initial_csv[one_csv],
165+
initial_conditions_path, initial_csv[one_csv]))
166+
167+
initial_csv[one_csv] = file_initial_csv[one_csv]
168+
169+
logger.debug(f"initial_csv = {initial_csv}")
170+
171+
if 'data' in initCond:
172+
# read initial conditions from in-place CSV (list of headers and list of values)
173+
dataConditions = initCond['data']
174+
initial_data = Conditions.read_data_values_from_table(dataConditions,
175+
reaction_types)
176+
logger.debug(f"initial_data = {initial_data}")
177+
178+
# override the CSV species initial values with JSON data
179+
numCSV = len(initial_csv)
180+
numData = len(initial_data)
181+
if (numCSV > 0 and numData > 0):
182+
logger.warning(f"Initial data values ({numData}) from JSON will override initial values ({numCSV}) from CSV.")
183+
for one_data in initial_data:
184+
chem_name_alone = one_data.split(".")[1] # remove reaction type
185+
chem_name_alone = chem_name_alone.split(" ")[0] # remove units
186+
initial_csv[chem_name_alone] = initial_data[one_data]
187+
188+
logger.debug(f"Overridden initial_csv = {initial_csv}")
189+
190+
return (initial_csv)
191+
108192
@classmethod
109193
def from_config_JSON(
110194
self,
111195
path_to_json,
112-
object):
196+
json_object):
113197
"""
114198
Creates an instance of the class from a configuration JSON object.
115199
@@ -118,51 +202,46 @@ def from_config_JSON(
118202
119203
Args:
120204
path_to_json (str): The path to the JSON file containing the initial conditions and settings.
121-
object (dict): The configuration JSON object containing the initial conditions and settings.
205+
json_object (dict): The configuration JSON object containing the initial conditions and settings.
122206
123207
Returns:
124208
object: An instance of the Conditions class with the settings from the configuration JSON object.
125209
"""
126210
pressure = convert_pressure(
127-
object['environmental conditions']['pressure'],
211+
json_object['environmental conditions']['pressure'],
128212
'initial value')
129213

130214
temperature = convert_temperature(
131-
object['environmental conditions']['temperature'],
215+
json_object['environmental conditions']['temperature'],
132216
'initial value')
133217

134-
# Set initial species concentrations
135-
initial_concentrations = {}
136-
reaction_rates = {}
218+
logger.debug(f"From original JSON temperature = {temperature} pessure = {pressure}")
137219

138-
# reads initial conditions from csv if it is given
139-
if 'initial conditions' in object and len(
140-
list(object['initial conditions'].keys())) > 0:
220+
# we will read environment, species concentrations, and reaction rates on three passes
221+
environmental_conditions = Conditions.retrieve_initial_conditions_from_JSON(
222+
path_to_json, json_object, {"ENV"})
223+
species_concentrations = Conditions.retrieve_initial_conditions_from_JSON(
224+
path_to_json, json_object, {"CONC"})
225+
reaction_rates = Conditions.retrieve_initial_conditions_from_JSON(
226+
path_to_json, json_object, {"EMIS", "PHOTO", "LOSS"})
141227

142-
initial_conditions_path = os.path.join(
143-
os.path.dirname(path_to_json),
144-
list(object['initial conditions'].keys())[0])
228+
# override presure and temperature
229+
if ("pressure" in environmental_conditions):
230+
pressure = environmental_conditions["pressure"]
231+
if ("temperature" in environmental_conditions):
232+
temperature = environmental_conditions["temperature"]
145233

146-
reaction_rates = Conditions.read_initial_rates_from_file(
147-
initial_conditions_path)
148-
149-
# reads from config file directly if present
150-
if 'chemical species' in object:
151-
initial_concentrations = {
152-
species: convert_concentration(
153-
object['chemical species'][species], 'initial value', temperature, pressure
154-
)
155-
for species in object['chemical species']
156-
}
234+
logger.debug(f"Returning species_concentrations = {species_concentrations}")
235+
logger.debug(f"Returning reaction_rates = {reaction_rates}")
157236

158237
return self(
159238
pressure,
160239
temperature,
161-
initial_concentrations,
240+
species_concentrations,
162241
reaction_rates)
163242

164243
@classmethod
165-
def read_initial_rates_from_file(cls, file_path):
244+
def read_initial_conditions_from_file(cls, file_path, react_types=None):
166245
"""
167246
Reads initial reaction rates from a file.
168247
@@ -171,14 +250,15 @@ def read_initial_rates_from_file(cls, file_path):
171250
172251
Args:
173252
file_path (str): The path to the file containing the initial reaction rates.
253+
react_types = set of reaction types only to include, or None to include all.
174254
175255
Returns:
176256
dict: A dictionary of initial reaction rates.
177257
"""
178258

179259
reaction_rates = {}
180260

181-
df = pd.read_csv(file_path)
261+
df = pd.read_csv(file_path, skipinitialspace=True)
182262
rows, _ = df.shape
183263
if rows > 1:
184264
raise ValueError(f'Initial conditions file ({file_path}) may only have one row of data. There are {rows} rows present.')
@@ -196,10 +276,58 @@ def read_initial_rates_from_file(cls, file_path):
196276
rate_name = f'{reaction_type}.{label}'
197277
if rate_name in reaction_rates:
198278
raise ValueError(f"Duplicate reaction rate found: {rate_name}")
199-
reaction_rates[rate_name] = df.iloc[0][key]
279+
280+
# are we looking for this type?
281+
if (react_types):
282+
if (reaction_type not in react_types):
283+
continue
284+
285+
# create key-value pair of chemical-concentration
286+
# initial concentration looks like this: CONC.a-pinene [mol m-3]
287+
# reaction rate looks like this: LOSS.SOA2 wall loss.s-1
288+
chem_name_alone = f"{reaction_type}.{label}" # reaction
289+
if len(parts) == 2:
290+
chem_name_alone = label.split(' ')[0] # strip off [units] to get chemical
291+
reaction_rates[chem_name_alone] = df.at[0, key] # retrieve (row, column)
200292

201293
return reaction_rates
202294

295+
@classmethod
296+
def read_data_values_from_table(cls, data_json, react_types=None):
297+
"""
298+
Reads data values from a CSV-type table expressed in JSON.
299+
300+
This class method takes a JSON element, reads two rows, and
301+
sets variable names and values to the header and value rows.
302+
Example of the data:
303+
"data": [
304+
["ENV.temperature [K]", "ENV.pressure [Pa]", "CONC.A [mol m-3]", "CONC.B [mol m-3]"],
305+
[200, 70000, 0.67, 2.3e-9]
306+
]
307+
308+
Args:
309+
data_json (object): JSON list of two lists.
310+
react_types = set of reaction types only to include, or None to include all.
311+
312+
Returns:
313+
dict: A dictionary of initial data values.
314+
"""
315+
316+
data_values = {}
317+
318+
rows = len(data_json)
319+
if rows != 2:
320+
raise ValueError(f'Initial conditions data in JSON ({data_json}) should have only header and value rows. There are {rows} rows present.')
321+
322+
# build the dictionary from the reaction columns
323+
header_row = data_json[0]
324+
value_row = data_json[1]
325+
data_values = {key: float(value) for key, value in zip(header_row, value_row)
326+
if key.split('.')[0] in react_types}
327+
logger.debug(f"For {react_types} data_values = {data_values}")
328+
329+
return data_values
330+
203331
def add_species_concentration(self, species_concentration):
204332
"""
205333
Add a SpeciesConcentration instance to the list of species concentrations.

0 commit comments

Comments
 (0)