Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Detect and display error for duplicate reaction names (keys) - Issue #235 #241

Merged
merged 12 commits into from
Sep 24, 2024
63 changes: 63 additions & 0 deletions src/acom_music_box/music_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,67 @@ def add_evolving_condition(self, time_point, conditions):
time=[time_point], conditions=[conditions])
self.evolvingConditions.append(evolving_condition)

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.

Args:
path_to_config (str): The path to CAMP configuration directory.

Returns:
None
"""
# Create a solver object using the configuration file
self.solver = musica.create_solver(
path_to_config,
solver_type,
number_of_grid_cells)

def check_config(self, boxConfigPath):
"""
Verifies correct configuration of the MusicBox object.
There is intentionally no check for the presence of a solver;
this test function is for the loaded configuration only.

Args:
boxConfigPath = filename and path of MusicBox configuration file
This filename is supplied only for the error message;
the configuration should already be loaded.

Returns:
True if all checks passed
Throws error for the first check failed.
"""

# look for duplicate reaction names
if (self.initial_conditions):
if (self.initial_conditions.reaction_rates):
reactionNames = []
for rate in self.initial_conditions.reaction_rates:
# watch out for Nones in here
if not rate.reaction:
continue
if not rate.reaction.name:
continue
reactionNames.append(rate.reaction.name)

# look for name already seen
seen = set()
dupNames = [name for name in reactionNames if name in seen or seen.add(name)]

if (len(dupNames) > 0):
# inform user of the error and its remedy
errString = ("Error: Duplicate reaction names specified within {}: {}."
.format(boxConfigPath, dupNames))
errString += " Please remove or rename the duplicates."
raise Exception(errString)

return(True)

def solve(self, output_path=None, callback=None):
"""
Solves the box model simulation and optionally writes the output to a file.
Expand Down Expand Up @@ -260,6 +321,8 @@ def loadJson(self, path_to_json):
self.evolving_conditions = EvolvingConditions.from_config_JSON(
path_to_json, data, self.species_list, self.reaction_list)

self.check_config(os.path.join(os.getcwd(), path_to_json))

camp_path = os.path.join(
os.path.dirname(path_to_json),
self.config_file)
Expand Down
33 changes: 33 additions & 0 deletions tests/unit/test_duplicate_reactions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from acom_music_box import MusicBox, Reaction, ReactionRate, Conditions
from acom_music_box.reaction_list import ReactionList

import pytest


class TestDuplicateReactions:
def test_run(self):
# set up dummy reactions
abc123 = ReactionRate(Reaction("abc"), 12.3)
def456 = ReactionRate(Reaction("def"), 45.6)
abc789 = ReactionRate(Reaction("abc"), 78.9)

# Pass: unique reaction names
pass_reactions = [abc123, def456]
pass_conditions = Conditions(reaction_rates=pass_reactions)
box_model = MusicBox(initial_conditions=pass_conditions)
assert box_model.check_config("Loaded from string.")

# Fail: duplicate reaction names
fail_reactions = reactions=[abc123, abc789]
fail_conditions = Conditions(reaction_rates=fail_reactions)
box_model = MusicBox(initial_conditions=fail_conditions) # new instance

# verify that the fail exception was properly raised
with pytest.raises(Exception):
box_model.check_config("Loaded from string.")


if __name__ == "__main__":
test = TestDuplicateReactions()
test.test_run()