11from .utils import convert_pressure , convert_temperature , convert_concentration
22import pandas as pd
3+ import numpy
34import os
45
56import 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