Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

220 config file csv - initial chem species are moved to CSV, and also specified with JSON data #299

Open
wants to merge 32 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
8f9ffb2
Adding initial conditions as CSV or JSON.
Dec 9, 2024
945b04c
Handle the case where chem_step != output_step != 1.
Dec 12, 2024
98a6c8d
initial conditions now has filepath.
Dec 12, 2024
98d83b2
New JSON format for initial conditions CSV.
Dec 12, 2024
5decc90
Added retrieval of command-line args musicaDir and template.
Dec 19, 2024
798482f
Avoid overwriting TS1 input with WACCM test output.
Dec 19, 2024
c40a670
Headers in CSV file need reactionType prefix.
Dec 19, 2024
f0164da
Misleading typo in comment.
Dec 23, 2024
9486ca8
Changed values to be more suitable for testing.
Dec 24, 2024
77eaaf5
Reclassified alleged Warnings to be Info or Debug message.
Dec 26, 2024
8596984
Number density is calculated from pressure and temperature, not set v…
Dec 27, 2024
47a143e
Removed chemical species section. Better testing values.
Dec 28, 2024
6bc3449
Support multiple initial_conditions.csv files. Override CSV values wi…
Dec 30, 2024
35da4bf
New input configuration for intial conditions in CSV files.
Dec 30, 2024
3175b5b
Added LOSS to reactions.
Dec 30, 2024
4dc4b84
Species names differ in format from reaction names. Support multiple …
Dec 30, 2024
05302da
Initial check-in of CSV initial concentrations.
Dec 30, 2024
9133563
Initial species concentrations moved from JSON to CSV file.
Dec 31, 2024
fe0a335
Allow space after comma for human readability.
Dec 31, 2024
ec2fc5a
Allow time steps != 1 second. Take initial concentrations from the JS…
Dec 31, 2024
dbff66e
Converted JSON intial species concentrations JSON to CSV.
Dec 31, 2024
81f75aa
Converted JSON species concentrations to CSV.
Dec 31, 2024
c4c3dd4
Removed unnecessary debug logging.
Jan 1, 2025
a38a3b9
Give warning when one CSV file overrides a previous one.
Jan 1, 2025
00270fe
filepath is now plural: filepaths.
Jan 2, 2025
5a668a6
Removed deprecated section chemical species from JSON.
Jan 2, 2025
897fa90
There is no longer a chemical species section in my_config.json.
Jan 2, 2025
06a7955
Read multiple CSV evolution conditions. Still need to merge the multi…
Jan 2, 2025
99ecd9a
Keep evolving conditions sorted in order of time. Merge the override …
Jan 2, 2025
c176728
Corrected call to non-implemented member evolvingConditions.
Jan 2, 2025
7cad82f
Added warning for overrides when multiple files of evolving conditions.
Jan 2, 2025
3d6a202
Allow multiple files of evolving conditions.
Jan 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 158 additions & 29 deletions src/acom_music_box/conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,94 @@ def from_UI_JSON(self, UI_JSON, species_list, reaction_list):
species_concentrations,
reaction_rates)


@classmethod
def retrieve_initial_conditions_from_JSON(
self,
path_to_json,
json_object,
reaction_types):
"""
Retrieves initial conditions from CSV file and JSON structures.
If both are present, JSON values will override the CSV values.

This class method takes a path to a JSON file, a configuration JSON object,
and a list of desired reaction types.

Args:
path_to_json (str): The path to the JSON file containing the initial conditions and settings.
json_object (dict): The configuration JSON object containing the initial conditions and settings.
reaction_types: Use set like {"ENV", "CONC"} for species concentrations, {"EMIS", "PHOTO"} for reaction rates.

Returns:
object: A dictionary of name:value pairs.
"""

# look for that JSON section
if (not 'initial conditions' in json_object):
return({})
if (len(list(json_object['initial conditions'].keys())) == 0):
return({})

# retrieve initial conditions from CSV and JSON
initial_csv = {}
initial_data = {}

initCond = json_object['initial conditions']
logger.debug(f"initCond: {initCond}")
if 'filepaths' in initCond:
file_paths = initCond['filepaths']

# loop through the CSV files
for file_path in file_paths:
# read initial conditions from CSV file
initial_conditions_path = os.path.join(
os.path.dirname(path_to_json), file_path)

file_initial_csv = Conditions.read_initial_conditions_from_file(
initial_conditions_path, reaction_types)
logger.debug(f"file_initial_csv = {file_initial_csv}")

# tranfer conditions from this file to the aggregated dictionary
for one_csv in file_initial_csv:
# give warning if one file CSV overrides a prior CSV
if one_csv in initial_csv:
logger.warning(
"Value {}:{} in file {} will override prior value {}"
.format(one_csv, file_initial_csv[one_csv],
initial_conditions_path, initial_csv[one_csv]))

initial_csv[one_csv] = file_initial_csv[one_csv]

logger.debug(f"initial_csv = {initial_csv}")

if 'data' in initCond:
# read initial conditions from in-place CSV (list of headers and list of values)
dataConditions = initCond['data']
initial_data = Conditions.read_data_values_from_table(dataConditions,
reaction_types)
logger.debug(f"initial_data = {initial_data}")

# override the CSV species initial values with JSON data
numCSV = len(initial_csv)
numData = len(initial_data)
if (numCSV > 0 and numData > 0):
logger.warning(f"Initial data values ({numData}) from JSON will override initial values ({numCSV}) from CSV.")
for one_data in initial_data:
chem_name_alone = one_data.split(".")[1] # remove reaction type
chem_name_alone = chem_name_alone.split(" ")[0] # remove units
initial_csv[chem_name_alone] = initial_data[one_data]

logger.debug(f"Overridden initial_csv = {initial_csv}")

return(initial_csv)


@classmethod
def from_config_JSON(
self,
path_to_json,
object):
json_object):
"""
Creates an instance of the class from a configuration JSON object.

Expand All @@ -118,51 +201,43 @@ def from_config_JSON(

Args:
path_to_json (str): The path to the JSON file containing the initial conditions and settings.
object (dict): The configuration JSON object containing the initial conditions and settings.
json_object (dict): The configuration JSON object containing the initial conditions and settings.

Returns:
object: An instance of the Conditions class with the settings from the configuration JSON object.
"""
pressure = convert_pressure(
object['environmental conditions']['pressure'],
json_object['environmental conditions']['pressure'],
'initial value')

temperature = convert_temperature(
object['environmental conditions']['temperature'],
json_object['environmental conditions']['temperature'],
'initial value')

# Set initial species concentrations
initial_concentrations = {}
reaction_rates = {}

# reads initial conditions from csv if it is given
if 'initial conditions' in object and len(
list(object['initial conditions'].keys())) > 0:

initial_conditions_path = os.path.join(
os.path.dirname(path_to_json),
list(object['initial conditions'].keys())[0])
# we will read species concentrations and reaction rates on two passes
species_concentrations = Conditions.retrieve_initial_conditions_from_JSON(
path_to_json, json_object, {"ENV", "CONC"})
reaction_rates = Conditions.retrieve_initial_conditions_from_JSON(
path_to_json, json_object, {"EMIS", "PHOTO", "LOSS"})

reaction_rates = Conditions.read_initial_rates_from_file(
initial_conditions_path)
# override presure and temperature
if ("pressure" in species_concentrations):
pressure = species_concentrations["pressure"]
if ("temperature" in species_concentrations):
temperature = species_concentrations["temperature"]

# reads from config file directly if present
if 'chemical species' in object:
initial_concentrations = {
species: convert_concentration(
object['chemical species'][species], 'initial value', temperature, pressure
)
for species in object['chemical species']
}
logger.debug(f"Returning species_concentrations = {species_concentrations}")
logger.debug(f"Returning reaction_rates = {reaction_rates}")

return self(
pressure,
temperature,
initial_concentrations,
species_concentrations,
reaction_rates)


@classmethod
def read_initial_rates_from_file(cls, file_path):
def read_initial_conditions_from_file(cls, file_path, react_types=None):
"""
Reads initial reaction rates from a file.

Expand All @@ -171,14 +246,15 @@ def read_initial_rates_from_file(cls, file_path):

Args:
file_path (str): The path to the file containing the initial reaction rates.
react_types = set of reaction types only to include, or None to include all.

Returns:
dict: A dictionary of initial reaction rates.
"""

reaction_rates = {}

df = pd.read_csv(file_path)
df = pd.read_csv(file_path, skipinitialspace=True)
rows, _ = df.shape
if rows > 1:
raise ValueError(f'Initial conditions file ({file_path}) may only have one row of data. There are {rows} rows present.')
Expand All @@ -196,10 +272,63 @@ def read_initial_rates_from_file(cls, file_path):
rate_name = f'{reaction_type}.{label}'
if rate_name in reaction_rates:
raise ValueError(f"Duplicate reaction rate found: {rate_name}")
reaction_rates[rate_name] = df.iloc[0][key]

# are we looking for this type?
if (react_types):
if (reaction_type not in react_types):
continue

# create key-value pair of chemical-concentration
# initial concentration looks like this: CONC.a-pinene [mol m-3]
# reaction rate looks like this: LOSS.SOA2 wall loss.s-1
chem_name_alone = f"{reaction_type}.{label}" # reaction
if len(parts) == 2:
chem_name_alone = label.split(' ')[0] # strip off [units] to get chemical
reaction_rates[chem_name_alone] = df.at[0, key] # retrieve (row, column)

return reaction_rates

@classmethod
def read_data_values_from_table(cls, data_json, react_types=None):
"""
Reads data values from a CSV-type table expressed in JSON.

This class method takes a JSON element, reads two rows, and
sets variable names and values to the header and value rows.
Example of the data:
"data": [
["ENV.temperature [K]", "ENV.pressure [Pa]", "CONC.A [mol m-3]", "CONC.B [mol m-3]"],
[200, 70000, 0.67, 2.3e-9]
]

Args:
data_json (object): JSON list of two lists.
react_types = set of reaction types only to include, or None to include all.

Returns:
dict: A dictionary of initial data values.
"""

data_values = {}

rows = len(data_json)
if rows != 2:
raise ValueError(f'Initial conditions data in JSON ({data_json}) should have only header and value rows. There are {rows} rows present.')

# build the dictionary from the columns
header_row = data_json[0]
value_row = data_json[1]
for header, value in zip(header_row, value_row):
# are we looking for this type?
if (react_types):
header_type = header.split('.')[0]
if (header_type not in react_types):
continue

data_values[header] = float(value)

return data_values

def add_species_concentration(self, species_concentration):
"""
Add a SpeciesConcentration instance to the list of species concentrations.
Expand Down
68 changes: 49 additions & 19 deletions src/acom_music_box/evolving_conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,49 +124,78 @@ def from_config_JSON(
evolving_conditions = EvolvingConditions()

# Check if 'evolving conditions' is a key in the JSON config
if 'evolving conditions' in config_JSON:
if len(config_JSON['evolving conditions'].keys()) > 0:
# Construct the path to the evolving conditions file

if (not 'evolving conditions' in config_JSON):
return(evolving_conditions)
if (len(list(config_JSON['evolving conditions'].keys())) == 0):
return(evolving_conditions)

evolveCond = config_JSON['evolving conditions']
logger.debug(f"evolveCond: {evolveCond}")
if 'filepaths' in evolveCond:
file_paths = evolveCond['filepaths']

# loop through the CSV files
allReactions = set() # provide warning for duplicates that will override
for file_path in file_paths:
# read initial conditions from CSV file
evolving_conditions_path = os.path.join(
os.path.dirname(path_to_json),
list(config_JSON['evolving conditions'].keys())[0])
os.path.dirname(path_to_json), file_path)

evolving_conditions = EvolvingConditions.read_conditions_from_file(
fileReactions = evolving_conditions.read_conditions_from_file(
evolving_conditions_path)
logger.debug(f"evolving_conditions.times = {evolving_conditions.times}")
logger.debug(f"evolving_conditions.conditions = {evolving_conditions.conditions}")

# any duplicate conditions?
overrideSet = allReactions.intersection(fileReactions)
if (len(overrideSet) > 0):
logger.warning("File {} will override earlier conditions {}"
.format(file_path, sorted(overrideSet)))
allReactions = allReactions.union(fileReactions)

return evolving_conditions

def add_condition(self, time_point, conditions):
"""
Add an evolving condition at a specific time point.
Keep the two lists sorted in order by time.
New arrivals will probably come at the end of the list.

Args:
time_point (float): The time point for the evolving condition.
conditions (Conditions): The associated conditions at the given time point.
"""
self.time.append(time_point)
self.conditions.append(conditions)
# Work backward from end of list, looking for first time <= this new time.
timeIndex = len(self.times)
while (timeIndex > 0
and self.times[timeIndex - 1] > time_point):
timeIndex -= 1

@classmethod
def read_conditions_from_file(cls, file_path):
self.times.insert(timeIndex, time_point)
self.conditions.insert(timeIndex, conditions)

def read_conditions_from_file(self, file_path):
"""
Read conditions from a file and update the evolving conditions.

Args:
file_path (str): The path to the file containing conditions UI_JSON.
"""

times = []
conditions = []
Returns:
set: the headers read from the CSV file;
used for warning about condition overrides
"""

df = pd.read_csv(file_path)
df = pd.read_csv(file_path, skipinitialspace=True)
header_set = set(df.columns)

# if present these columns must use these names
pressure_key = 'ENV.pressure.Pa'
temperature_key = 'ENV.temperature.K'
time_key = 'time.s'

header_set.remove(time_key)

time_and_environment_keys = [pressure_key, temperature_key, time_key]
# other keys will depend on the species names and reaction labels configured in the mechanism
other_keys = [key for key in df.columns if key not in time_and_environment_keys]
Expand Down Expand Up @@ -196,15 +225,16 @@ def read_conditions_from_file(cls, file_path):
else:
reaction_rates[f'{condition_type}.{label}'] = row[key]

times.append(time)
conditions.append(
self.add_condition(time,
Conditions(
pressure,
temperature,
species_concentrations,
reaction_rates))
reaction_rates
)
)

return cls(times=times, conditions=conditions)
return(header_set)

# allows len overload for this class

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ENV.temperature [K],ENV.pressure [Pa],CONC.A [mol m-3],CONC.B [mol m-3],CONC.C [mol m-3]
283.6,102364.4,0.9,0.1,0.3
Loading