Add causality and K-best example
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
# Causality and K-best
|
||||
|
||||
This directory is a self-contained example based on the current Butadiene code.
|
||||
Nothing in `ILP/butadien` or `mod/butadien` is changed.
|
||||
|
||||
## 1. Enumerate K best supports with Gurobi
|
||||
|
||||
Set `K` near the top of `butadiensynthesis_k_best.py` and run:
|
||||
|
||||
```bash
|
||||
python3 butadiensynthesis_k_best.py
|
||||
```
|
||||
|
||||
The script is the existing Butadiene ILP with one small generalisation: instead of
|
||||
computing only the best and second-best support, it repeatedly excludes every
|
||||
previously found support. It writes all solutions to `gurobi_flows.json`.
|
||||
|
||||
Thus "K best" here means K best distinct reaction supports, matching the meaning
|
||||
of the existing second-best code.
|
||||
|
||||
## 2. Check the Gurobi solutions for causality
|
||||
|
||||
Activate the MØD environment and run this directory with MØD:
|
||||
|
||||
```bash
|
||||
mod -f check_causality.py
|
||||
```
|
||||
|
||||
The checker reads `gurobi_flows.json`, maps each reaction to the current MØD
|
||||
derivation graph by educt/product SMILES, fixes exactly that hyperflow, and runs
|
||||
`causality.RealisabilityQuery`.
|
||||
|
||||
Typical output is:
|
||||
|
||||
```text
|
||||
solution 1: causally realisable: YES
|
||||
solution 2: causally realisable: NO
|
||||
...
|
||||
```
|
||||
|
||||
`butadienNetwork.py` and `graph.py` are local helper copies for constructing
|
||||
the same Butadiene MØD network.
|
||||
@@ -0,0 +1,232 @@
|
||||
config.common.ignoreDeprecation = False
|
||||
|
||||
config.ilp.solver="CPLEX"
|
||||
#import mod
|
||||
#from mod import *
|
||||
import networkx as nx
|
||||
|
||||
# import Graph class from graph.py
|
||||
from graph import GraphObj
|
||||
|
||||
#ethylen = Graph.fromGMLString(
|
||||
"""graph [
|
||||
node [ id 0 label "H" ]
|
||||
node [ id 1 label "H" ]
|
||||
node [ id 2 label "C" ]
|
||||
node [ id 3 label "C" ]
|
||||
node [ id 4 label "H" ]
|
||||
node [ id 5 label "H" ]
|
||||
edge [ source 0 target 2 label "-" ]
|
||||
edge [ source 1 target 2 label "-" ]
|
||||
edge [ source 2 target 3 label "=" ]
|
||||
edge [ source 3 target 4 label "-" ]
|
||||
edge [ source 3 target 5 label "-" ]
|
||||
]"""
|
||||
#, name="Ethylen")
|
||||
|
||||
butadien = Graph.fromGMLString(
|
||||
"""graph [
|
||||
node [ id 0 label "C" ]
|
||||
node [ id 1 label "C" ]
|
||||
node [ id 2 label "C" ]
|
||||
node [ id 3 label "C" ]
|
||||
node [ id 4 label "H" ]
|
||||
node [ id 5 label "H" ]
|
||||
node [ id 6 label "H" ]
|
||||
node [ id 7 label "H" ]
|
||||
node [ id 8 label "H" ]
|
||||
node [ id 9 label "H" ]
|
||||
edge [ source 0 target 1 label "=" ]
|
||||
edge [ source 1 target 2 label "-" ]
|
||||
edge [ source 2 target 3 label "=" ]
|
||||
edge [ source 0 target 4 label "-" ]
|
||||
edge [ source 0 target 5 label "-" ]
|
||||
edge [ source 1 target 6 label "-" ]
|
||||
edge [ source 2 target 7 label "-" ]
|
||||
edge [ source 3 target 8 label "-" ]
|
||||
edge [ source 3 target 9 label "-" ]
|
||||
]"""
|
||||
, name="Butadien")
|
||||
|
||||
#benzene = Graph.fromSMILES('c1=cc=cc=c1')
|
||||
|
||||
#benzene = Graph.fromGMLString(
|
||||
"""graph [
|
||||
node [ id 0 label "C" ]
|
||||
node [ id 1 label "H" ]
|
||||
node [ id 2 label "C" ]
|
||||
node [ id 3 label "H" ]
|
||||
node [ id 4 label "C" ]
|
||||
node [ id 5 label "H" ]
|
||||
node [ id 6 label "C" ]
|
||||
node [ id 7 label "H" ]
|
||||
node [ id 8 label "C" ]
|
||||
node [ id 9 label "H" ]
|
||||
node [ id 10 label "C" ]
|
||||
node [ id 11 label "H" ]
|
||||
edge [ source 0 target 1 label "-" ]
|
||||
edge [ source 0 target 2 label "=" ]
|
||||
edge [ source 2 target 3 label "-" ]
|
||||
edge [ source 2 target 4 label "-" ]
|
||||
edge [ source 4 target 5 label "-" ]
|
||||
edge [ source 4 target 6 label "=" ]
|
||||
edge [ source 6 target 7 label "-" ]
|
||||
edge [ source 6 target 8 label "-" ]
|
||||
edge [ source 8 target 9 label "-" ]
|
||||
edge [ source 8 target 10 label "=" ]
|
||||
edge [ source 10 target 11 label "-" ]
|
||||
edge [ source 10 target 0 label "-" ]
|
||||
]"""
|
||||
#, name="Benzene")
|
||||
|
||||
|
||||
#pentadien = Graph.fromGMLString(
|
||||
"""graph
|
||||
[
|
||||
node [ id 0 label "C" ]
|
||||
node [ id 1 label "C" ]
|
||||
node [ id 2 label "C" ]
|
||||
node [ id 3 label "C" ]
|
||||
node [ id 4 label "H" ]
|
||||
node [ id 5 label "H" ]
|
||||
node [ id 6 label "H" ]
|
||||
node [ id 7 label "H" ]
|
||||
node [ id 8 label "H" ]
|
||||
node [ id 9 label "C" ]
|
||||
node [ id 10 label "H" ]
|
||||
node [ id 11 label "H" ]
|
||||
node [ id 12 label "H" ]
|
||||
edge [ source 0 target 1 label "=" ]
|
||||
edge [ source 1 target 2 label "-" ]
|
||||
edge [ source 2 target 3 label "=" ]
|
||||
edge [ source 0 target 4 label "-" ]
|
||||
edge [ source 0 target 5 label "-" ]
|
||||
edge [ source 1 target 6 label "-" ]
|
||||
edge [ source 2 target 7 label "-" ]
|
||||
edge [ source 3 target 8 label "-" ]
|
||||
edge [ source 3 target 9 label "-" ]
|
||||
edge [ source 9 target 10 label "-" ]
|
||||
edge [ source 9 target 11 label "-" ]
|
||||
edge [ source 9 target 12 label "-" ]
|
||||
]
|
||||
"""
|
||||
#, name="Pentadien")
|
||||
|
||||
restswap = Rule.fromGMLString(
|
||||
"""rule [
|
||||
left [
|
||||
edge [ source 1 target 2 label "=" ]
|
||||
edge [ source 3 target 4 label "=" ]
|
||||
]
|
||||
context [
|
||||
node [ id 1 label "C" ]
|
||||
node [ id 2 label "C"]
|
||||
node [ id 3 label "C"]
|
||||
node [ id 4 label "C"]
|
||||
]
|
||||
right [
|
||||
edge [ source 1 target 3 label "=" ]
|
||||
edge [ source 2 target 4 label "=" ]
|
||||
]
|
||||
]"""
|
||||
)
|
||||
|
||||
dielsalder = Rule.fromGMLString(
|
||||
"""rule [
|
||||
left [
|
||||
edge [ source 1 target 2 label "=" ]
|
||||
edge [ source 2 target 3 label "-" ]
|
||||
edge [ source 3 target 4 label "=" ]
|
||||
edge [ source 5 target 6 label "=" ]
|
||||
|
||||
]
|
||||
context [
|
||||
node [ id 1 label "C" ]
|
||||
node [ id 2 label "C"]
|
||||
node [ id 3 label "C"]
|
||||
node [ id 4 label "C"]
|
||||
node [ id 5 label "C"]
|
||||
node [ id 6 label "C"]
|
||||
]
|
||||
right [
|
||||
edge [ source 1 target 2 label "-" ]
|
||||
edge [ source 2 target 3 label "=" ]
|
||||
edge [ source 3 target 4 label "-" ]
|
||||
edge [ source 4 target 5 label "-" ]
|
||||
edge [ source 5 target 6 label "-" ]
|
||||
edge [ source 6 target 1 label "-" ]
|
||||
]
|
||||
]"""
|
||||
)
|
||||
|
||||
def cyclesizes(g):
|
||||
nxGraph = GraphObj(g).nx_graph
|
||||
|
||||
#Restriction to E/Z? Benzen can't be created or consumed by the reaction due to steric difficulties
|
||||
|
||||
#Chain restrictions: (No steric reason)
|
||||
for nodestart in nxGraph.nodes:
|
||||
for nodeend in nxGraph.nodes:
|
||||
if nodeend != nodestart:
|
||||
paths = sorted(list(nx.all_shortest_paths(nxGraph, nodestart, nodeend)))
|
||||
for path in paths:
|
||||
if len(path) > 10: #First node included in length
|
||||
return True
|
||||
|
||||
#Cycle restrictions:
|
||||
cycles = sorted(list(nx.chordless_cycles(nxGraph)))
|
||||
cycle_lengths = [len(x) for x in cycles]
|
||||
if cycles == []:
|
||||
return False
|
||||
#One Atom can't be in four different cycles
|
||||
for vertice in nxGraph.nodes:
|
||||
counter = 0
|
||||
for cycle in cycles:
|
||||
if vertice in cycle:
|
||||
counter += 1
|
||||
if counter >= 4:
|
||||
return True
|
||||
#One Cycle can't overlapp with another on over 2 Connection points
|
||||
for cycle in cycles:
|
||||
for cycleref in cycles:
|
||||
if cycle != cycleref and len(list(set(cycle) & set(cycleref))) >= 3:
|
||||
return True
|
||||
#Only cordless cycles of length 5,6 and 7 are acceptable
|
||||
if min(cycle_lengths) >= 6 and max(cycle_lengths) <=6:
|
||||
return False
|
||||
return True
|
||||
|
||||
#Disallows Allenes (Two Doublebonds on same carbon)
|
||||
def doubledoublebond(g):
|
||||
for vertice in g.vertices:
|
||||
if (vertice.stringLabel == "C" and vertice.degree == 2): # and vertice.edges.label == ["=", "="]
|
||||
for edge in vertice.incidentEdges:
|
||||
print(type(edge.bondType))
|
||||
bonds = [type(edge.bondType) for edge in vertice.incidentEdges]
|
||||
if (bonds[0] == bonds[1] and len(bonds) == 2):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def restriction(dg):
|
||||
for a in dg.right:
|
||||
if a.vLabelCount("C") > 10:
|
||||
return False
|
||||
if doubledoublebond(a):
|
||||
return False
|
||||
if cyclesizes(a):
|
||||
return False
|
||||
return True
|
||||
|
||||
ethylen = Graph.fromSMILES('C=C')
|
||||
goal = Graph.fromSMILES('C1C=CC=CC=1')
|
||||
dg = DG(graphDatabase=inputGraphs)
|
||||
dg.build().execute(
|
||||
addSubset(inputGraphs)
|
||||
>> rightPredicate[
|
||||
restriction
|
||||
](
|
||||
repeat(revive(inputRules)) #Revive not necessary
|
||||
)
|
||||
)
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import gurobipy as gp
|
||||
from gurobipy import GRB, Model, quicksum
|
||||
|
||||
HYPERGRAPH = {
|
||||
0: (['Butadien'], []),
|
||||
2: (['p_{0,0}'], []),
|
||||
3: (['p_{0,1}'], []),
|
||||
4: (['Butadien', 'Butadien'], ['p_{0,0}', 'p_{0,1}']),
|
||||
6: (['p_{0,2}'], []),
|
||||
7: (['Butadien', 'Butadien'], ['p_{0,2}']),
|
||||
10: (['p_{0,0}', 'p_{0,1}'], ['Butadien', 'Butadien']),
|
||||
12: (['p_{0,3}'], []),
|
||||
13: (['p_{0,0}', 'p_{0,2}'], ['p_{0,3}']),
|
||||
17: (['p_{0,4}'], []),
|
||||
18: (['p_{0,1}', 'p_{0,1}'], ['Butadien', 'p_{0,4}']),
|
||||
19: (['p_{0,5}'], []),
|
||||
20: (['p_{0,1}', 'p_{0,2}'], ['Butadien', 'p_{0,5}']),
|
||||
23: (['Butadien', 'p_{0,1}'], ['p_{0,0}', 'p_{0,4}']),
|
||||
26: (['Butadien', 'p_{0,2}'], ['p_{0,0}', 'p_{0,5}']),
|
||||
27: (['p_{0,6}'], []),
|
||||
28: (['p_{0,0}', 'p_{0,1}'], ['p_{0,6}']),
|
||||
29: (['p_{0,7}'], []),
|
||||
30: (['Butadien', 'p_{0,0}'], ['p_{0,7}']),
|
||||
31: (['p_{0,8}'], []),
|
||||
32: (['Butadien', 'p_{0,1}'], ['p_{0,8}']),
|
||||
33: (['Butadien', 'p_{0,1}'], ['p_{0,5}']),
|
||||
34: (['p_{0,9}'], []),
|
||||
35: (['Butadien', 'p_{0,1}'], ['p_{0,9}']),
|
||||
36: (['p_{0,10}'], []),
|
||||
37: (['Butadien', 'p_{0,1}'], ['p_{0,10}']),
|
||||
38: (['p_{0,3}'], ['p_{0,0}', 'p_{0,2}']),
|
||||
41: (['p_{0,11}'], []),
|
||||
42: (['p_{0,4}'], ['p_{0,0}', 'p_{0,11}']),
|
||||
43: (['p_{0,12}'], []),
|
||||
44: (['p_{0,5}'], ['p_{0,12}']),
|
||||
63: (['p_{0,13}'], []),
|
||||
64: (['p_{0,4}', 'p_{0,6}'], ['p_{0,1}', 'p_{0,13}']),
|
||||
65: (['p_{0,2}', 'p_{0,4}'], ['p_{0,1}', 'p_{0,5}']),
|
||||
66: (['p_{0,0}', 'p_{0,4}'], ['Butadien', 'p_{0,1}']),
|
||||
67: (['Butadien', 'p_{0,4}'], ['p_{0,1}', 'p_{0,1}']),
|
||||
77: (['p_{0,5}', 'p_{0,6}'], ['p_{0,2}', 'p_{0,13}']),
|
||||
79: (['p_{0,0}', 'p_{0,5}'], ['Butadien', 'p_{0,2}']),
|
||||
80: (['Butadien', 'p_{0,5}'], ['p_{0,1}', 'p_{0,2}']),
|
||||
83: (['p_{0,1}', 'p_{0,5}'], ['p_{0,2}', 'p_{0,4}']),
|
||||
96: (['Butadien', 'p_{0,6}'], ['p_{0,0}', 'p_{0,13}']),
|
||||
97: (['p_{0,1}', 'p_{0,6}'], ['Butadien', 'p_{0,13}']),
|
||||
99: (['p_{0,14}'], []),
|
||||
100: (['p_{0,0}', 'p_{0,6}'], ['p_{0,14}']),
|
||||
101: (['p_{0,15}'], []),
|
||||
102: (['p_{0,0}', 'p_{0,7}'], ['p_{0,15}']),
|
||||
121: (['p_{0,0}', 'p_{0,4}'], ['p_{0,13}']),
|
||||
122: (['p_{0,16}'], []),
|
||||
123: (['p_{0,0}', 'p_{0,4}'], ['p_{0,16}']),
|
||||
124: (['p_{0,17}'], []),
|
||||
125: (['Butadien', 'p_{0,7}'], ['p_{0,17}']),
|
||||
127: (['p_{0,12}'], ['p_{0,5}']),
|
||||
130: (['p_{0,14}'], ['p_{0,0}', 'p_{0,6}']),
|
||||
132: (['p_{0,15}'], ['p_{0,0}', 'p_{0,7}']),
|
||||
135: (['p_{0,0}', 'p_{0,11}'], ['p_{0,4}']),
|
||||
153: (['Butadien', 'p_{0,13}'], ['p_{0,1}', 'p_{0,6}']),
|
||||
155: (['p_{0,0}', 'p_{0,13}'], ['Butadien', 'p_{0,6}']),
|
||||
157: (['p_{0,1}', 'p_{0,13}'], ['p_{0,4}', 'p_{0,6}']),
|
||||
158: (['p_{0,2}', 'p_{0,13}'], ['p_{0,5}', 'p_{0,6}']),
|
||||
210: (['p_{0,18}'], []),
|
||||
211: (['Butadien', 'p_{0,11}'], ['p_{0,18}']),
|
||||
213: ([], ['Butadien']),
|
||||
}
|
||||
|
||||
HYPERGRAPH2 = {
|
||||
2: (['p_{0,0}'], []),
|
||||
3: (['p_{0,1}'], []),
|
||||
4: (['Butadien', 'Butadien'], ['p_{0,0}', 'p_{0,1}']),
|
||||
6: (['p_{0,2}'], []),
|
||||
7: (['Butadien', 'Butadien'], ['p_{0,2}']),
|
||||
17: (['p_{0,4}'], []),
|
||||
23: (['Butadien', 'p_{0,1}'], ['p_{0,0}', 'p_{0,4}']),
|
||||
41: (['p_{0,11}'], []),
|
||||
42: (['p_{0,4}'], ['p_{0,0}', 'p_{0,11}']),
|
||||
213: ([], ['Butadien']),
|
||||
}
|
||||
|
||||
HYPERGRAPH3 = {
|
||||
0: (['Butadien'], []),
|
||||
213: ([], ['Butadien']),
|
||||
4: (['Butadien', 'Butadien'], ['p_{0,0}', 'Butadien']),
|
||||
2: (['p_{0,0}'], []),
|
||||
}
|
||||
|
||||
|
||||
FIXED_FLOWS = {
|
||||
213: 3,
|
||||
#2: 3,
|
||||
4: 1,
|
||||
#23: 1,
|
||||
#41: 1,
|
||||
#42: 1,
|
||||
}
|
||||
|
||||
K = 10
|
||||
|
||||
def build_model(name, hyperedges, elmax, el1, el2, el3, excluded_supports=None):
|
||||
model = Model(name)
|
||||
|
||||
x = {e_id: model.addVar(vtype=GRB.INTEGER, lb = 0, name = f"x_{e_id}") for e_id in hyperedges}
|
||||
b = {e_id: model.addVar(vtype=GRB.BINARY, name = f"b_{e_id}") for e_id in hyperedges}
|
||||
n1 = {e_id: model.addVar(vtype=GRB.BINARY, name = f"n1_{e_id}") for e_id in hyperedges}
|
||||
n2 = {e_id: model.addVar(vtype=GRB.BINARY, name = f"n2_{e_id}") for e_id in hyperedges}
|
||||
n3 = {e_id: model.addVar(vtype=GRB.BINARY, name = f"n3_{e_id}") for e_id in hyperedges}
|
||||
|
||||
|
||||
vertices = set(v for tails, heads in hyperedges.values() for v in tails + heads)
|
||||
|
||||
#Every item created has to be consumed:
|
||||
'''
|
||||
for v in vertices:
|
||||
count[v] = 0
|
||||
count = model.addVars(vertices, vtype=GRB.CONTINUOUS, lb = 0.0, name = "count")
|
||||
for mol in vertices:
|
||||
for edge, (tails, heads) in hyperedges.items():
|
||||
count[mol] += x[edge] * (tails.count(mol) - heads.count(mol))
|
||||
model.addConstr(count[mol] == 0)
|
||||
'''
|
||||
|
||||
for v in vertices:
|
||||
inflow = quicksum(x[e_id] * heads.count(v) for e_id, (_, heads) in hyperedges.items())
|
||||
outflow = quicksum(x[e_id] * tails.count(v) for e_id, (tails, _) in hyperedges.items())
|
||||
model.addConstr(inflow == outflow, name = f"flow_conservation_{v}")
|
||||
|
||||
for e_id, value in FIXED_FLOWS.items():
|
||||
model.addConstr(x[e_id] == value, name = f"fixed_flow_{e_id}")
|
||||
|
||||
for e_id in hyperedges:
|
||||
model.addGenConstrIndicator(b[e_id], 0, x[e_id] == 0, name = f"unused_implies_zero_{e_id}")
|
||||
model.addConstr(x[e_id] >= b[e_id], name = f"used_implies_positive_flow_{e_id}")
|
||||
#Only if an edge has flow, can it contribute to the likelihood
|
||||
model.addConstr(b[e_id] >= n1[e_id], name = f"only_used_contribute_to_nmr1_{e_id}")
|
||||
model.addConstr(b[e_id] >= n2[e_id], name = f"only_used_contribute_to_nmr2_{e_id}")
|
||||
model.addConstr(b[e_id] >= n3[e_id], name = f"only_used_contribute_to_nmr3_{e_id}")
|
||||
#One reaction can only contribute once
|
||||
model.addConstr(n1[e_id] + n2[e_id] + n3[e_id] <= 1)
|
||||
|
||||
#Only one edge per nmr can contribute
|
||||
model.addConstr(quicksum(n1[e_id] for e_id in hyperedges) == 1)
|
||||
model.addConstr(quicksum(n2[e_id] for e_id in hyperedges) == 1)
|
||||
model.addConstr(quicksum(n3[e_id] for e_id in hyperedges) == 1)
|
||||
|
||||
reaction_path = {}
|
||||
|
||||
|
||||
if excluded_supports:
|
||||
for i, support in enumerate(excluded_supports):
|
||||
model.addConstr(
|
||||
quicksum(b[e_id] for e_id in support) <= len(support) - 1,
|
||||
name=f"different_hyperedges_{i}",
|
||||
)
|
||||
|
||||
model.setObjective(quicksum(1000 * (el1[e_id] * n1[e_id] + el2[e_id] * n2[e_id] + el3[e_id] * n3[e_id]) - (1 / elmax[e_id]) * x[e_id] for e_id in hyperedges if elmax[e_id] != 0),GRB.MAXIMIZE)
|
||||
|
||||
|
||||
#model.setObjective(quicksum(1000 * elmax[e_id] * b[e_id] - (1 - elmax[e_id]) * x[e_id] for e_id in hyperedges),GRB.MAXIMIZE)
|
||||
#model.setObjective(quicksum(1000 * elmax[e_id] * b[e_id] - (1 / elmax[e_id]) * x[e_id] for e_id in hyperedges if elmax[e_id] != 0),GRB.MAXIMIZE)
|
||||
#model.setObjective(quicksum(1000 * elmax[e_id] * b[e_id] + np.log(elmax[e_id]) * x[e_id] for e_id in hyperedges if elmax[e_id] != 0),GRB.MAXIMIZE)
|
||||
#Excluding creation and destruction only three reactions for three nmr
|
||||
#model.addConstr(quicksum(b[e_id] for e_id, (tails, heads) in hyperedges.items() if tails != [] and heads != []) == 3)
|
||||
|
||||
#2 Butadien create first different molecule and it has to be created first:
|
||||
startmolecule = ["Butadien"]
|
||||
|
||||
#Including it or not changes first and second solution (sometimes flipped)
|
||||
#model.addConstr(quicksum(b[e_id] for e_id, (tails, _) in hyperedges.items() if list(set(tails)) == startmolecule) == 1)
|
||||
#model.addConstr(b[4] + b[7] == 1)
|
||||
|
||||
model.addConstr(quicksum(b[e_id] for e_id, (tails, heads) in hyperedges.items() if tails == [] and heads == startmolecule)== 1)
|
||||
#model.addConstr(b[213] == 1)
|
||||
|
||||
#model.addConstr(b[41] == 1)
|
||||
|
||||
|
||||
|
||||
#No cyclic reaction pairs:
|
||||
for e_id1, (tails1, heads1) in hyperedges.items():
|
||||
for e_id2, (tails2, heads2) in hyperedges.items():
|
||||
if (heads1, tails1) == (tails2, heads2):
|
||||
#print(e_id1, e_id2)
|
||||
model.addConstr((b[e_id1] + b[e_id2]) <= 1)
|
||||
|
||||
return model, x, b
|
||||
|
||||
|
||||
def positive_entries(variable_dict, threshold = 0.5):
|
||||
return {e_id: var.X for e_id, var in variable_dict.items() if var.X > threshold}
|
||||
|
||||
def edgelikelihoods(variable_dict, edgelikelihood, threshold = 0.5):
|
||||
return {e_id: edgelikelihood[e_id] for e_id, var in variable_dict.items() if var.X > threshold}
|
||||
|
||||
def flow_to_json(flow_solution, smiles):
|
||||
inputs = {}
|
||||
outputs = {}
|
||||
reactions = []
|
||||
|
||||
for e_id, value in flow_solution.items():
|
||||
value = int(round(value))
|
||||
tails, heads = HYPERGRAPH[e_id]
|
||||
|
||||
if tails == []:
|
||||
for molecule in heads:
|
||||
inputs[smiles[molecule]] = inputs.get(smiles[molecule], 0) + value
|
||||
elif heads == []:
|
||||
for molecule in tails:
|
||||
outputs[smiles[molecule]] = outputs.get(smiles[molecule], 0) + value
|
||||
else:
|
||||
reactions.append({
|
||||
"flow": value,
|
||||
"educts": [smiles[molecule] for molecule in tails],
|
||||
"products": [smiles[molecule] for molecule in heads],
|
||||
})
|
||||
|
||||
return {
|
||||
"inputs": inputs,
|
||||
"outputs": outputs,
|
||||
"reactions": reactions,
|
||||
}
|
||||
|
||||
def write_flows_json(flows):
|
||||
path = Path(__file__).with_name("gurobi_flows.json")
|
||||
path.write_text(json.dumps(flows, indent=2) + "\n")
|
||||
print(f"\nWrote {path}")
|
||||
|
||||
def print_solution(title, flow_solution, binary_solution, edge_likelihoods, hyperedges):
|
||||
print(f"\n{title}:")
|
||||
for e_id in sorted(flow_solution):
|
||||
flow = flow_solution[e_id]
|
||||
tails, heads = hyperedges[e_id]
|
||||
likelihood = edge_likelihoods[e_id]
|
||||
print(f"Hyperedge {e_id}: Flow = {flow}, Tails = {tails}, Heads = {heads}, With likelihood = {likelihood}")
|
||||
print("\nBinary Variables:")
|
||||
for e_id in sorted(binary_solution):
|
||||
print(f"Binary Variable b_{e_id} = {binary_solution[e_id]}")
|
||||
|
||||
print(f"\nTotal flow: {sum(flow_solution.values())}")
|
||||
print(f"Number of used hyperedges: {len(binary_solution)}")
|
||||
|
||||
def main():
|
||||
VERTICES = ['Butadien', 'p_{0,0}', 'p_{0,1}', 'p_{0,2}', 'p_{0,3}', 'p_{0,4}', 'p_{0,5}', 'p_{0,6}', 'p_{0,7}', 'p_{0,8}', 'p_{0,9}', 'p_{0,10}', 'p_{0,11}', 'p_{0,12}', 'p_{0,13}', 'p_{0,14}', 'p_{0,15}', 'p_{0,16}', 'p_{0,17}', 'p_{0,18}']
|
||||
#Vergleich mit dem NMR von Ethylen und Hexatrien
|
||||
NMR1 = [0.32, 0.5, 0.87, 0.0, 0.11, 0.58, 0.06, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.09, 0.0, 0.0, 0.17, 0.0, 0.09]
|
||||
#Vergleich mit dem NMR von Ethylen und Octrien
|
||||
NMR2 = [0.33, 0.43, 0.6, 0.0, 0.13, 0.9, 0.07, 0.01, 0.0, 0.0, 0.01, 0.0, 0.0, 0.01, 0.1, 0.0, 0.0, 0.22, 0.0, 0.13]
|
||||
#Vergleich mit dem NMR von Ethylen und Benzol
|
||||
NMR3 = [0.0, 0.32, 0.0, 0.0, 0.0, 0.0, 0.05, 0.0, 0.0, 0.02, 0.0, 0.29, 0.95, 0.17, 0.0, 0.0, 0.0, 0.0, 0.0, 0.05]
|
||||
VERTICESSMILES = ['C=CC=C', 'C=C', 'C=CC=CC=C', 'C1CCC(C=C)CC=1', 'C(CCC(C=C)CC=C)=C', 'C=CC=CC=CC=C', 'C1CCC(C=CC=C)CC=1', 'C=CC1C=CCCC1', 'C1CCCCC=1', 'C=CC1CC=CCC1C=C', 'C1CCC(C=C)C(C=C)C=1', 'C(C1CC(C=C)C=CC1)=C', 'C1C=CC=CC=1', 'C(CCC1C=CC=CC1)=C', 'C1C(C=CC=C)CCCC=1', 'C=CC(C=C)CCCC=C', 'C=CCCCCC=C', 'C=CC1C=CC(C=C)CC1', 'C1CC2CCCCC2CC=1', 'C1CC2C=CC=CC2CC=1']
|
||||
SMILES = dict(zip(VERTICES, VERTICESSMILES))
|
||||
|
||||
#Chosable parameters
|
||||
modes = ["Product", "Average"]
|
||||
mode = modes[1]
|
||||
normalize = True
|
||||
|
||||
if normalize:
|
||||
NMR1 = [round(l/sum(NMR1), 2) for l in NMR1]
|
||||
NMR2 = [round(l/sum(NMR2), 2) for l in NMR2]
|
||||
NMR3 = [round(l/sum(NMR3), 2) for l in NMR3]
|
||||
#print(NMR1)
|
||||
#print(NMR2)
|
||||
#print(NMR3)
|
||||
|
||||
|
||||
#Kombiniert Molekül it Wahrscheinlichkeit für NMR1:
|
||||
VERTICE1 = {}
|
||||
for vertice, likelihood in zip(VERTICES, NMR1):
|
||||
VERTICE1[vertice] = likelihood
|
||||
#Kombiniert Molekül it Wahrscheinlichkeit für NMR2:
|
||||
VERTICE2 = {}
|
||||
for vertice, likelihood in zip(VERTICES, NMR2):
|
||||
VERTICE2[vertice] = likelihood
|
||||
#Kombiniert Molekül it Wahrscheinlichkeit für NMR3:
|
||||
VERTICE3 = {}
|
||||
for vertice, likelihood in zip(VERTICES, NMR3):
|
||||
VERTICE3[vertice] = likelihood
|
||||
|
||||
#Kantenwahrscheinlichkeiten für NMR1:
|
||||
EDGE1 = {}
|
||||
#Kantenwahrscheinlichkeiten für NMR2:
|
||||
EDGE2 = {}
|
||||
#Kantenwahrscheinlichkeiten für NMR3:
|
||||
EDGE3 = {}
|
||||
#Maximum der verschiedenen Kantenwahrscheinlichkeiten:
|
||||
EDGEMAX = {}
|
||||
|
||||
#Ist es besser, die Edukte mit zu berücksichtigen? Dann hin rück gleich wahrscheinlich, aber nur 1 kann gewählt werden
|
||||
#Ordnet die Kantenwahrscheinlichkeit basierend auf dem Mittel oder Produkt der Wahrscheinlichkeiten der Produkte
|
||||
for edge, (tails, heads) in HYPERGRAPH.items():
|
||||
if heads == [] or tails == []:
|
||||
EDGE1[edge] = 0.0
|
||||
EDGE2[edge] = 0.0
|
||||
EDGE3[edge] = 0.0
|
||||
elif mode == "Product":
|
||||
#print([VERTICE1[head] for head in heads])
|
||||
EDGE1[edge] = round(np.prod([VERTICE1[head] for head in heads]),2)
|
||||
EDGE2[edge] = round(np.prod([VERTICE2[head] for head in heads]),2)
|
||||
EDGE3[edge] = round(np.prod([VERTICE3[head] for head in heads]),2)
|
||||
elif mode == "Average":
|
||||
EDGE1[edge] = round(sum(VERTICE1[head] for head in heads)/len(heads),2)
|
||||
EDGE2[edge] = round(sum(VERTICE2[head] for head in heads)/len(heads),2)
|
||||
EDGE3[edge] = round(sum(VERTICE3[head] for head in heads)/len(heads),2)
|
||||
|
||||
EDGEMAX[edge] = max([EDGE1[edge], EDGE2[edge], EDGE3[edge]])
|
||||
#print(EDGEMAX[edge], HYPERGRAPH[edge])
|
||||
|
||||
'''
|
||||
#Vertices from example:
|
||||
verticelist = [0,1,2,3,4,5,11,12]
|
||||
print([VERTICES[a] for a in verticelist])
|
||||
print([NMR1[a] for a in verticelist])
|
||||
print([NMR2[a] for a in verticelist])
|
||||
print([NMR3[a] for a in verticelist])
|
||||
#Edges from example
|
||||
print(EDGEMAX[4], HYPERGRAPH[4])
|
||||
print(EDGEMAX[7], HYPERGRAPH[7])
|
||||
print(EDGEMAX[13], HYPERGRAPH[13])
|
||||
print(EDGEMAX[23], HYPERGRAPH[23])
|
||||
print(EDGEMAX[37], HYPERGRAPH[37])
|
||||
print(EDGEMAX[42], HYPERGRAPH[42])
|
||||
print(EDGEMAX[67], HYPERGRAPH[67])
|
||||
'''
|
||||
|
||||
excluded_supports = []
|
||||
flows = []
|
||||
|
||||
for rank in range(1, K + 1):
|
||||
model, x, b = build_model(
|
||||
f"HypergraphFlow_{rank}",
|
||||
HYPERGRAPH,
|
||||
EDGEMAX,
|
||||
EDGE1,
|
||||
EDGE2,
|
||||
EDGE3,
|
||||
excluded_supports=excluded_supports,
|
||||
)
|
||||
model.optimize()
|
||||
|
||||
if model.status != GRB.Status.OPTIMAL:
|
||||
break
|
||||
|
||||
solution = positive_entries(x)
|
||||
binary_solution = positive_entries(b)
|
||||
likelihoods = edgelikelihoods(b, EDGEMAX)
|
||||
|
||||
print_solution(
|
||||
f"Solution {rank}",
|
||||
solution,
|
||||
binary_solution,
|
||||
likelihoods,
|
||||
HYPERGRAPH,
|
||||
)
|
||||
|
||||
flows.append(flow_to_json(solution, SMILES))
|
||||
excluded_supports.append(list(binary_solution.keys()))
|
||||
|
||||
write_flows_json(flows)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,114 @@
|
||||
import json
|
||||
from collections import Counter
|
||||
|
||||
include("butadienNetwork.py")
|
||||
|
||||
with open("gurobi_flows.json") as f:
|
||||
gurobi_flows = json.load(f)
|
||||
|
||||
|
||||
def canonical_smiles(smiles):
|
||||
return str(Graph.fromSMILES(smiles).smiles)
|
||||
|
||||
|
||||
def vertex_key(v):
|
||||
return str(v.graph.smiles)
|
||||
|
||||
|
||||
vertices_by_smiles = {}
|
||||
for v in dg.vertices:
|
||||
vertices_by_smiles.setdefault(vertex_key(v), []).append(v)
|
||||
|
||||
|
||||
def current_vertex(smiles):
|
||||
matches = vertices_by_smiles.get(canonical_smiles(smiles), [])
|
||||
if len(matches) != 1:
|
||||
raise RuntimeError(
|
||||
f"{smiles}: expected one MØD vertex, found {len(matches)}"
|
||||
)
|
||||
return matches[0]
|
||||
|
||||
|
||||
def current_reaction(educts, products):
|
||||
wanted_educts = Counter(canonical_smiles(x) for x in educts)
|
||||
wanted_products = Counter(canonical_smiles(x) for x in products)
|
||||
|
||||
matches = []
|
||||
for e in dg.edges:
|
||||
current_educts = Counter(vertex_key(v) for v in e.sources)
|
||||
current_products = Counter(vertex_key(v) for v in e.targets)
|
||||
if current_educts == wanted_educts and current_products == wanted_products:
|
||||
matches.append(e)
|
||||
|
||||
if len(matches) != 1:
|
||||
raise RuntimeError(
|
||||
f"{educts} -> {products}: expected one MØD edge, found {len(matches)}"
|
||||
)
|
||||
return matches[0]
|
||||
|
||||
|
||||
def check_causality(gurobi_flow, rank):
|
||||
source_flow = {
|
||||
current_vertex(smiles): value
|
||||
for smiles, value in gurobi_flow["inputs"].items()
|
||||
}
|
||||
output_flow = {
|
||||
current_vertex(smiles): value
|
||||
for smiles, value in gurobi_flow["outputs"].items()
|
||||
}
|
||||
|
||||
reaction_flow = {}
|
||||
for reaction in gurobi_flow["reactions"]:
|
||||
edge = current_reaction(reaction["educts"], reaction["products"])
|
||||
reaction_flow[edge] = reaction_flow.get(edge, 0) + reaction["flow"]
|
||||
|
||||
flow = hyperflow.Model(dg)
|
||||
|
||||
for v in source_flow:
|
||||
flow.addSource(v.graph)
|
||||
for v in output_flow:
|
||||
flow.addSink(v.graph)
|
||||
|
||||
for e in dg.edges:
|
||||
flow.addConstraint(edgeFlow[e] == reaction_flow.get(e, 0))
|
||||
|
||||
for v in dg.vertices:
|
||||
flow.addConstraint(inFlow[v] == source_flow.get(v, 0))
|
||||
flow.addConstraint(outFlow[v] == output_flow.get(v, 0))
|
||||
|
||||
flow.findSolutions(maxNumSolutions=1)
|
||||
|
||||
try:
|
||||
solution = next(iter(flow.solutions))
|
||||
except StopIteration:
|
||||
print(f"solution {rank}: not a valid MØD hyperflow")
|
||||
return "INVALID"
|
||||
|
||||
query = causality.RealisabilityQuery(flow.dg)
|
||||
dag = query.findDAG(solution)
|
||||
|
||||
if not dag:
|
||||
print(f"solution {rank}: causally realisable: NO")
|
||||
return "NO"
|
||||
|
||||
print(f"solution {rank}: causally realisable: YES")
|
||||
|
||||
printer = DGPrinter()
|
||||
printer.graphvizPrefix = 'layout = "dot";'
|
||||
|
||||
def vertex_visible(v):
|
||||
return solution.eval(vertexFlow[v]) != 0
|
||||
|
||||
printer.pushVertexVisible(vertex_visible)
|
||||
dg.print(printer=printer, data=dag.getPrintData(False))
|
||||
|
||||
return "YES"
|
||||
|
||||
|
||||
results = []
|
||||
for rank, gurobi_flow in enumerate(gurobi_flows, start=1):
|
||||
results.append(check_causality(gurobi_flow, rank))
|
||||
|
||||
print("\n=== causality summary ===")
|
||||
for rank, result in enumerate(results, start=1):
|
||||
print(f"solution {rank}: {result}")
|
||||
@@ -0,0 +1,136 @@
|
||||
import mod
|
||||
import networkx as nx
|
||||
import time
|
||||
|
||||
# from equilibrator_assets.generate_compound import create_compounds
|
||||
|
||||
# each vertex has attribute "label" which stores string label
|
||||
# each edge has attribute "bond" which stores bond order (-, =, etc.)
|
||||
|
||||
class GraphObj:
|
||||
def __init__(self, graph, verbose=False):
|
||||
self._verbose = verbose
|
||||
|
||||
mod_lg_class = mod.libpymod.Rule.LeftGraph
|
||||
mod_rg_class = mod.libpymod.Rule.RightGraph
|
||||
mod_g_class = mod.libpymod.Graph
|
||||
if isinstance(graph, mod_lg_class) or isinstance(graph, mod_rg_class) or isinstance(graph, mod_g_class):
|
||||
self._nx_graph = self.mod_to_nx_graph(graph)
|
||||
self._gml_string = self.nx_graph_to_GML_string(self._nx_graph)
|
||||
elif isinstance(graph, nx.Graph):
|
||||
self._nx_graph = graph
|
||||
self._gml_string = self.nx_graph_to_GML_string(self._nx_graph)
|
||||
elif isinstance(graph, str):
|
||||
self._gml_string = graph
|
||||
self._nx_graph = self.GML_to_nx_graph(self._gml_string)
|
||||
else:
|
||||
print(f"ERROR: Graph class cannot identify graph type in constructor: {type(graph)}")
|
||||
|
||||
# up to the caller to check the number of components created
|
||||
ccs = [self._nx_graph.subgraph(c).copy() for c in nx.connected_components(self._nx_graph)]
|
||||
ccs = sorted(ccs, key=len, reverse=True)
|
||||
self._num_components = len(ccs)
|
||||
if self._num_components > 1:
|
||||
self._components = [Graph(c) for c in ccs]
|
||||
else:
|
||||
self._components = [self]
|
||||
|
||||
if self._num_components == 1:
|
||||
self._mod_graph = self.nx_graph_to_mod(self._nx_graph)
|
||||
|
||||
# self._equ_compounds = self.equ_compounds()
|
||||
|
||||
def equ_compounds(self):
|
||||
smiles = [c.mod_graph.smiles for c in self._components]
|
||||
equ_comps = create_compounds(smiles, mol_format="smiles", bypass_chemaxon=True, save_empty_compounds=True)
|
||||
return equ_comps
|
||||
|
||||
def mod_to_nx_graph(self, modGraph: mod.Graph):
|
||||
g = nx.Graph()
|
||||
|
||||
for v in modGraph.vertices:
|
||||
g.add_node(int(v.id), label=str(v.stringLabel), modID=int(v.id))
|
||||
|
||||
for e in modGraph.edges:
|
||||
g.add_edge(int(e.source.id), int(e.target.id), bond=str(e.bondType))
|
||||
|
||||
return g
|
||||
|
||||
def nx_graph_to_mod(self, nxGraph):
|
||||
try:
|
||||
return mod.Graph.fromGMLString(self.nx_graph_to_GML_string(nxGraph))
|
||||
except mod.libpymod.InputError: # graph is not connected probably
|
||||
if self._verbose:
|
||||
print("Error converting nxGraph to mod graph. Likely graph is not connected. This will not affect rule generation.")
|
||||
return None
|
||||
|
||||
def GML_to_nx_graph(self, gml_string):
|
||||
g = nx.Graph()
|
||||
lines = gml_string.split("\n")
|
||||
for line in lines:
|
||||
tokens = line.split()
|
||||
if len(tokens) < 2:
|
||||
continue
|
||||
if tokens[0] == "node":
|
||||
(_,_,_, mid, _, l, _) = tokens
|
||||
g.add_node(int(mid), label=l[1:-1], modID=int(mid))
|
||||
elif tokens[0] == "edge":
|
||||
(_, _, _, u, _, v, _, bondOrder, _ ) = tokens
|
||||
g.add_edge(int(u), int(v), bond=bondOrder[1:-1])
|
||||
return g
|
||||
|
||||
def nx_graph_to_GML_string(self, nxGraph):
|
||||
out = []
|
||||
out.append("graph [")
|
||||
|
||||
out.extend([f"\t\tnode [ id {nxGraph.nodes[node]['modID']} label \"{nxGraph.nodes[node]['label']}\" ]" for node in nxGraph.nodes])
|
||||
out.extend([f"\t\tedge [ source {u} target {v} label \"{nxGraph[u][v]['bond']}\" ]"
|
||||
for (u,v) in nxGraph.edges])
|
||||
|
||||
out.append("]")
|
||||
|
||||
return "\n".join(out)
|
||||
|
||||
@property
|
||||
def nx_graph(self):
|
||||
return self._nx_graph
|
||||
|
||||
@property
|
||||
def mod_graph(self):
|
||||
if self.num_components == 1:
|
||||
return self._mod_graph
|
||||
else:
|
||||
return mod.Graph.fromGMLString(self.gml_string)
|
||||
|
||||
@property
|
||||
def gml_string(self):
|
||||
return self._gml_string
|
||||
|
||||
@property
|
||||
def gml(self):
|
||||
return self._gml_string
|
||||
|
||||
@property
|
||||
def edges(self):
|
||||
return self._nx_graph.edges
|
||||
|
||||
@property
|
||||
def nodes(self):
|
||||
return self._nx_graph.nodes
|
||||
|
||||
@property
|
||||
def num_components(self):
|
||||
return self._num_components
|
||||
|
||||
@property
|
||||
def connected_components(self):
|
||||
return self._components
|
||||
|
||||
def mod_print(self):
|
||||
self._mod_graph.print()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self._gml_string
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self._gml_string)
|
||||
Reference in New Issue
Block a user