Skip to content

Commit

Permalink
Implement MILP scheme of Buerger et al. for error canceling reactions
Browse files Browse the repository at this point in the history
  • Loading branch information
amarkpayne committed Aug 10, 2019
1 parent f73771e commit cc75360
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 0 deletions.
110 changes: 110 additions & 0 deletions arkane/isodesmic.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@

from __future__ import division

import signal

from lpsolve55 import lpsolve, EQ, LE
import numpy as np
import pyomo.environ as pyo

from rmgpy.molecule import Molecule
from rmgpy.quantity import ScalarQuantity
Expand Down Expand Up @@ -265,6 +269,112 @@ def __init__(self, target, reference_set, conserve_bonds, conserve_ring_size):
self.target_constraint, self.constraint_matrix = self.constraints.calculate_constraints()
self.reference_species = self.constraints.reference_species

def _find_error_canceling_reaction(self, reference_subset, milp_software='lpsolve'):
"""
Automatically find a valid error canceling reaction given a subset of the available benchmark species. This
is done by solving a mixed integer linear programming (MILP) problem similiar to
Buerger et al. (https://doi.org/10.1016/j.combustflame.2017.08.013)
Args:
reference_subset (list): A list of indices from self.reference_species that can participate in the reaction
milp_software (str, optional): 'lpsolve' (default) or 'pyomo'. lpsolve is usually faster.
Returns:
ErrorCancelingReaction: reaction with the target species (if a valid reaction is found, else `None`)
"""
# Define the constraints based on the provided subset
c_matrix = np.take(self.constraint_matrix, reference_subset, axis=0)
c_matrix = np.tile(c_matrix, (2, 1))
sum_constraints = np.sum(c_matrix, 1, dtype=int)
targets = -1*self.target_constraint
m = c_matrix.shape[0]
n = c_matrix.shape[1]
split = int(m/2)
solution = None

if milp_software == 'pyomo':
# Setup the MILP problem using pyomo
lp_model = pyo.ConcreteModel()
lp_model.i = pyo.RangeSet(0, m - 1)
lp_model.j = pyo.RangeSet(0, n - 1)
lp_model.r = pyo.RangeSet(0, split-1) # indices before the split correspond to reactants
lp_model.p = pyo.RangeSet(split, m - 1) # indices after the split correspond to products
lp_model.v = pyo.Var(lp_model.i, domain=pyo.NonNegativeIntegers) # The stoich. coef. we are solving for
lp_model.c = pyo.Param(lp_model.i, lp_model.j, initialize=lambda _, i, j: c_matrix[i, j])
lp_model.s = pyo.Param(lp_model.i, initialize=lambda _, i: sum_constraints[i])
lp_model.t = pyo.Param(lp_model.j, initialize=lambda _, j: targets[j])

def obj_expression(model):
return pyo.summation(model.v, model.s, index=model.i)

lp_model.obj = pyo.Objective(rule=obj_expression)

def constraint_rule(model, j):
return sum(model.v[i] * model.c[i, j] for i in model.r) - \
sum(model.v[i] * model.c[i, j] for i in model.p) == model.t[j]

lp_model.constraints = pyo.Constraint(lp_model.j, rule=constraint_rule)

# Solve the MILP problem using the CBC MILP solver (https://www.coin-or.org/Cbc/)
opt = pyo.SolverFactory('glpk')
results = opt.solve(lp_model)

# Return None if a valid reaction is not found
if results.solver.status != pyo.SolverStatus.ok:
return None

# Extract the solution and find the species with non-zero stoichiometric coefficients
solution = lp_model.v.extract_values().values()

elif milp_software == 'lpsolve':
# Save the current signal handler
sig = signal.getsignal(signal.SIGINT)

# Setup the MILP problem using lpsolve
lp = lpsolve('make_lp', 0, m)
lpsolve('set_verbose', lp, 2) # Reduce the logging from lpsolve
lpsolve('set_obj_fn', lp, sum_constraints)
lpsolve('set_minim', lp)

for j in range(n):
lpsolve('add_constraint', lp, np.concatenate((c_matrix[:split, j], -1*c_matrix[split:, j])), EQ,
targets[j])

lpsolve('add_constraint', lp, np.ones(m), LE, 20) # Use at most 20 species (including replicates)
lpsolve('set_timeout', lp, 5) # Move on if lpsolve can't find a solution quickly

# Constrain v_i to be 4 or less
for i in range(m):
lpsolve('set_upbo', lp, i, 4)

# All v_i must be integers
lpsolve('set_int', lp, [True]*m)

status = lpsolve('solve', lp)

# Reset signal handling since lpsolve changed it
try:
signal.signal(signal.SIGINT, sig)
except ValueError:
# This is not being run in the main thread, so we cannot reset signal
pass

if status != 0:
return None

else:
_, solution = lpsolve('get_solution', lp)[:2]

reaction = ErrorCancelingReaction(self.target, dict())
for index, v in enumerate(solution):
if v > 0:
if index < split:
reaction.species.update({self.reference_species[reference_subset[index]]: -v})
else:
reaction.species.update({self.reference_species[reference_subset[index % split]]: v})

return reaction


class IsodesmicScheme(ErrorCancelingScheme):
"""
Expand Down
19 changes: 19 additions & 0 deletions arkane/isodesmicTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,9 @@ class TestErrorCancelingScheme(unittest.TestCase):

def setUp(self):
self.propene = ErrorCancelingSpecies(Molecule(SMILES='CC=C'), (100, 'kJ/mol'), 'test')
self.propane = ErrorCancelingSpecies(Molecule(SMILES='CCC'), (75, 'kJ/mol'), 'test')
self.butane = ErrorCancelingSpecies(Molecule(SMILES='CCCC'), (150, 'kJ/mol'), 'test')
self.butene = ErrorCancelingSpecies(Molecule(SMILES='C=CCC'), (175, 'kJ/mol'), 'test')
self.benzene = ErrorCancelingSpecies(Molecule(SMILES='c1ccccc1'), (-50, 'kJ/mol'), 'test')
self.caffeine = ErrorCancelingSpecies(Molecule(SMILES='CN1C=NC2=C1C(=O)N(C(=O)N2C)C'), (300, 'kJ/mol'), 'test')
self.ethyne = ErrorCancelingSpecies(Molecule(SMILES='C#C'), (200, 'kJ/mol'), 'test')
Expand All @@ -199,6 +201,23 @@ def test_creating_error_canceling_schemes(self):

self.assertEqual(isodesmic_scheme.reference_species, [self.butane, self.benzene])

def test_find_error_canceling_reaction(self):
"""
Test that the MILP problem can be solved to find a single isodesmic reaction
"""
scheme = IsodesmicScheme(self.propene, [self.propane, self.butane, self.butene, self.caffeine, self.ethyne])

# Note that caffeine and ethyne will not be allowed, so for the full set the indices are [0, 1, 2]
rxn = scheme._find_error_canceling_reaction([0, 1, 2], milp_software='lpsolve')
self.assertEqual(rxn.species[self.butane], -1)
self.assertEqual(rxn.species[self.propane], 1)
self.assertEqual(rxn.species[self.butene], 1)

rxn = scheme._find_error_canceling_reaction([0, 1, 2], milp_software='pyomo')
self.assertEqual(rxn.species[self.butane], -1)
self.assertEqual(rxn.species[self.propane], 1)
self.assertEqual(rxn.species[self.butene], 1)


if __name__ == '__main__':
unittest.main(testRunner=unittest.TextTestRunner(verbosity=2))
3 changes: 3 additions & 0 deletions environment_linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ dependencies:
- cairo
- cairocffi
- cantera >=2.3.0
- coincbc
- coolprop
- coverage
- cython >=0.25.2
- dde
- ffmpeg
- glpk
- gprof2dot
- graphviz
- jinja2
Expand All @@ -37,6 +39,7 @@ dependencies:
- pydqed >=1.0.0
- pygpu
- pymongo
- pyomo
- pyparsing
- pyrdl
- python >=2.7
Expand Down
3 changes: 3 additions & 0 deletions environment_mac.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ dependencies:
- cairo
- cairocffi
- cantera >=2.3.0
- coincbc
- coolprop
- coverage
- cython >=0.25.2
- dde
- ffmpeg
- glpk
- gprof2dot
- graphviz
- jinja2
Expand All @@ -37,6 +39,7 @@ dependencies:
- pydqed >=1.0.0
- pygpu
- pymongo
- pyomo
- pyparsing
- pyrdl
- python >=2.7
Expand Down
3 changes: 3 additions & 0 deletions environment_windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ dependencies:
- cairo
- cairocffi
- cantera >=2.3.0
- coincbc
- coolprop
- coverage
- cython >=0.25.2
- dde
- ffmpeg
- glpk
- gprof2dot
- graphviz
- jinja2
Expand All @@ -38,6 +40,7 @@ dependencies:
- pydqed >=1.0.0
- pygpu
- pymongo
- pyomo
- pyparsing
- pyrdl
- python >=2.7
Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ MarkupSafe # this makes Jinja2 faster
Jinja2 # this is for rendering the HTML output files
cairocffi
yaml
pyomo # For MILP problems such as Clar structure generation and automatic generation of error canceling reactions
coincbc # MILP solver

# For postprocessing the profiling data
argparse
Expand Down

0 comments on commit cc75360

Please sign in to comment.