From 877105322c88b8f66cf847d7ad96deb6b02e053d Mon Sep 17 00:00:00 2001 From: Carl Drews Date: Tue, 13 Aug 2024 12:07:50 -0600 Subject: [PATCH 1/4] Address issue 169 air density, M third body, catch unexpected JSON (#186) * Added logic to catch unexpected JSON config - eg, missing keys. * Clearer code for setting initial reaction rates. * Changed for musica 0.7.3; air density and M third body. * Musica version is now 0.7.3. * Removed commented-out logging code. --- requirements.txt | 1 + src/acom_music_box/music_box.py | 11 ++++++----- src/acom_music_box/music_box_conditions.py | 9 +++++++-- .../music_box_evolving_conditions.py | 9 ++++++--- src/acom_music_box/music_box_reaction_list.py | 14 +++++++++----- 5 files changed, 29 insertions(+), 15 deletions(-) diff --git a/requirements.txt b/requirements.txt index 46a81507..364caa52 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +musica==0.7.3 pandas pipx pytest diff --git a/src/acom_music_box/music_box.py b/src/acom_music_box/music_box.py index 94c587b3..0b285ddc 100644 --- a/src/acom_music_box/music_box.py +++ b/src/acom_music_box/music_box.py @@ -488,7 +488,7 @@ def solve(self, output_path = None): next_output_time = curr_time #runs the simulation at each timestep - + while(curr_time <= self.box_model_options.simulation_length): #outputs to output_array if enough time has elapsed @@ -502,7 +502,7 @@ def solve(self, output_path = None): output_array.append(row) next_output_time += self.box_model_options.output_step_time - #iterates evolvings conditons if enough time has elapsed + #iterates evolving conditions if enough time has elapsed while(next_conditions != None and next_conditions_time <= curr_time): curr_conditions.update_conditions(next_conditions) @@ -523,7 +523,8 @@ def solve(self, output_path = None): BOLTZMANN_CONSTANT = 1.380649e-23 AVOGADRO_CONSTANT = 6.02214076e23; GAS_CONSTANT = BOLTZMANN_CONSTANT * AVOGADRO_CONSTANT - air_density = curr_conditions.pressure / (GAS_CONSTANT * curr_conditions.temperature) + air_density = curr_conditions.pressure / (GAS_CONSTANT * curr_conditions.temperature) + #solves and updates concentration values in concentration array if (not ordered_concentrations): @@ -535,7 +536,7 @@ def solve(self, output_path = None): #increments time curr_time += self.box_model_options.chem_step_time - + #outputs to file if output is present if(output_path != None): logger.info("path_to_output = {}".format(output_path)) @@ -705,7 +706,7 @@ def order_species_concentrations(self, curr_conditions, species_constant_orderin concentrations[concentraton.species.name] = concentraton.concentration ordered_concentrations = len(concentrations.keys()) * [0.0] - + for key, value in concentrations.items(): ordered_concentrations[species_constant_ordering[key]] = value return ordered_concentrations diff --git a/src/acom_music_box/music_box_conditions.py b/src/acom_music_box/music_box_conditions.py index 1001604a..01abd22b 100644 --- a/src/acom_music_box/music_box_conditions.py +++ b/src/acom_music_box/music_box_conditions.py @@ -1,5 +1,9 @@ import csv import os + +import logging +logger = logging.getLogger(__name__) + from typing import List from .music_box_reaction_rate import ReactionRate from .music_box_species import Species @@ -137,9 +141,10 @@ def from_config_JSON(cls, path_to_json, config_JSON, species_list, reaction_list species_concentrations.append(SpeciesConcentration(species, 0)) # Set initial reaction rates - for reaction in reaction_list.reactions: - if reaction.name != None and not any(reac.reaction.name == reaction.name for reac in reaction_rates): + if (reaction.name is None): + continue + if not any(rate.reaction.name == reaction.name for rate in reaction_rates): reaction_rates.append(ReactionRate(reaction, 0)) diff --git a/src/acom_music_box/music_box_evolving_conditions.py b/src/acom_music_box/music_box_evolving_conditions.py index a6a90777..ab05ce87 100644 --- a/src/acom_music_box/music_box_evolving_conditions.py +++ b/src/acom_music_box/music_box_evolving_conditions.py @@ -108,9 +108,12 @@ def from_config_JSON(cls, path_to_json ,config_JSON, species_list, reaction_list # Check if 'evolving conditions' is a key in the JSON config if 'evolving conditions' in config_JSON: - # Construct the path to the evolving conditions file - evolving_conditions_path = os.path.dirname(path_to_json) + "/" + list(config_JSON['evolving conditions'].keys())[0] - evolving_conditions = EvolvingConditions.read_conditions_from_file( evolving_conditions_path, species_list, reaction_list) + if len(config_JSON['evolving conditions'].keys()) > 0: + # Construct the path to the evolving conditions file + evolving_conditions_path = (os.path.dirname(path_to_json) + "/" + + list(config_JSON['evolving conditions'].keys())[0]) + evolving_conditions = EvolvingConditions.read_conditions_from_file( + evolving_conditions_path, species_list, reaction_list) return evolving_conditions diff --git a/src/acom_music_box/music_box_reaction_list.py b/src/acom_music_box/music_box_reaction_list.py index 512ed5ba..f433aa6b 100644 --- a/src/acom_music_box/music_box_reaction_list.py +++ b/src/acom_music_box/music_box_reaction_list.py @@ -5,6 +5,9 @@ from .music_box_reactant import Reactant from .music_box_product import Product +import logging +logger = logging.getLogger(__name__) + class ReactionList: """ Represents a list of chemical reactions. @@ -114,12 +117,13 @@ def get_reactants_from_JSON(self, reaction, species_list): """ reactants = [] - for reactant, reactant_info in reaction['reactants'].items(): - match = filter(lambda x: x.name == reactant, species_list.species) - species = next(match, None) - quantity = reactant_info['qty'] if 'qty' in reactant_info else None + if ('reactants' in reaction.keys()): + for reactant, reactant_info in reaction['reactants'].items(): + match = filter(lambda x: x.name == reactant, species_list.species) + species = next(match, None) + quantity = reactant_info['qty'] if 'qty' in reactant_info else None - reactants.append(Reactant(species, quantity)) + reactants.append(Reactant(species, quantity)) return reactants @classmethod From 9fe28066a4eb235f653faa9fb5f6f3cc5598d5f3 Mon Sep 17 00:00:00 2001 From: Kyle Shores Date: Tue, 13 Aug 2024 13:11:47 -0500 Subject: [PATCH 2/4] updating git action trigger --- .github/workflows/CI_Tests.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/CI_Tests.yml b/.github/workflows/CI_Tests.yml index 25506b0e..01bae148 100644 --- a/.github/workflows/CI_Tests.yml +++ b/.github/workflows/CI_Tests.yml @@ -1,6 +1,11 @@ name: CI Tests -on: [push, workflow_dispatch] +on: + push: + branches: + - main + pull_request: + workflow_dispatch: jobs: build: From 5615094defe8afbc9b2405da5135b86c8c5819f8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 13 Aug 2024 13:17:36 -0500 Subject: [PATCH 3/4] Auto-format code using Clang-Format (#188) Co-authored-by: GitHub Actions Co-authored-by: Kyle Shores --- doc/source/conf.py | 2 +- src/acom_music_box/__init__.py | 3 +- src/acom_music_box/music_box.py | 365 ++++++++++-------- src/acom_music_box/music_box_conditions.py | 156 +++++--- .../music_box_evolving_conditions.py | 138 ++++--- src/acom_music_box/music_box_main.py | 33 +- src/acom_music_box/music_box_model_options.py | 37 +- src/acom_music_box/music_box_reactant.py | 1 - src/acom_music_box/music_box_reaction.py | 93 ++++- src/acom_music_box/music_box_reaction_list.py | 119 ++++-- src/acom_music_box/music_box_reaction_rate.py | 2 +- src/acom_music_box/music_box_species.py | 18 +- .../music_box_species_concentration.py | 3 +- src/acom_music_box/music_box_species_list.py | 48 ++- src/acom_music_box/utils.py | 13 +- tests/test_chapman.py | 6 +- tests/test_full_gas_phase_mechanism.py | 6 +- tests/test_wall_loss.py | 6 +- 18 files changed, 655 insertions(+), 394 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 6985fb1f..a84d7e6d 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -49,4 +49,4 @@ ] html_favicon = '_static/favicon.png' -html_logo = "_static/MusicBox.svg" \ No newline at end of file +html_logo = "_static/MusicBox.svg" diff --git a/src/acom_music_box/__init__.py b/src/acom_music_box/__init__.py index b7110b30..b8f042a5 100644 --- a/src/acom_music_box/__init__.py +++ b/src/acom_music_box/__init__.py @@ -1,7 +1,7 @@ """ This is the music_box package. -This package contains modules for handling various aspects of a music box, +This package contains modules for handling various aspects of a music box, including species, products, reactants, reactions, and more. """ __version__ = "2.1.5" @@ -19,4 +19,3 @@ from .music_box_evolving_conditions import EvolvingConditions from .music_box import MusicBox - diff --git a/src/acom_music_box/music_box.py b/src/acom_music_box/music_box.py index 0b285ddc..1cef19b5 100644 --- a/src/acom_music_box/music_box.py +++ b/src/acom_music_box/music_box.py @@ -1,20 +1,17 @@ +import musica +import csv +from .music_box_conditions import Conditions +from .music_box_model_options import BoxModelOptions +from .music_box_species_list import SpeciesList +from .music_box_reaction import Reaction, Branched, Arrhenius, Tunneling, Troe_Ternary +from .music_box_reaction_list import ReactionList +from .music_box_evolving_conditions import EvolvingConditions import json import os import logging logger = logging.getLogger(__name__) -from .music_box_evolving_conditions import EvolvingConditions -from .music_box_reaction_list import ReactionList -from .music_box_reaction import Reaction, Branched, Arrhenius, Tunneling, Troe_Ternary -from .music_box_species_list import SpeciesList -from .music_box_model_options import BoxModelOptions -from .music_box_conditions import Conditions - -import csv -import musica - - class MusicBox: """ @@ -29,8 +26,14 @@ class MusicBox: evolvingConditions (List[EvolvingConditions]): List of evolving conditions over time. """ - def __init__(self, box_model_options=None, species_list=None, reaction_list=None, - initial_conditions=None, evolving_conditions=None, config_file=None): + def __init__( + self, + box_model_options=None, + species_list=None, + reaction_list=None, + initial_conditions=None, + evolving_conditions=None, + config_file=None): """ Initializes a new instance of the BoxModel class. @@ -46,12 +49,11 @@ def __init__(self, box_model_options=None, species_list=None, reaction_list=None self.species_list = species_list if species_list is not None else SpeciesList() self.reaction_list = reaction_list if reaction_list is not None else ReactionList() self.initial_conditions = initial_conditions if initial_conditions is not None else Conditions() - self.evolving_conditions = evolving_conditions if evolving_conditions is not None else EvolvingConditions([], []) + self.evolving_conditions = evolving_conditions if evolving_conditions is not None else EvolvingConditions([ + ], []) self.config_file = config_file if config_file is not None else "camp_data/config.json" - - self.solver = None - + self.solver = None def add_evolving_condition(self, time_point, conditions): """ @@ -61,7 +63,8 @@ def add_evolving_condition(self, time_point, conditions): time_point (float): The time point for the evolving condition. conditions (Conditions): The associated conditions at the given time point. """ - evolving_condition = EvolvingConditions(time=[time_point], conditions=[conditions]) + evolving_condition = EvolvingConditions( + time=[time_point], conditions=[conditions]) self.evolvingConditions.append(evolving_condition) def generateConfig(self, directory): @@ -79,9 +82,9 @@ def generateConfig(self, directory): # Check if directory exists and create it if it doesn't if not os.path.exists(output_path): os.makedirs(output_path) - os.makedirs(output_path + "/camp_data") + os.makedirs(output_path + "/camp_data") - # Make camp_data config + # Make camp_data config with open(output_path + "/camp_data/config.json", 'w') as camp_config_file: data = { "camp-files": [ @@ -114,7 +117,8 @@ def generateConfig(self, directory): if self.initial_conditions.species_concentrations is not None: for species_concentration in self.initial_conditions.species_concentrations: - data["chemical species"][species_concentration.species.name] = { "initial value [mol m-3]": species_concentration.concentration } + data["chemical species"][species_concentration.species.name] = { + "initial value [mol m-3]": species_concentration.concentration} data["environmental conditions"] = { "pressure": { @@ -133,15 +137,13 @@ def generateConfig(self, directory): "initial_conditions.csv": {} } - - data["model components"] = [ { "type": "CAMP", "configuration file": "camp_data/config.json", "override species": { "M": { - "mixing ratio mol mol-1": 1 + "mixing ratio mol mol-1": 1 } }, "suppress output": { @@ -151,8 +153,7 @@ def generateConfig(self, directory): ] config_file.write(json.dumps(data, indent=4)) - - + # Make evolving conditions config with open(output_path + "/evolving_conditions.csv", 'w', newline='') as evolving_conditions_file: writer = csv.writer(evolving_conditions_file) @@ -163,28 +164,33 @@ def generateConfig(self, directory): for header in self.evolving_conditions.headers[1:]: if header == "ENV.pressure.Pa": - row.append(self.evolving_conditions.conditions[i].pressure) + row.append( + self.evolving_conditions.conditions[i].pressure) elif header == "ENV.temperature.K": - row.append(self.evolving_conditions.conditions[i].temperature) + row.append( + self.evolving_conditions.conditions[i].temperature) elif header.startswith("CONC."): species_name = header.split('.')[1] - species_concentration = next((x for x in self.evolving_conditions.conditions[i].species_concentrations if x.species.name == species_name), None) + species_concentration = next( + (x for x in self.evolving_conditions.conditions[i].species_concentrations if x.species.name == species_name), + None) row.append(species_concentration.concentration) elif header.endswith(".s-1"): reaction_name = header.split('.') if reaction_name[0] == 'LOSS' or reaction_name[0] == 'EMIS': - reaction_name = reaction_name[0] + '_' + reaction_name[1] + reaction_name = reaction_name[0] + \ + '_' + reaction_name[1] else: reaction_name = reaction_name[1] - reaction_rate = next((x for x in self.evolving_conditions.conditions[i].reaction_rates if x.reaction.name == reaction_name), None) - row.append(reaction_rate.rate) + reaction_rate = next( + (x for x in self.evolving_conditions.conditions[i].reaction_rates if x.reaction.name == reaction_name), + None) + row.append(reaction_rate.rate) writer.writerow(row) - - reaction_names = [] reaction_rates = [] @@ -198,14 +204,12 @@ def generateConfig(self, directory): reaction_names.append(name) reaction_rates.append(reaction_rate.rate) - #writes reaction rates inital conditions to file + # writes reaction rates inital conditions to file with open(output_path + "/initial_conditions.csv", 'w', newline='') as initial_conditions_file: writer = csv.writer(initial_conditions_file) writer.writerow(reaction_names) writer.writerow(reaction_rates) - - def generateSpeciesConfig(self): """ Generate a JSON configuration for the species in the box model. @@ -216,48 +220,47 @@ def generateSpeciesConfig(self): speciesArray = [] - #Adds relative tolerance if value is set - if(self.species_list.relative_tolerance != None): + # Adds relative tolerance if value is set + if (self.species_list.relative_tolerance is not None): relativeTolerance = {} relativeTolerance["type"] = "RELATIVE_TOLERANCE" relativeTolerance["value"] = self.species_list.relative_tolerance speciesArray.append(relativeTolerance) - #Adds species to config + # Adds species to config for species in self.species_list.species: spec = {} - #Add species name if value is set - if(species.name != None): + # Add species name if value is set + if (species.name is not None): spec["name"] = species.name spec["type"] = "CHEM_SPEC" - - #Add species absoluate tolerance if value is set - if(species.absolute_tolerance != None): + + # Add species absoluate tolerance if value is set + if (species.absolute_tolerance is not None): spec["absolute tolerance"] = species.absolute_tolerance - - #Add species phase if value is set - if(species.phase != None): - spec["phase"] = species.phase - #Add species molecular weight if value is set - if(species.molecular_weight != None): + # Add species phase if value is set + if (species.phase is not None): + spec["phase"] = species.phase + + # Add species molecular weight if value is set + if (species.molecular_weight is not None): spec["molecular weight [kg mol-1]"] = species.molecular_weight - - #Add species density if value is set - if(species.density != None): + + # Add species density if value is set + if (species.density is not None): spec["density [kg m-3]"] = species.density speciesArray.append(spec) species_json = { - "camp-data" : speciesArray + "camp-data": speciesArray } return json.dumps(species_json, indent=4) - - + def generateReactionConfig(self): """ Generate a JSON configuration for the reactions in the box model. @@ -267,49 +270,49 @@ def generateReactionConfig(self): """ reacList = {} - #Add mechanism name if value is set - if self.reaction_list.name != None: + # Add mechanism name if value is set + if self.reaction_list.name is not None: reacList["name"] = self.reaction_list.name - + reacList["type"] = "MECHANISM" reactionsArray = [] - #Adds reaction to config + # Adds reaction to config for reaction in self.reaction_list.reactions: reac = {} - #Adds reaction name if value is set - if(reaction.reaction_type != None): + # Adds reaction name if value is set + if (reaction.reaction_type is not None): reac["type"] = reaction.reaction_type reactants = {} - #Adds reactants + # Adds reactants for reactant in reaction.reactants: quantity = {} - #Adds reactant quantity if value is set - if reactant.quantity != None: + # Adds reactant quantity if value is set + if reactant.quantity is not None: quantity["qty"] = reactant.quantity reactants[reactant.name] = quantity - + reac["reactants"] = reactants if not isinstance(reaction, Branched): products = {} - #Adds products + # Adds products for product in reaction.products: yield_value = {} - #Adds product yield if value is set - if product.yield_value != None: + # Adds product yield if value is set + if product.yield_value is not None: yield_value["yield"] = product.yield_value products[product.name] = yield_value - + reac["products"] = products - + # Add reaction parameters if necessary if isinstance(reaction, Branched): alkoxy_products = {} @@ -319,10 +322,10 @@ def generateReactionConfig(self): yield_value = {} # Adds alkoxy product yield if value is set - if alkoxy_product.yield_value != None: + if alkoxy_product.yield_value is not None: yield_value["yield"] = alkoxy_product.yield_value alkoxy_products[alkoxy_product.name] = yield_value - + reac["alkoxy products"] = alkoxy_products nitrate_products = {} @@ -332,10 +335,10 @@ def generateReactionConfig(self): yield_value = {} # Adds nitrate product yield if value is set - if nitrate_product.yield_value != None: + if nitrate_product.yield_value is not None: yield_value["yield"] = nitrate_product.yield_value nitrate_products[nitrate_product.name] = yield_value - + reac["nitrate products"] = nitrate_products # Adds parameters for the reaction @@ -347,7 +350,7 @@ def generateReactionConfig(self): reac["a0"] = reaction.a0 if reaction.n is not None: reac["n"] = reaction.n - + elif isinstance(reaction, Arrhenius): # Adds parameters for the reaction if reaction.A is not None: @@ -360,7 +363,7 @@ def generateReactionConfig(self): reac["E"] = reaction.E if reaction.Ea is not None: reac["Ea"] = reaction.Ea - + elif isinstance(reaction, Tunneling): # Adds parameters for the reaction if reaction.A is not None: @@ -369,7 +372,7 @@ def generateReactionConfig(self): reac["B"] = reaction.B if reaction.C is not None: reac["C"] = reaction.C - + elif isinstance(reaction, Troe_Ternary): # Adds parameters for the reaction if reaction.k0_A is not None: @@ -388,12 +391,12 @@ def generateReactionConfig(self): reac["Fc"] = reaction.Fc if reaction.N is not None: reac["N"] = reaction.N - - #Adds reaction name if value is set - if(reaction.name != None): + + # Adds reaction name if value is set + if (reaction.name is not None): reac["MUSICA name"] = reaction.name - if(reaction.scaling_factor != None): + if (reaction.scaling_factor is not None): reac["scaling factor"] = reaction.scaling_factor reactionsArray.append(reac) @@ -401,12 +404,16 @@ def generateReactionConfig(self): reacList["reactions"] = reactionsArray reactionsJson = { - "camp-data" : [reacList] + "camp-data": [reacList] } return json.dumps(reactionsJson, indent=4) - - def create_solver(self, path_to_config, solver_type = musica.micmsolver.rosenbrock, number_of_grid_cells = 1): + + def create_solver( + self, + path_to_config, + solver_type=musica.micmsolver.rosenbrock, + number_of_grid_cells=1): """ Creates a micm solver object using the CAMP configuration files. @@ -417,82 +424,85 @@ def create_solver(self, path_to_config, solver_type = musica.micmsolver.rosenbro None """ # Create a solver object using the configuration file - self.solver = musica.create_solver(path_to_config, musica.micmsolver.rosenbrock, number_of_grid_cells) - + self.solver = musica.create_solver( + path_to_config, + musica.micmsolver.rosenbrock, + number_of_grid_cells) - def solve(self, output_path = None): + def solve(self, output_path=None): """ Solves the box model simulation and optionally writes the output to a file. - This function runs the box model simulation using the current settings and - conditions. If a path is provided, it writes the output of the simulation to + This function runs the box model simulation using the current settings and + conditions. If a path is provided, it writes the output of the simulation to the specified file. Args: - path_to_output (str, optional): The path to the file where the output will + path_to_output (str, optional): The path to the file where the output will be written. If None, no output file is created. Defaults to None. Returns: - list: A 2D list where each inner list represents the results of the simulation + list: A 2D list where each inner list represents the results of the simulation at a specific time step. """ - - #sets up initial conditions to be current conditions + + # sets up initial conditions to be current conditions curr_conditions = self.initial_conditions - #sets up next condition if evolving conditions is not empty + # sets up next condition if evolving conditions is not empty next_conditions = None next_conditions_time = 0 next_conditions_index = 0 - if(len(self.evolving_conditions) != 0): - if(self.evolving_conditions.times[0] != 0): + if (len(self.evolving_conditions) != 0): + if (self.evolving_conditions.times[0] != 0): next_conditions_index = 0 next_conditions = self.evolving_conditions.conditions[0] next_conditions_time = self.evolving_conditions.times[0] - elif(len(self.evolving_conditions) > 1): + elif (len(self.evolving_conditions) > 1): next_conditions_index = 1 next_conditions = self.evolving_conditions.conditions[1] - next_conditions_time = self.evolving_conditions.times[1] - + next_conditions_time = self.evolving_conditions.times[1] - #initalizes output headers + # initalizes output headers output_array = [] - + headers = [] headers.append("time") headers.append("ENV.temperature") headers.append("ENV.pressure") - if (self.solver is None): raise Exception("Error: MusicBox object {} has no solver." - .format(self)) - rate_constant_ordering = musica.user_defined_reaction_rates(self.solver) + .format(self)) + rate_constant_ordering = musica.user_defined_reaction_rates( + self.solver) species_constant_ordering = musica.species_ordering(self.solver) - - #adds species headers to output - ordered_species_headers = [k for k, v in sorted(species_constant_ordering.items(), key=lambda item: item[1])] + # adds species headers to output + ordered_species_headers = [ + k for k, + v in sorted( + species_constant_ordering.items(), + key=lambda item: item[1])] for spec in ordered_species_headers: headers.append("CONC." + spec) - ordered_concentrations = self.order_species_concentrations(curr_conditions, species_constant_ordering) - ordered_rate_constants = self.order_reaction_rates(curr_conditions, rate_constant_ordering) - - + ordered_concentrations = self.order_species_concentrations( + curr_conditions, species_constant_ordering) + ordered_rate_constants = self.order_reaction_rates( + curr_conditions, rate_constant_ordering) + output_array.append(headers) - - + curr_time = 0 next_output_time = curr_time - #runs the simulation at each timestep - + # runs the simulation at each timestep - while(curr_time <= self.box_model_options.simulation_length): + while (curr_time <= self.box_model_options.simulation_length): - #outputs to output_array if enough time has elapsed - if(next_output_time <= curr_time): + # outputs to output_array if enough time has elapsed + if (next_output_time <= curr_time): row = [] row.append(next_output_time) row.append(curr_conditions.temperature) @@ -501,57 +511,63 @@ def solve(self, output_path = None): row.append(conc) output_array.append(row) next_output_time += self.box_model_options.output_step_time - - #iterates evolving conditions if enough time has elapsed - while(next_conditions != None and next_conditions_time <= curr_time): - + + # iterates evolving conditions if enough time has elapsed + while ( + next_conditions is not None and next_conditions_time <= curr_time): + curr_conditions.update_conditions(next_conditions) - - #iterates next_conditions if there are remaining evolving conditions - if(len(self.evolving_conditions) > next_conditions_index + 1): + + # iterates next_conditions if there are remaining evolving + # conditions + if (len(self.evolving_conditions) > next_conditions_index + 1): next_conditions_index += 1 next_conditions = self.evolving_conditions.conditions[next_conditions_index] next_conditions_time = self.evolving_conditions.times[next_conditions_index] - - ordered_rate_constants = self.order_reaction_rates(curr_conditions, rate_constant_ordering) - + + ordered_rate_constants = self.order_reaction_rates( + curr_conditions, rate_constant_ordering) + else: next_conditions = None - - + # calculate air density from the ideal gas law BOLTZMANN_CONSTANT = 1.380649e-23 - AVOGADRO_CONSTANT = 6.02214076e23; + AVOGADRO_CONSTANT = 6.02214076e23 GAS_CONSTANT = BOLTZMANN_CONSTANT * AVOGADRO_CONSTANT - air_density = curr_conditions.pressure / (GAS_CONSTANT * curr_conditions.temperature) + air_density = curr_conditions.pressure / \ + (GAS_CONSTANT * curr_conditions.temperature) - - #solves and updates concentration values in concentration array + # solves and updates concentration values in concentration array if (not ordered_concentrations): logger.info("Warning: ordered_concentrations list is empty.") - musica.micm_solve(self.solver, self.box_model_options.chem_step_time, - curr_conditions.temperature, curr_conditions.pressure, air_density, - ordered_concentrations, ordered_rate_constants) - - - #increments time - curr_time += self.box_model_options.chem_step_time - - #outputs to file if output is present - if(output_path != None): + musica.micm_solve( + self.solver, + self.box_model_options.chem_step_time, + curr_conditions.temperature, + curr_conditions.pressure, + air_density, + ordered_concentrations, + ordered_rate_constants) + + # increments time + curr_time += self.box_model_options.chem_step_time + + # outputs to file if output is present + if (output_path is not None): logger.info("path_to_output = {}".format(output_path)) with open(output_path, 'w', newline='') as output: writer = csv.writer(output) writer.writerows(output_array) - #returns output_array + # returns output_array return output_array - + def readFromUIJson(self, path_to_json): """ Reads and parses a JSON file from the MusicBox Interactive UI to set up the box model simulation. - This function takes the path to a JSON file, reads the file, and parses the JSON + This function takes the path to a JSON file, reads the file, and parses the JSON to set up the box model simulation. Args: @@ -564,7 +580,6 @@ def readFromUIJson(self, path_to_json): ValueError: If the JSON file cannot be read or parsed. """ - with open(path_to_json, 'r') as json_file: data = json.load(json_file) @@ -575,13 +590,16 @@ def readFromUIJson(self, path_to_json): self.species_list = SpeciesList.from_UI_JSON(data) # Set reaction list - self.reaction_list = ReactionList.from_UI_JSON(data, self.species_list) + self.reaction_list = ReactionList.from_UI_JSON( + data, self.species_list) # Set initial conditions - self.initial_conditions = Conditions.from_UI_JSON(data, self.species_list, self.reaction_list) + self.initial_conditions = Conditions.from_UI_JSON( + data, self.species_list, self.reaction_list) # Set evolving conditions - self.evolving_conditions = EvolvingConditions.from_UI_JSON(data, self.species_list, self.reaction_list) + self.evolving_conditions = EvolvingConditions.from_UI_JSON( + data, self.species_list, self.reaction_list) def readFromUIJsonString(self, data): """ @@ -596,7 +614,6 @@ def readFromUIJsonString(self, data): Raises: ValueError: If the JSON string cannot be parsed. """ - # Set box model options self.box_model_options = BoxModelOptions.from_UI_JSON(data) @@ -608,10 +625,12 @@ def readFromUIJsonString(self, data): self.reaction_list = ReactionList.from_UI_JSON(data, self.species_list) # Set initial conditions - self.initial_conditions = Conditions.from_UI_JSON(data, self.species_list, self.reaction_list) + self.initial_conditions = Conditions.from_UI_JSON( + data, self.species_list, self.reaction_list) # Set evolving conditions - self.evolving_conditions = EvolvingConditions.from_UI_JSON(data, self.species_list, self.reaction_list) + self.evolving_conditions = EvolvingConditions.from_UI_JSON( + data, self.species_list, self.reaction_list) def readConditionsFromJson(self, path_to_json): """ @@ -633,22 +652,25 @@ def readConditionsFromJson(self, path_to_json): self.box_model_options = BoxModelOptions.from_config_JSON(data) # Set species list - self.species_list = SpeciesList.from_config_JSON(path_to_json, data) + self.species_list = SpeciesList.from_config_JSON( + path_to_json, data) - self.reaction_list = ReactionList.from_config_JSON(path_to_json, data, self.species_list) + self.reaction_list = ReactionList.from_config_JSON( + path_to_json, data, self.species_list) # Set initial conditions - self.initial_conditions = Conditions.from_config_JSON(path_to_json, data, self.species_list, self.reaction_list) + self.initial_conditions = Conditions.from_config_JSON( + path_to_json, data, self.species_list, self.reaction_list) # Set initial conditions - self.evolving_conditions = EvolvingConditions.from_config_JSON(path_to_json, data, self.species_list, self.reaction_list) - + self.evolving_conditions = EvolvingConditions.from_config_JSON( + path_to_json, data, self.species_list, self.reaction_list) def speciesOrdering(self): """ Retrieves the ordering of species used in the solver. - This function calls the `species_ordering` function from the `musica` module, + This function calls the `species_ordering` function from the `musica` module, passing the solver instance from the current object. Returns: @@ -660,7 +682,7 @@ def userDefinedReactionRates(self): """ Retrieves the user-defined reaction rates from the solver. - This function calls the `user_defined_reaction_rates` function from the `musica` module, + This function calls the `user_defined_reaction_rates` function from the `musica` module, passing the solver instance from the current object. Returns: @@ -671,7 +693,7 @@ def order_reaction_rates(self, curr_conditions, rate_constant_ordering): """ Orders the reaction rates based on the provided ordering. - This function takes the current conditions and a specified ordering for the rate constants, + This function takes the current conditions and a specified ordering for the rate constants, and reorders the reaction rates accordingly. Args: @@ -684,9 +706,9 @@ def order_reaction_rates(self, curr_conditions, rate_constant_ordering): rate_constants = {} for rate in curr_conditions.reaction_rates: - if(rate.reaction.reaction_type == "PHOTOLYSIS"): + if (rate.reaction.reaction_type == "PHOTOLYSIS"): key = "PHOTO." + rate.reaction.name - elif(rate.reaction.reaction_type == "LOSS"): + elif (rate.reaction.reaction_type == "LOSS"): key = "LOSS." + rate.reaction.name elif (rate.reaction.reaction_type == "EMISSION"): key = "EMIS." + rate.reaction.name @@ -697,18 +719,19 @@ def order_reaction_rates(self, curr_conditions, rate_constant_ordering): ordered_rate_constants[rate_constant_ordering[key]] = float(value) return ordered_rate_constants - + @classmethod - def order_species_concentrations(self, curr_conditions, species_constant_ordering): + def order_species_concentrations( + self, + curr_conditions, + species_constant_ordering): concentrations = {} for concentraton in curr_conditions.species_concentrations: concentrations[concentraton.species.name] = concentraton.concentration - + ordered_concentrations = len(concentrations.keys()) * [0.0] for key, value in concentrations.items(): ordered_concentrations[species_constant_ordering[key]] = value return ordered_concentrations - - diff --git a/src/acom_music_box/music_box_conditions.py b/src/acom_music_box/music_box_conditions.py index 01abd22b..8ece10a2 100644 --- a/src/acom_music_box/music_box_conditions.py +++ b/src/acom_music_box/music_box_conditions.py @@ -1,15 +1,14 @@ +from .utils import convert_time, convert_pressure, convert_temperature, convert_concentration +from .music_box_species_concentration import SpeciesConcentration +from .music_box_species import Species +from .music_box_reaction_rate import ReactionRate +from typing import List import csv import os import logging logger = logging.getLogger(__name__) -from typing import List -from .music_box_reaction_rate import ReactionRate -from .music_box_species import Species -from .music_box_species_concentration import SpeciesConcentration -from .utils import convert_time, convert_pressure, convert_temperature, convert_concentration - class Conditions: """ @@ -23,8 +22,12 @@ class Conditions: reactionRates (List[ReactionRate]): A list of reaction rates. """ - def __init__(self, pressure=None, temperature=None, species_concentrations=None, reaction_rates=None): - + def __init__( + self, + pressure=None, + temperature=None, + species_concentrations=None, + reaction_rates=None): """ Initializes a new instance of the Conditions class. @@ -38,10 +41,10 @@ def __init__(self, pressure=None, temperature=None, species_concentrations=None, self.temperature = temperature self.species_concentrations = species_concentrations if species_concentrations is not None else [] self.reaction_rates = reaction_rates if reaction_rates is not None else [] - + def __repr__(self): return f"Conditions(pressure={self.pressure}, temperature={self.temperature}, species_concentrations={self.species_concentrations}, reaction_rates={self.reaction_rates})" - + def __str__(self): return f"Pressure: {self.pressure}, Temperature: {self.temperature}, Species Concentrations: {self.species_concentrations}, Reaction Rates: {self.reaction_rates}" @@ -50,7 +53,7 @@ def from_UI_JSON(cls, UI_JSON, species_list, reaction_list): """ Creates an instance of the class from a UI JSON object. - This class method takes a UI JSON object, a species list, and a reaction list, + This class method takes a UI JSON object, a species list, and a reaction list, and uses them to create a new instance of the class. Args: @@ -61,9 +64,13 @@ def from_UI_JSON(cls, UI_JSON, species_list, reaction_list): Returns: object: An instance of the Conditions class with the settings from the UI JSON object. """ - pressure = convert_pressure(UI_JSON['conditions']['environmental conditions']['pressure'], 'initial value') + pressure = convert_pressure( + UI_JSON['conditions']['environmental conditions']['pressure'], + 'initial value') - temperature = convert_temperature(UI_JSON['conditions']['environmental conditions']['temperature'], 'initial value') + temperature = convert_temperature( + UI_JSON['conditions']['environmental conditions']['temperature'], + 'initial value') # Set initial species concentrations species_concentrations = [] @@ -71,34 +78,48 @@ def from_UI_JSON(cls, UI_JSON, species_list, reaction_list): match = filter(lambda x: x.name == chem_spec, species_list.species) species = next(match, None) - concentration = convert_concentration(UI_JSON['conditions']['chemical species'][chem_spec], 'initial value') + concentration = convert_concentration( + UI_JSON['conditions']['chemical species'][chem_spec], 'initial value') - species_concentrations.append(SpeciesConcentration(species, concentration)) + species_concentrations.append( + SpeciesConcentration( + species, concentration)) for species in species_list.species: - if not any(conc.species.name == species.name for conc in species_concentrations): - species_concentrations.append(SpeciesConcentration(species, 0)) + if not any(conc.species.name == + species.name for conc in species_concentrations): + species_concentrations.append(SpeciesConcentration(species, 0)) # Set initial reaction rates reaction_rates = [] for reaction in UI_JSON['conditions']['initial conditions']: - match = filter(lambda x: x.name == reaction.split('.')[1], reaction_list.reactions) + match = filter( + lambda x: x.name == reaction.split('.')[1], + reaction_list.reactions) reaction_from_list = next(match, None) rate = UI_JSON['conditions']['initial conditions'][reaction] reaction_rates.append(ReactionRate(reaction_from_list, rate)) - - - return cls(pressure, temperature, species_concentrations, reaction_rates) - + + return cls( + pressure, + temperature, + species_concentrations, + reaction_rates) + @classmethod - def from_config_JSON(cls, path_to_json, config_JSON, species_list, reaction_list): + def from_config_JSON( + cls, + path_to_json, + config_JSON, + species_list, + reaction_list): """ Creates an instance of the class from a configuration JSON object. - This class method takes a path to a JSON file, a configuration JSON object, a species list, + This class method takes a path to a JSON file, a configuration JSON object, a species list, and a reaction list, and uses them to create a new instance of the class. Args: @@ -110,53 +131,65 @@ def from_config_JSON(cls, path_to_json, config_JSON, species_list, reaction_list Returns: object: An instance of the Conditions class with the settings from the configuration JSON object. """ - pressure = convert_pressure(config_JSON['environmental conditions']['pressure'], 'initial value') - - temperature = convert_temperature(config_JSON['environmental conditions']['temperature'], 'initial value') + pressure = convert_pressure( + config_JSON['environmental conditions']['pressure'], + 'initial value') + temperature = convert_temperature( + config_JSON['environmental conditions']['temperature'], + 'initial value') # Set initial species concentrations species_concentrations = [] reaction_rates = [] - #reads initial conditions from csv if it is given - if 'initial conditions' in config_JSON and len(list(config_JSON['initial conditions'].keys())) > 0: + # reads initial conditions from csv if it is given + if 'initial conditions' in config_JSON and len( + list(config_JSON['initial conditions'].keys())) > 0: - initial_conditions_path = os.path.dirname(path_to_json) + "/" + list(config_JSON['initial conditions'].keys())[0] - reaction_rates = Conditions.read_initial_rates_from_file(initial_conditions_path, reaction_list) + initial_conditions_path = os.path.dirname( + path_to_json) + "/" + list(config_JSON['initial conditions'].keys())[0] + reaction_rates = Conditions.read_initial_rates_from_file( + initial_conditions_path, reaction_list) - - #reads from config file directly if present + # reads from config file directly if present if 'chemical species' in config_JSON: for chem_spec in config_JSON['chemical species']: - species = Species(name = chem_spec) - concentration = convert_concentration(config_JSON['chemical species'][chem_spec], 'initial value') + species = Species(name=chem_spec) + concentration = convert_concentration( + config_JSON['chemical species'][chem_spec], 'initial value') + + species_concentrations.append( + SpeciesConcentration( + species, concentration)) - species_concentrations.append(SpeciesConcentration(species, concentration)) - for species in species_list.species: if species.tracer_type == 'THIRD_BODY': continue - if not any(conc.species.name == species.name for conc in species_concentrations): - species_concentrations.append(SpeciesConcentration(species, 0)) + if not any(conc.species.name == + species.name for conc in species_concentrations): + species_concentrations.append(SpeciesConcentration(species, 0)) # Set initial reaction rates for reaction in reaction_list.reactions: if (reaction.name is None): continue - if not any(rate.reaction.name == reaction.name for rate in reaction_rates): - reaction_rates.append(ReactionRate(reaction, 0)) - - - return cls(pressure, temperature, species_concentrations, reaction_rates) + if not any(rate.reaction.name == + reaction.name for rate in reaction_rates): + reaction_rates.append(ReactionRate(reaction, 0)) + return cls( + pressure, + temperature, + species_concentrations, + reaction_rates) @classmethod def read_initial_rates_from_file(cls, file_path, reaction_list): """ Reads initial reaction rates from a file. - This class method takes a file path and a ReactionList, reads the file, and + This class method takes a file path and a ReactionList, reads the file, and sets the initial reaction rates based on the contents of the file. Args: @@ -171,29 +204,28 @@ def read_initial_rates_from_file(cls, file_path, reaction_list): with open(file_path, 'r') as csv_file: initial_conditions = list(csv.reader(csv_file)) - - if(len(initial_conditions) > 1): + + if (len(initial_conditions) > 1): # The first row of the CSV contains headers headers = initial_conditions[0] # The second row of the CSV contains rates rates = initial_conditions[1] - for i in range(0, len(headers)): - reaction_rate = headers[i] - match = filter(lambda x: x.name == reaction_rate.split('.')[1], reaction_list.reactions) - + match = filter( + lambda x: x.name == reaction_rate.split('.')[1], + reaction_list.reactions) + reaction = next(match, None) rate = rates[i] reaction_rates.append(ReactionRate(reaction, rate)) return reaction_rates - def add_species_concentration(self, species_concentration): """ Add a SpeciesConcentration instance to the list of species concentrations. @@ -220,7 +252,7 @@ def get_concentration_array(self): list: An array containing concentrations of each species. Notes: - This function extracts the concentration attribute from each SpeciesConcentration object in + This function extracts the concentration attribute from each SpeciesConcentration object in the species_concentrations list and returns them as a single array to be used by the micm solver. """ concentration_array = [] @@ -245,7 +277,6 @@ def get_reaction_rate_array(self): rate_array.append(reaction_rate.rate) return rate_array - def update_conditions(self, new_conditions): """ @@ -258,15 +289,18 @@ def update_conditions(self, new_conditions): self.pressure = new_conditions.pressure if new_conditions.temperature is not None: self.temperature = new_conditions.temperature - for conc in new_conditions.species_concentrations: - match = filter(lambda x: x.species.name == conc.species.name, self.species_concentrations) + for conc in new_conditions.species_concentrations: + match = filter( + lambda x: x.species.name == conc.species.name, + self.species_concentrations) for item in list(match): item.concentration = conc.concentration - + for rate in new_conditions.reaction_rates: - - match = filter(lambda x: x.reaction.name == rate.reaction.name, self.reaction_rates) - + + match = filter( + lambda x: x.reaction.name == rate.reaction.name, + self.reaction_rates) + for item in list(match): item.rate = rate.rate - diff --git a/src/acom_music_box/music_box_evolving_conditions.py b/src/acom_music_box/music_box_evolving_conditions.py index ab05ce87..0c7da184 100644 --- a/src/acom_music_box/music_box_evolving_conditions.py +++ b/src/acom_music_box/music_box_evolving_conditions.py @@ -50,20 +50,28 @@ def from_UI_JSON(cls, UI_JSON, species_list, reaction_list): pressure = None if 'ENV.pressure.Pa' in headers: - pressure = float(evol_from_json[i][headers.index('ENV.pressure.Pa')]) + pressure = float( + evol_from_json[i][headers.index('ENV.pressure.Pa')]) temperature = None if 'ENV.temperature.K' in headers: - temperature = float(evol_from_json[i][headers.index('ENV.temperature.K')]) + temperature = float( + evol_from_json[i][headers.index('ENV.temperature.K')]) concentrations = [] - concentration_headers = list(filter(lambda x: 'CONC' in x, headers)) + concentration_headers = list( + filter(lambda x: 'CONC' in x, headers)) for j in range(len(concentration_headers)): - match = filter(lambda x: x.name == concentration_headers[j].split('.')[1], species_list.species) + match = filter( + lambda x: x.name == concentration_headers[j].split('.')[1], + species_list.species) species = next(match, None) - concentration = float(evol_from_json[i][headers.index(concentration_headers[j])]) - concentrations.append(SpeciesConcentration(species, concentration)) + concentration = float( + evol_from_json[i][headers.index(concentration_headers[j])]) + concentrations.append( + SpeciesConcentration( + species, concentration)) rates = [] rate_headers = list(filter(lambda x: 's-1' in x, headers)) @@ -75,22 +83,34 @@ def from_UI_JSON(cls, UI_JSON, species_list, reaction_list): else: name_to_match = name_to_match[1] - match = filter(lambda x: x.name == name_to_match, reaction_list.reactions) + match = filter( + lambda x: x.name == name_to_match, + reaction_list.reactions) reaction = next(match, None) rate = float(evol_from_json[i][headers.index(rate_headers[k])]) rates.append(ReactionRate(reaction, rate)) - conditions.append(Conditions(pressure, temperature, concentrations, rates)) + conditions.append( + Conditions( + pressure, + temperature, + concentrations, + rates)) return cls(headers, times, conditions) - + @classmethod - def from_config_JSON(cls, path_to_json ,config_JSON, species_list, reaction_list): + def from_config_JSON( + cls, + path_to_json, + config_JSON, + species_list, + reaction_list): """ Creates an instance of the EvolvingConditions class from a configuration JSON object. - This class method takes a path to a JSON file, a configuration JSON object, a SpeciesList, + This class method takes a path to a JSON file, a configuration JSON object, a SpeciesList, and a ReactionList, and uses them to create a new instance of the EvolvingConditions class. Args: @@ -103,20 +123,21 @@ def from_config_JSON(cls, path_to_json ,config_JSON, species_list, reaction_list EvolvingConditions: An instance of the EvolvingConditions class with the settings from the configuration JSON object. """ - 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 - evolving_conditions_path = (os.path.dirname(path_to_json) + "/" - + list(config_JSON['evolving conditions'].keys())[0]) - evolving_conditions = EvolvingConditions.read_conditions_from_file( + evolving_conditions_path = ( + os.path.dirname(path_to_json) + + "/" + + list( + config_JSON['evolving conditions'].keys())[0]) + evolving_conditions = EvolvingConditions.read_conditions_from_file( evolving_conditions_path, species_list, reaction_list) - + return evolving_conditions - def add_condition(self, time_point, conditions): """ @@ -128,7 +149,7 @@ def add_condition(self, time_point, conditions): """ self.time.append(time_point) self.conditions.append(conditions) - + @classmethod def read_conditions_from_file(cls, file_path, species_list, reaction_list): """ @@ -140,15 +161,15 @@ def read_conditions_from_file(cls, file_path, species_list, reaction_list): times = [] conditions = [] - + # Open the evolving conditions file and read it as a CSV with open(file_path, 'r') as csv_file: - evolving_conditions = list(csv.reader(csv_file)) - - if(len(evolving_conditions) > 1): + evolving_conditions = list(csv.reader(csv_file)) + + if (len(evolving_conditions) > 1): # The first row of the CSV contains headers headers = evolving_conditions[0] - + # Iterate over the remaining rows of the CSV for i in range(1, len(evolving_conditions)): # The first column of each row is a time value @@ -158,57 +179,78 @@ def read_conditions_from_file(cls, file_path, species_list, reaction_list): pressure = None temperature = None - # If pressure and temperature headers are present in the CSV, extract their values + # If pressure and temperature headers are present in the + # CSV, extract their values if 'ENV.pressure.Pa' in headers: - pressure = float(evolving_conditions[i][headers.index('ENV.pressure.Pa')]) + pressure = float( + evolving_conditions[i][headers.index('ENV.pressure.Pa')]) if 'ENV.temperature.K' in headers: - temperature = float(evolving_conditions[i][headers.index('ENV.temperature.K')]) + temperature = float( + evolving_conditions[i][headers.index('ENV.temperature.K')]) - # Initialize concentrations list and extract concentration headers + # Initialize concentrations list and extract concentration + # headers concentrations = [] - concentration_headers = list(filter(lambda x: 'CONC' in x, headers)) + concentration_headers = list( + filter(lambda x: 'CONC' in x, headers)) - # For each concentration header, find the matching species and append its concentration to the list + # For each concentration header, find the matching species + # and append its concentration to the list for j in range(len(concentration_headers)): - match = filter(lambda x: x.name == concentration_headers[j].split('.')[1], species_list.species) + match = filter( + lambda x: x.name == concentration_headers[j].split('.')[1], + species_list.species) species = next(match, None) - concentration = float(evolving_conditions[i][headers.index(concentration_headers[j])]) - - concentrations.append(SpeciesConcentration(species, concentration)) - + concentration = float( + evolving_conditions[i][headers.index(concentration_headers[j])]) + + concentrations.append( + SpeciesConcentration( + species, concentration)) # Initialize rates list and extract rate headers rates = [] rate_headers = list(filter(lambda x: 's-1' in x, headers)) - # For each rate header, find the matching reaction and append its rate to the list + # For each rate header, find the matching reaction and + # append its rate to the list for k in range(len(rate_headers)): name_to_match = rate_headers[k].split('.') - + if name_to_match[0] == 'LOSS' or name_to_match[0] == 'EMIS': - name_to_match = name_to_match[0] + '_' + name_to_match[1] + name_to_match = name_to_match[0] + \ + '_' + name_to_match[1] else: name_to_match = name_to_match[1] - match = filter(lambda x: x.name == name_to_match, reaction_list.reactions) + match = filter( + lambda x: x.name == name_to_match, + reaction_list.reactions) reaction = next(match, None) - rate = float(evolving_conditions[i][headers.index(rate_headers[k])]) + rate = float( + evolving_conditions[i][headers.index(rate_headers[k])]) rates.append(ReactionRate(reaction, rate)) - # Append the conditions for this time point to the conditions list - conditions.append(Conditions(pressure, temperature, concentrations, rates)) + # Append the conditions for this time point to the + # conditions list + conditions.append( + Conditions( + pressure, + temperature, + concentrations, + rates)) # Return a new instance of the class with the times and conditions - - return cls(times = times, conditions = conditions) - - #allows len overload for this class + return cls(times=times, conditions=conditions) + + # allows len overload for this class + def __len__(self): """ Returns the number of time points in the EvolvingConditions instance. - This method is a part of Python's data model methods and allows the built-in - `len()` function to work with an instance of the EvolvingConditions class. + This method is a part of Python's data model methods and allows the built-in + `len()` function to work with an instance of the EvolvingConditions class. It should return the number of time points for which conditions are recorded. Returns: diff --git a/src/acom_music_box/music_box_main.py b/src/acom_music_box/music_box_main.py index 3a3c6f08..b6dd522f 100644 --- a/src/acom_music_box/music_box_main.py +++ b/src/acom_music_box/music_box_main.py @@ -1,3 +1,5 @@ +import os +import argparse from acom_music_box import MusicBox @@ -8,10 +10,6 @@ import logging logger = logging.getLogger(__name__) -import argparse -import os - - # configure argparse for key-value pairs class KeyValueAction(argparse.Action): @@ -25,8 +23,11 @@ def __call__(self, parser, namespace, values, option_string=None): # argPairs = list of arguments, probably from sys.argv[1:] # named arguments are formatted like this=3.14159 # return dictionary of keywords and values + + def getArgsDictionary(argPairs): - parser = argparse.ArgumentParser(description='Process some key=value pairs.') + parser = argparse.ArgumentParser( + description='Process some key=value pairs.') parser.add_argument( 'key_value_pairs', nargs='+', # This means one or more arguments are expected @@ -36,18 +37,17 @@ def getArgsDictionary(argPairs): argDict = vars(parser.parse_args(argPairs)) # return dictionary - return(argDict) - + return (argDict) def main(): logging.basicConfig(stream=sys.stdout, level=logging.INFO) logger.info("{}".format(__file__)) logger.info("Start time: {}".format(datetime.datetime.now())) - + logger.info("Hello, MusicBox World!") logger.info("Working directory = {}".format(os.getcwd())) - + # retrieve and parse the command-line arguments myArgs = getArgsDictionary(sys.argv[1:]) logger.info("Command line = {}".format(myArgs)) @@ -65,17 +65,24 @@ def main(): # check for required arguments and provide examples if (musicBoxConfigFile is None): errorString = "Error: The configFile parameter is required." - errorString += (" Example: configFile={}" - .format(os.path.join("tests", "configs", "analytical_config", "my_config.json"))) + errorString += ( + " Example: configFile={}" .format( + os.path.join( + "tests", + "configs", + "analytical_config", + "my_config.json"))) raise Exception(errorString) - + # create and load a MusicBox object myBox = MusicBox() myBox.readConditionsFromJson(musicBoxConfigFile) logger.info("myBox = {}".format(myBox)) # create solver and solve, writing output to requested directory - campConfig = os.path.join(os.path.dirname(musicBoxConfigFile), myBox.config_file) + campConfig = os.path.join( + os.path.dirname(musicBoxConfigFile), + myBox.config_file) logger.info("CAMP config = {}".format(campConfig)) myBox.create_solver(campConfig) logger.info("myBox.solver = {}".format(myBox.solver)) diff --git a/src/acom_music_box/music_box_model_options.py b/src/acom_music_box/music_box_model_options.py index b2a9c4ef..f3124595 100644 --- a/src/acom_music_box/music_box_model_options.py +++ b/src/acom_music_box/music_box_model_options.py @@ -12,8 +12,12 @@ class BoxModelOptions: simulationLength (float): Length of the simulation in hours. """ - def __init__(self, chem_step_time=None, output_step_time=None, simulation_length=None, grid="box"): - + def __init__( + self, + chem_step_time=None, + output_step_time=None, + simulation_length=None, + grid="box"): """ Initializes a new instance of the BoxModelOptions class. @@ -39,14 +43,19 @@ def from_UI_JSON(cls, UI_JSON): Returns: BoxModelOptions: A new instance of the BoxModelOptions class. """ - chem_step_time = convert_time(UI_JSON['conditions']['box model options'], 'chemistry time step') - output_step_time = convert_time(UI_JSON['conditions']['box model options'], 'output time step') - simulation_length = convert_time(UI_JSON['conditions']['box model options'], 'simulation length') + chem_step_time = convert_time( + UI_JSON['conditions']['box model options'], + 'chemistry time step') + output_step_time = convert_time( + UI_JSON['conditions']['box model options'], + 'output time step') + simulation_length = convert_time( + UI_JSON['conditions']['box model options'], + 'simulation length') grid = UI_JSON['conditions']['box model options']['grid'] - + return cls(chem_step_time, output_step_time, simulation_length, grid) - @classmethod def from_config_JSON(cls, config_JSON): @@ -60,10 +69,16 @@ def from_config_JSON(cls, config_JSON): BoxModelOptions: A new instance of the BoxModelOptions class. """ - chem_step_time = convert_time(config_JSON['box model options'], 'chemistry time step') - output_step_time = convert_time(config_JSON['box model options'], 'output time step') - simulation_length = convert_time(config_JSON['box model options'], 'simulation length') + chem_step_time = convert_time( + config_JSON['box model options'], + 'chemistry time step') + output_step_time = convert_time( + config_JSON['box model options'], + 'output time step') + simulation_length = convert_time( + config_JSON['box model options'], + 'simulation length') grid = config_JSON['box model options']['grid'] - + return cls(chem_step_time, output_step_time, simulation_length, grid) diff --git a/src/acom_music_box/music_box_reactant.py b/src/acom_music_box/music_box_reactant.py index 634ddce9..c88dae0d 100644 --- a/src/acom_music_box/music_box_reactant.py +++ b/src/acom_music_box/music_box_reactant.py @@ -18,4 +18,3 @@ def __init__(self, species, quantity=None): self.name = species.name self.species = species self.quantity = quantity - diff --git a/src/acom_music_box/music_box_reaction.py b/src/acom_music_box/music_box_reaction.py index bfb93514..e02ed88b 100644 --- a/src/acom_music_box/music_box_reaction.py +++ b/src/acom_music_box/music_box_reaction.py @@ -1,5 +1,6 @@ from typing import List + class Reaction: """ Represents a chemical reaction with attributes such as name, type, reactants, and products. @@ -12,7 +13,13 @@ class Reaction: scaling_factor (float, optional): A scaling factor for the reaction rate. Defaults to None. """ - def __init__(self, name=None, reaction_type=None, reactants=None, products=None, scaling_factor=None): + def __init__( + self, + name=None, + reaction_type=None, + reactants=None, + products=None, + scaling_factor=None): """ Initializes a new instance of the Reaction class. @@ -31,7 +38,7 @@ def __init__(self, name=None, reaction_type=None, reactants=None, products=None, def __str__(self): return f"{self.name}: {self.reaction_type}" - + def __repr__(self): return f"{self.name}: {self.reaction_type}" @@ -53,14 +60,25 @@ def add_product(self, product): """ self.products.append(product) + class Branched(Reaction): - - def __init__(self, name=None, reaction_type=None, reactants=None, alkoxy_products=None, nitrate_products=None, X=None, Y=None, a0=None, n=None): + + def __init__( + self, + name=None, + reaction_type=None, + reactants=None, + alkoxy_products=None, + nitrate_products=None, + X=None, + Y=None, + a0=None, + n=None): """ Initializes an instance of the Branched class. - This method initializes an instance of the Branched class with optional parameters for name, - reaction type, reactants, alkoxy products, nitrate products, X, Y, a0, and n. If these parameters + This method initializes an instance of the Branched class with optional parameters for name, + reaction type, reactants, alkoxy products, nitrate products, X, Y, a0, and n. If these parameters are not provided, they will be set to None. Args: @@ -74,8 +92,13 @@ def __init__(self, name=None, reaction_type=None, reactants=None, alkoxy_product a0 (float, optional): A parameter related to the reaction. Defaults to None. n (float, optional): A parameter related to the reaction. Defaults to None. """ - - super().__init__(name, reaction_type, reactants, alkoxy_products + nitrate_products) + + super().__init__( + name, + reaction_type, + reactants, + alkoxy_products + + nitrate_products) self.X = X self.Y = Y self.a0 = a0 @@ -83,13 +106,24 @@ def __init__(self, name=None, reaction_type=None, reactants=None, alkoxy_product self.alkoxy_products = alkoxy_products self.nitrate_products = nitrate_products + class Arrhenius(Reaction): - def __init__(self, name=None, reaction_type=None, reactants=None, products=None, A=None, B=None, D=None, E=None, Ea=None): + def __init__( + self, + name=None, + reaction_type=None, + reactants=None, + products=None, + A=None, + B=None, + D=None, + E=None, + Ea=None): """ Initializes an instance of the Arrhenius class. - This method initializes an instance of the Arrhenius class with optional parameters for name, - reaction type, reactants, products, and Arrhenius parameters A, B, D, E, Ea. If these parameters + This method initializes an instance of the Arrhenius class with optional parameters for name, + reaction type, reactants, products, and Arrhenius parameters A, B, D, E, Ea. If these parameters are not provided, they will be set to None. Args: @@ -110,13 +144,22 @@ def __init__(self, name=None, reaction_type=None, reactants=None, products=None, self.E = E self.Ea = Ea + class Tunneling(Reaction): - def __init__(self, name=None, reaction_type=None, reactants=None, products=None, A=None, B=None, C=None): + def __init__( + self, + name=None, + reaction_type=None, + reactants=None, + products=None, + A=None, + B=None, + C=None): """ Initializes an instance of the Tunneling class. - This method initializes an instance of the Tunneling class with optional parameters for name, - reaction type, reactants, products, and Tunneling parameters A, B, C. If these parameters + This method initializes an instance of the Tunneling class with optional parameters for name, + reaction type, reactants, products, and Tunneling parameters A, B, C. If these parameters are not provided, they will be set to None. Args: @@ -133,13 +176,27 @@ def __init__(self, name=None, reaction_type=None, reactants=None, products=None, self.B = B self.C = C + class Troe_Ternary(Reaction): - def __init__(self, name=None, reaction_type=None, reactants=None, products=None, k0_A=None, k0_B=None, k0_C=None, kinf_A=None, kinf_B=None, kinf_C=None, Fc=None, N=None): + def __init__( + self, + name=None, + reaction_type=None, + reactants=None, + products=None, + k0_A=None, + k0_B=None, + k0_C=None, + kinf_A=None, + kinf_B=None, + kinf_C=None, + Fc=None, + N=None): """ Initializes an instance of the Troe_Ternary class. - This method initializes an instance of the Troe_Ternary class with optional parameters for name, - reaction type, reactants, products, and Troe_Ternary parameters k0_A, k0_B, k0_C, kinf_A, kinf_B, + This method initializes an instance of the Troe_Ternary class with optional parameters for name, + reaction type, reactants, products, and Troe_Ternary parameters k0_A, k0_B, k0_C, kinf_A, kinf_B, kinf_C, Fc, N. If these parameters are not provided, they will be set to None. Args: @@ -164,4 +221,4 @@ def __init__(self, name=None, reaction_type=None, reactants=None, products=None, self.kinf_B = kinf_B self.kinf_C = kinf_C self.Fc = Fc - self.N = N \ No newline at end of file + self.N = N diff --git a/src/acom_music_box/music_box_reaction_list.py b/src/acom_music_box/music_box_reaction_list.py index f433aa6b..78f54cfc 100644 --- a/src/acom_music_box/music_box_reaction_list.py +++ b/src/acom_music_box/music_box_reaction_list.py @@ -8,6 +8,7 @@ import logging logger = logging.getLogger(__name__) + class ReactionList: """ Represents a list of chemical reactions. @@ -20,7 +21,7 @@ def __init__(self, name=None, reactions=None): """ Initializes an instance of the ReactionList class. - This method initializes an instance of the ReactionList class with an optional name and list of reactions. + This method initializes an instance of the ReactionList class with an optional name and list of reactions. If these parameters are not provided, they will be set to None. Args: @@ -48,10 +49,12 @@ def from_UI_JSON(cls, UI_JSON, species_list): for reaction in UI_JSON['mechanism']['reactions']['camp-data'][0]['reactions']: - reactions.append(ReactionList.get_reactions_from_JSON(reaction, species_list)) + reactions.append( + ReactionList.get_reactions_from_JSON( + reaction, species_list)) return cls(list_name, reactions) - + @classmethod def from_config_JSON(cls, path_to_json, config_JSON, species_list): """ @@ -67,26 +70,28 @@ def from_config_JSON(cls, path_to_json, config_JSON, species_list): reactions = [] list_name = None - #gets config file path - config_file_path = os.path.dirname(path_to_json) + "/" + config_JSON['model components'][0]['configuration file'] + # gets config file path + config_file_path = os.path.dirname( + path_to_json) + "/" + config_JSON['model components'][0]['configuration file'] - #opnens config path to read reaction file + # opnens config path to read reaction file with open(config_file_path, 'r') as json_file: config = json.load(json_file) - #assumes reactions file is second in the list - if(len(config['camp-files']) > 1): - reaction_file_path = os.path.dirname(config_file_path) + "/" + config['camp-files'][1] + # assumes reactions file is second in the list + if (len(config['camp-files']) > 1): + reaction_file_path = os.path.dirname( + config_file_path) + "/" + config['camp-files'][1] with open(reaction_file_path, 'r') as reaction_file: reaction_data = json.load(reaction_file) - - #assumes there is only one mechanism + + # assumes there is only one mechanism list_name = reaction_data['camp-data'][0]['name'] for reaction in reaction_data['camp-data'][0]['reactions']: - reactions.append(ReactionList.get_reactions_from_JSON(reaction, species_list)) - - + reactions.append( + ReactionList.get_reactions_from_JSON( + reaction, species_list)) return cls(list_name, reactions) @@ -119,13 +124,15 @@ def get_reactants_from_JSON(self, reaction, species_list): if ('reactants' in reaction.keys()): for reactant, reactant_info in reaction['reactants'].items(): - match = filter(lambda x: x.name == reactant, species_list.species) + match = filter( + lambda x: x.name == reactant, + species_list.species) species = next(match, None) quantity = reactant_info['qty'] if 'qty' in reactant_info else None reactants.append(Reactant(species, quantity)) return reactants - + @classmethod def get_products_from_JSON(self, reaction, species_list): """ @@ -145,20 +152,22 @@ def get_products_from_JSON(self, reaction, species_list): """ products = [] if 'products' in reaction: - for product, product_info in reaction['products'].items(): - match = filter(lambda x: x.name == product, species_list.species) - species = next(match, None) - yield_value = product_info['yield'] if 'yield' in product_info else None + for product, product_info in reaction['products'].items(): + match = filter( + lambda x: x.name == product, + species_list.species) + species = next(match, None) + yield_value = product_info['yield'] if 'yield' in product_info else None - products.append(Product(species, yield_value)) + products.append(Product(species, yield_value)) return products - + @classmethod def get_reactions_from_JSON(self, reaction, species_list): """ Retrieves reactions from a JSON object. - This method takes a reaction and a SpeciesList, and retrieves the corresponding reactions + This method takes a reaction and a SpeciesList, and retrieves the corresponding reactions from a JSON object. Args: @@ -172,15 +181,19 @@ def get_reactions_from_JSON(self, reaction, species_list): name = reaction['MUSICA name'] if 'MUSICA name' in reaction else None scaling_factor = reaction['scaling factor'] if 'scaling factor' in reaction else None reaction_type = reaction['type'] - - reactants = ReactionList.get_reactants_from_JSON(reaction, species_list) + + reactants = ReactionList.get_reactants_from_JSON( + reaction, species_list) products = ReactionList.get_products_from_JSON(reaction, species_list) - + if reaction_type == 'WENNBERG_NO_RO2': alkoxy_products = [] - for alkoxy_product, alkoxy_product_info in reaction.get('alkoxy products', {}).items(): - match = filter(lambda x: x.name == alkoxy_product, species_list.species) + for alkoxy_product, alkoxy_product_info in reaction.get( + 'alkoxy products', {}).items(): + match = filter( + lambda x: x.name == alkoxy_product, + species_list.species) species = next(match, None) yield_value = alkoxy_product_info.get('yield') @@ -188,8 +201,11 @@ def get_reactions_from_JSON(self, reaction, species_list): nitrate_products = [] - for nitrate_product, nitrate_product_info in reaction.get('nitrate products', {}).items(): - match = filter(lambda x: x.name == nitrate_product, species_list.species) + for nitrate_product, nitrate_product_info in reaction.get( + 'nitrate products', {}).items(): + match = filter( + lambda x: x.name == nitrate_product, + species_list.species) species = next(match, None) yield_value = nitrate_product_info.get('yield') @@ -199,14 +215,32 @@ def get_reactions_from_JSON(self, reaction, species_list): Y = reaction.get('Y') a0 = reaction.get('a0') n = reaction.get('n') - return Branched(name, reaction_type, reactants, alkoxy_products, nitrate_products, X, Y, a0, n) + return Branched( + name, + reaction_type, + reactants, + alkoxy_products, + nitrate_products, + X, + Y, + a0, + n) elif reaction_type == 'ARRHENIUS': A = reaction.get('A') B = reaction.get('B') D = reaction.get('D') E = reaction.get('E') Ea = reaction.get('Ea') - return Arrhenius(name, reaction_type, reactants, products, A, B, D, E, Ea) + return Arrhenius( + name, + reaction_type, + reactants, + products, + A, + B, + D, + E, + Ea) elif reaction_type == 'WENNBERG_TUNNELING': A = reaction.get('A') B = reaction.get('B') @@ -221,6 +255,23 @@ def get_reactions_from_JSON(self, reaction, species_list): kinf_C = reaction.get('kinf_C') Fc = reaction.get('Fc') N = reaction.get('N') - return Troe_Ternary(name, reaction_type, reactants, products, k0_A, k0_B, k0_C, kinf_A, kinf_B, kinf_C, Fc, N) + return Troe_Ternary( + name, + reaction_type, + reactants, + products, + k0_A, + k0_B, + k0_C, + kinf_A, + kinf_B, + kinf_C, + Fc, + N) else: - return Reaction(name, reaction_type, reactants, products, scaling_factor) + return Reaction( + name, + reaction_type, + reactants, + products, + scaling_factor) diff --git a/src/acom_music_box/music_box_reaction_rate.py b/src/acom_music_box/music_box_reaction_rate.py index 1bfae1fa..cead1da7 100644 --- a/src/acom_music_box/music_box_reaction_rate.py +++ b/src/acom_music_box/music_box_reaction_rate.py @@ -20,6 +20,6 @@ def __init__(self, reaction, rate): def __str__(self): return f"{self.reaction.name}: {self.rate}" - + def __repr__(self): return f"{self.reaction.name}: {self.rate}" diff --git a/src/acom_music_box/music_box_species.py b/src/acom_music_box/music_box_species.py index c353a9fc..18e28614 100644 --- a/src/acom_music_box/music_box_species.py +++ b/src/acom_music_box/music_box_species.py @@ -9,7 +9,14 @@ class Species: molecular_weight (float): The molecular weight of the species in kg mol^-1. """ - def __init__(self, name=None, absolute_tolerance=None, phase=None, molecular_weight=None, tracer_type=None, diffusion_coefficient=None): + def __init__( + self, + name=None, + absolute_tolerance=None, + phase=None, + molecular_weight=None, + tracer_type=None, + diffusion_coefficient=None): """ Initializes a new instance of the Species class. @@ -27,11 +34,12 @@ def __init__(self, name=None, absolute_tolerance=None, phase=None, molecular_wei self.molecular_weight = molecular_weight self.tracer_type = tracer_type self.diffusion_coefficient = diffusion_coefficient - + def __repr__(self): - return (f"Species(name={self.name!r}, absolute_tolerance={self.absolute_tolerance!r}, " - f"phase={self.phase!r}, molecular_weight={self.molecular_weight!r}, " - f"tracer_type={self.tracer_type!r}, diffusion_coefficient={self.diffusion_coefficient!r})") + return ( + f"Species(name={self.name!r}, absolute_tolerance={self.absolute_tolerance!r}, " + f"phase={self.phase!r}, molecular_weight={self.molecular_weight!r}, " + f"tracer_type={self.tracer_type!r}, diffusion_coefficient={self.diffusion_coefficient!r})") def __str__(self): return (f"Species: {self.name}, Phase: {self.phase}, " diff --git a/src/acom_music_box/music_box_species_concentration.py b/src/acom_music_box/music_box_species_concentration.py index 06d31a2c..346bae4e 100644 --- a/src/acom_music_box/music_box_species_concentration.py +++ b/src/acom_music_box/music_box_species_concentration.py @@ -20,7 +20,6 @@ def __init__(self, species, concentration): def __str__(self): return f"{self.species.name}: {self.concentration}" - + def __repr__(self): return f"{self.species.name}: {self.concentration}" - \ No newline at end of file diff --git a/src/acom_music_box/music_box_species_list.py b/src/acom_music_box/music_box_species_list.py index 600144ed..67142a4d 100644 --- a/src/acom_music_box/music_box_species_list.py +++ b/src/acom_music_box/music_box_species_list.py @@ -3,6 +3,7 @@ from typing import List from .music_box_species import Species + class SpeciesList: """ Represents a list of species with a relative tolerance. @@ -44,10 +45,16 @@ def from_UI_JSON(cls, UI_JSON): # TODO: Add phase and density to species - species_from_json.append(Species(name, absolute_tolerance, None, molecular_weight, None)) - + species_from_json.append( + Species( + name, + absolute_tolerance, + None, + molecular_weight, + None)) + return cls(species_from_json) - + @classmethod def from_config_JSON(cls, path_to_json, config_JSON): """ @@ -62,31 +69,42 @@ def from_config_JSON(cls, path_to_json, config_JSON): species_from_json = [] - #gets config file path - config_file_path = os.path.join(os.path.dirname(path_to_json), config_JSON['model components'][0]['configuration file']) + # gets config file path + config_file_path = os.path.join( + os.path.dirname(path_to_json), + config_JSON['model components'][0]['configuration file']) - #opnens config path to read species file + # opnens config path to read species file with open(config_file_path, 'r') as json_file: config = json.load(json_file) - #assumes species file is first in the list - if(len(config['camp-files']) > 0): - species_file_path = os.path.dirname(config_file_path) + "/" + config['camp-files'][0] + # assumes species file is first in the list + if (len(config['camp-files']) > 0): + species_file_path = os.path.dirname( + config_file_path) + "/" + config['camp-files'][0] with open(species_file_path, 'r') as species_file: species_data = json.load(species_file) - #loads species by names from camp files + # loads species by names from camp files for species in species_data['camp-data']: if species['type'] == 'CHEM_SPEC': tolerance = species.get('absolute tolerance', None) - molecular_weight = species.get('molecular weight [kg mol-1]', None) + molecular_weight = species.get( + 'molecular weight [kg mol-1]', None) phase = species.get('phase', None) - diffusion_coefficient = species.get('diffusion coefficient [m2 s-1]', None) + diffusion_coefficient = species.get( + 'diffusion coefficient [m2 s-1]', None) tracer_type = species.get('tracer type', None) name = species.get('name') - species_from_json.append(Species(name=name, absolute_tolerance=tolerance, molecular_weight=molecular_weight, phase=phase, diffusion_coefficient=diffusion_coefficient, tracer_type=tracer_type)) - - return cls(species_from_json) + species_from_json.append( + Species( + name=name, + absolute_tolerance=tolerance, + molecular_weight=molecular_weight, + phase=phase, + diffusion_coefficient=diffusion_coefficient, + tracer_type=tracer_type)) + return cls(species_from_json) def add_species(self, species): """ diff --git a/src/acom_music_box/utils.py b/src/acom_music_box/utils.py index 45d7bcce..072179b9 100644 --- a/src/acom_music_box/utils.py +++ b/src/acom_music_box/utils.py @@ -10,7 +10,7 @@ def convert_time(data, key): float: The time in seconds. """ time = None - + for unit in ['sec', 'min', 'hour', 'hr', 'day']: if f'{key} [{unit}]' in data: time_value = float(data[f'{key} [{unit}]']) @@ -25,6 +25,7 @@ def convert_time(data, key): break return time + def convert_pressure(data, key): """ Convert the pressure from the input data to Pascals. @@ -53,6 +54,7 @@ def convert_pressure(data, key): break return pressure + def convert_temperature(data, key): """ Convert the temperature from the input data to Kelvin. @@ -73,18 +75,19 @@ def convert_temperature(data, key): elif unit == 'C': temperature = temperature_value + 273.15 elif unit == 'F': - temperature = (temperature_value - 32) * 5/9 + 273.15 + temperature = (temperature_value - 32) * 5 / 9 + 273.15 break return temperature + def convert_concentration(data, key): """ Convert the concentration from the input data to molecules per cubic meter. - + Args: data (dict): The input data. key (str): The key for the concentration in the input data. - + Returns: float: The concentration in molecules per cubic meter. """ @@ -101,4 +104,4 @@ def convert_concentration(data, key): elif unit == 'molec cm-3': concentration = concentration_value * 1e3 / 6.02214076e23 break - return concentration \ No newline at end of file + return concentration diff --git a/tests/test_chapman.py b/tests/test_chapman.py index f9dfa69c..d73d0435 100644 --- a/tests/test_chapman.py +++ b/tests/test_chapman.py @@ -35,8 +35,10 @@ def test_run(self): model_output_header = model_output[0] test_output_header = test_output[0] - output_indices = [model_output_header.index(conc) for conc in concs_to_test] - test_output_indices = [test_output_header.index(conc) for conc in concs_to_test] + output_indices = [model_output_header.index( + conc) for conc in concs_to_test] + test_output_indices = [ + test_output_header.index(conc) for conc in concs_to_test] model_output_concs = [ [row[i] for i in output_indices] for row in model_output[1:] diff --git a/tests/test_full_gas_phase_mechanism.py b/tests/test_full_gas_phase_mechanism.py index 36ff313d..2f104601 100644 --- a/tests/test_full_gas_phase_mechanism.py +++ b/tests/test_full_gas_phase_mechanism.py @@ -92,8 +92,10 @@ def test_run(self): model_output_header = model_output[0] test_output_header = test_output[0] - output_indices = [model_output_header.index(conc) for conc in concs_to_test] - test_output_indices = [test_output_header.index(conc) for conc in concs_to_test] + output_indices = [model_output_header.index( + conc) for conc in concs_to_test] + test_output_indices = [ + test_output_header.index(conc) for conc in concs_to_test] model_output_concs = [ [row[i] for i in output_indices] for row in model_output[1:] diff --git a/tests/test_wall_loss.py b/tests/test_wall_loss.py index b96c4ad7..c93b62c7 100644 --- a/tests/test_wall_loss.py +++ b/tests/test_wall_loss.py @@ -27,8 +27,10 @@ def test_run(self): model_output_header = model_output[0] test_output_header = test_output[0] - output_indices = [model_output_header.index(conc) for conc in concs_to_test] - test_output_indices = [test_output_header.index(conc) for conc in concs_to_test] + output_indices = [model_output_header.index( + conc) for conc in concs_to_test] + test_output_indices = [ + test_output_header.index(conc) for conc in concs_to_test] model_output_concs = [ [row[i] for i in output_indices] for row in model_output[1:] From d457bbd7180f7c67682d365637dee4cc7fa1ca3f Mon Sep 17 00:00:00 2001 From: Kyle Shores Date: Thu, 15 Aug 2024 07:48:36 -0500 Subject: [PATCH 4/4] renaming docs, adding dockerfile, github action (#185) * renaming docs, adding dockerfile, github action * Update .github/workflows/gh_pages.yml Co-authored-by: Jiwon Gim <55209567+boulderdaze@users.noreply.github.com> * action triggers --------- Co-authored-by: Jiwon Gim <55209567+boulderdaze@users.noreply.github.com> --- .github/workflows/gh_pages.yml | 132 ++++++++++++++++++ docker/Dockerfile.docs | 24 ++++ {doc => docs}/Makefile | 0 {doc => docs}/make.bat | 0 {doc => docs}/requirements.txt | 0 {doc => docs}/source/_static/MusicBox.svg | 0 {doc => docs}/source/_static/custom.css | 0 {doc => docs}/source/_static/favicon.png | Bin {doc => docs}/source/_static/index_api.svg | 0 .../source/_static/index_contribute.svg | 0 .../source/_static/index_getting_started.svg | 0 .../source/_static/index_user_guide.svg | 0 {doc => docs}/source/_static/switcher.json | 0 {doc => docs}/source/api/acom_music_box.rst | 0 {doc => docs}/source/api/index.rst | 0 {doc => docs}/source/conf.py | 7 +- {doc => docs}/source/contributing/index.rst | 0 {doc => docs}/source/getting_started.rst | 0 {doc => docs}/source/index.rst | 0 {doc => docs}/source/user_guide/index.rst | 0 20 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/gh_pages.yml create mode 100644 docker/Dockerfile.docs rename {doc => docs}/Makefile (100%) rename {doc => docs}/make.bat (100%) rename {doc => docs}/requirements.txt (100%) rename {doc => docs}/source/_static/MusicBox.svg (100%) rename {doc => docs}/source/_static/custom.css (100%) rename {doc => docs}/source/_static/favicon.png (100%) rename {doc => docs}/source/_static/index_api.svg (100%) rename {doc => docs}/source/_static/index_contribute.svg (100%) rename {doc => docs}/source/_static/index_getting_started.svg (100%) rename {doc => docs}/source/_static/index_user_guide.svg (100%) rename {doc => docs}/source/_static/switcher.json (100%) rename {doc => docs}/source/api/acom_music_box.rst (100%) rename {doc => docs}/source/api/index.rst (100%) rename {doc => docs}/source/conf.py (91%) rename {doc => docs}/source/contributing/index.rst (100%) rename {doc => docs}/source/getting_started.rst (100%) rename {doc => docs}/source/index.rst (100%) rename {doc => docs}/source/user_guide/index.rst (100%) diff --git a/.github/workflows/gh_pages.yml b/.github/workflows/gh_pages.yml new file mode 100644 index 00000000..0299aef2 --- /dev/null +++ b/.github/workflows/gh_pages.yml @@ -0,0 +1,132 @@ +# Build and deploy documentation to GitHub Pages +name: GitHub Pages + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +env: + DEFAULT_BRANCH: "release" + +jobs: + build-and-deploy: + name: Build and deploy to gh-pages + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + lfs: true + + - name: Debugging information + run: | + echo "github.ref:" ${{github.ref}} + echo "github.event_name:" ${{github.event_name}} + echo "github.head_ref:" ${{github.head_ref}} + echo "github.base_ref:" ${{github.base_ref}} + set -x + git rev-parse --abbrev-ref HEAD + git branch + git branch -a + git remote -v + python -V + pip list --not-required + pip list + + # Clone and set up the old gh-pages branch + - name: Clone old gh-pages + if: ${{ github.event_name == 'push' }} + run: | + set -x + git fetch + ( git branch gh-pages remotes/origin/gh-pages && git clone . --branch=gh-pages _gh-pages/ ) || mkdir _gh-pages + rm -rf _gh-pages/.git/ + mkdir -p _gh-pages/branch/ + + # If a push and default branch, copy build to _gh-pages/ as the "main" + # deployment. + - name: Build and copy documentation (default branch) + if: | + contains(github.event_name, 'push') && + contains(github.ref, env.DEFAULT_BRANCH) + run: | + set -x + mkdir -p _build/html/versions + + # create two copies of the documentation + # 1. the frozen version, represented as vX.X in the version switcher + docker build -t music_box -f docker/Dockerfile.docs . + id=$(docker create music_box) + docker cp $id:/build/docs/build/html tmpdocs + docker rm -v $id + version=$(sed -nr "s/^release = f'v(.+)\{suffix\}'.*$/\1/p" docs/source/conf.py) + mv tmpdocs _build/html/versions/${version} + + # 2. stable, represented as vX.X (stable) in the version switcher + # edit conf.py to produce a version string that looks like vX.X (stable) + docker build -t music_box -f docker/Dockerfile.docs --build-arg SUFFIX=" (stable)" . + id=$(docker create music_box) + docker cp $id:/build/docs/build/html tmpdocs + docker rm -v $id + mv tmpdocs _build/html/versions/stable + # Delete everything under _gh-pages/ that is from the + # primary branch deployment. Excludes the other branches + # _gh-pages/branch-* paths, and not including + # _gh-pages itself. + find _gh-pages/ -mindepth 1 ! -path '_gh-pages/branch*' ! -path '_gh-pages/versions*' -delete + rsync -a _build/html/versions/stable/* _gh-pages/ + mkdir -p _gh-pages/versions + rsync -a _build/html/versions/* _gh-pages/versions + # mv docs/switcher.json _gh-pages + + # If a push and not on default branch, then copy the build to + # _gh-pages/branch/$brname (transforming '/' into '--') + - name: Build and copy documentation (branch) + if: | + contains(github.event_name, 'push') && + !contains(github.ref, env.DEFAULT_BRANCH) + run: | + set -x + docker build -t music_box -f docker/Dockerfile.docs . + id=$(docker create music_box) + docker cp $id:/music-box/docs/build/html tmpdocs + docker rm -v $id + brname="${{github.ref}}" + brname="${brname##refs/heads/}" + brdir=${brname//\//--} # replace '/' with '--' + rm -rf _gh-pages/branch/${brdir} + rsync -a tmpdocs/ _gh-pages/branch/${brdir} + + # Go through each branch in _gh-pages/branch/, if it's not a + # ref, then delete it. + - name: Delete old feature branches + if: ${{ github.event_name == 'push' }} + run: | + set -x + for brdir in `ls _gh-pages/branch/` ; do + brname=${brdir//--/\/} # replace '--' with '/' + if ! git show-ref remotes/origin/$brname ; then + echo "Removing $brdir" + rm -r _gh-pages/branch/$brdir/ + fi + done + + # Add the .nojekyll file + - name: nojekyll + if: ${{ github.event_name == 'push' }} + run: | + touch _gh-pages/.nojekyll + + # Deploy + # https://github.com/peaceiris/actions-gh-pages + - name: Deploy + uses: peaceiris/actions-gh-pages@v3 + if: ${{ github.event_name == 'push' }} + with: + publish_branch: gh-pages + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: _gh-pages/ + force_orphan: true \ No newline at end of file diff --git a/docker/Dockerfile.docs b/docker/Dockerfile.docs new file mode 100644 index 00000000..11cc4dc3 --- /dev/null +++ b/docker/Dockerfile.docs @@ -0,0 +1,24 @@ +FROM fedora:37 + +RUN dnf -y update \ + && dnf -y install \ + git \ + make \ + python3 \ + python3-pip \ + && dnf clean all + +COPY . /music-box/ + +WORKDIR /music-box + +RUN pip3 install -e . + +ARG SUFFIX="" +ENV SWITCHER_SUFFIX=$SUFFIX + +RUN echo "The suffix is '$SWITCHER_SUFFIX'" + +RUN cd docs \ + && pip install -r requirements.txt \ + && make html \ No newline at end of file diff --git a/doc/Makefile b/docs/Makefile similarity index 100% rename from doc/Makefile rename to docs/Makefile diff --git a/doc/make.bat b/docs/make.bat similarity index 100% rename from doc/make.bat rename to docs/make.bat diff --git a/doc/requirements.txt b/docs/requirements.txt similarity index 100% rename from doc/requirements.txt rename to docs/requirements.txt diff --git a/doc/source/_static/MusicBox.svg b/docs/source/_static/MusicBox.svg similarity index 100% rename from doc/source/_static/MusicBox.svg rename to docs/source/_static/MusicBox.svg diff --git a/doc/source/_static/custom.css b/docs/source/_static/custom.css similarity index 100% rename from doc/source/_static/custom.css rename to docs/source/_static/custom.css diff --git a/doc/source/_static/favicon.png b/docs/source/_static/favicon.png similarity index 100% rename from doc/source/_static/favicon.png rename to docs/source/_static/favicon.png diff --git a/doc/source/_static/index_api.svg b/docs/source/_static/index_api.svg similarity index 100% rename from doc/source/_static/index_api.svg rename to docs/source/_static/index_api.svg diff --git a/doc/source/_static/index_contribute.svg b/docs/source/_static/index_contribute.svg similarity index 100% rename from doc/source/_static/index_contribute.svg rename to docs/source/_static/index_contribute.svg diff --git a/doc/source/_static/index_getting_started.svg b/docs/source/_static/index_getting_started.svg similarity index 100% rename from doc/source/_static/index_getting_started.svg rename to docs/source/_static/index_getting_started.svg diff --git a/doc/source/_static/index_user_guide.svg b/docs/source/_static/index_user_guide.svg similarity index 100% rename from doc/source/_static/index_user_guide.svg rename to docs/source/_static/index_user_guide.svg diff --git a/doc/source/_static/switcher.json b/docs/source/_static/switcher.json similarity index 100% rename from doc/source/_static/switcher.json rename to docs/source/_static/switcher.json diff --git a/doc/source/api/acom_music_box.rst b/docs/source/api/acom_music_box.rst similarity index 100% rename from doc/source/api/acom_music_box.rst rename to docs/source/api/acom_music_box.rst diff --git a/doc/source/api/index.rst b/docs/source/api/index.rst similarity index 100% rename from doc/source/api/index.rst rename to docs/source/api/index.rst diff --git a/doc/source/conf.py b/docs/source/conf.py similarity index 91% rename from doc/source/conf.py rename to docs/source/conf.py index a84d7e6d..280f1f1f 100644 --- a/doc/source/conf.py +++ b/docs/source/conf.py @@ -12,11 +12,14 @@ # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information -release = acom_music_box.__version__ -project = f'Music Box ({release})' +version = acom_music_box.__version__ +project = f'Music Box ({version})' copyright = f'2024-{datetime.datetime.now().year}, NCAR/UCAR' author = 'NCAR/UCAR' +suffix = os.getenv("SWITCHER_SUFFIX", "") +release = f'{version}{suffix}' + # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/doc/source/contributing/index.rst b/docs/source/contributing/index.rst similarity index 100% rename from doc/source/contributing/index.rst rename to docs/source/contributing/index.rst diff --git a/doc/source/getting_started.rst b/docs/source/getting_started.rst similarity index 100% rename from doc/source/getting_started.rst rename to docs/source/getting_started.rst diff --git a/doc/source/index.rst b/docs/source/index.rst similarity index 100% rename from doc/source/index.rst rename to docs/source/index.rst diff --git a/doc/source/user_guide/index.rst b/docs/source/user_guide/index.rst similarity index 100% rename from doc/source/user_guide/index.rst rename to docs/source/user_guide/index.rst