-
Notifications
You must be signed in to change notification settings - Fork 2
/
ModelProcessingRevised.py
executable file
·2464 lines (2292 loc) · 108 KB
/
ModelProcessingRevised.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# This file contains functions for processing metabolic models with Mongoose
# Created by: Leonid Chindelevitch
# Last modified: January 30, 2017
import os, copy, itertools, random, subprocess, shelve
from functools import reduce
from decimal import Decimal
from math import gcd
from fractions import Fraction
from Utilities import *
import multiprocessing
import qsoptex
zero, one = Fraction(0), Fraction(1)
used = [0] * 11
#Docker ESOLVER_PATH:
ESOLVER_PATH = "/qsopt-ex/build/esolver/.libs/esolver"
#ESOLVER_PATH = "/Users/christopherle/Documents/Leonid/qsopt-ex/build/esolver/.libs/esolver"
#ESOLVER_PATH = "/Users/Admin/Downloads/DownloadedSoftware/qsopt-ex/build/esolver/.libs/esolver"
def reduceMatrix(N, Irr, Filename = 'Reduction.txt'):
# This function computes the reduced form of a given stoichiometric matrix
# assuming that the specified list of reactions is irreversible.
# The reduction proceeds in several steps, each of which is verified using
# exact (rational arithmetic-based) linear programming whenever possible.
# The sequence of steps is:
# 1) Find and delete topology-blocked reactions.
# 2) Find and delete stoichiometry-blocked reactions.
# 3) Find and delete irreversibility-blocked reactions.
# 4) Find and process the semi-blocked reactions.
# 5) Find and group reaction subsets.
# 6) Find and delete redundant constraints.
# rows[i], cols[i] contains the iteration at which the i-th row, column was deleted
# print("<> Here!")
m, n = getSize(N)
rows, cols = [0]*m, [0]*n
# currows[i], curcols[i] is the index of the current i-th row, column in original matrix
currows, curcols = list(range(m)), list(range(n))
# currirrev is True for the current irreversible reactions, False otherwise
curirrev = [bool(x in Irr) for x in range(n)]
Iter = 1
print((str(Iter) + ') Finding and deleting topology-blocked reactions.'))
(deadRows, deadCols, current) = processDeadEnds(N, False)
updateRecord(rows, currows, deadRows, Iter)
updateRecord(cols, curcols, deadCols, Iter)
currows = filterOut(currows, deadRows)
curcols = filterOut(curcols, deadCols)
curirrev = filterOut(curirrev, deadCols)
Iter = 2
print((str(Iter) + ') Finding and deleting irreversibility-blocked reactions.'))
(TBlocked, current) = processTBlocked(current, curirrev)
thermoBlocked = mapList(TBlocked, curcols)
updateRecord(cols, curcols, TBlocked, Iter)
curcols = filterOut(curcols, TBlocked)
curirrev = filterOut(curirrev, TBlocked)
Iter = 3
print((str(Iter) + ') Finding and deleting stoichiometry-blocked reactions'))
(SBlocked, current, curB) = processSBlocked(current)
stoichioBlocked = mapList(SBlocked, curcols)
updateRecord(cols, curcols, SBlocked, Iter)
curcols = filterOut(curcols, SBlocked)
curirrev = filterOut(curirrev, SBlocked)
Iter = 4
print((str(Iter) + ') Finding and processing the semi-blocked reactions'))
(onlyPos, onlyNeg, current) = processUnidirectional(current, curirrev, option = 'null')
onlyPositive = mapList(onlyPos, curcols)
onlyNegative = mapList(onlyNeg, curcols)
updateRecord(cols, curcols, onlyPos + onlyNeg, Iter)
for i in onlyPos + onlyNeg:
curirrev[i] = True
# adjust the nullspace by flipping the reactions that can carry only negative fluxes
for i in onlyNeg:
curB[i] = [-curB[i][k] for k in range(len(curB[i]))]
Iter = 5
print((str(Iter) + ') Finding and grouping reaction subsets.'))
(Enzymes, lumpedReacts, subsetReacts, current) = processSubsets(current, curB)
Enzymes = [list(zip(mapList(subset[0], curcols), subset[1])) for subset in Enzymes]
updateRecord(cols, curcols, subsetReacts, Iter)
curcols = filterOut(curcols, lumpedReacts)
curirrev = filterOut(curirrev, lumpedReacts)
Iter = 6
print((str(Iter) + ') Finding and deleting redundant constraints'))
(redRows, current) = processRedundant(current)
updateRecord(rows, currows, redRows, Iter)
currows = filterOut(currows, redRows)
reductionRecord = (rows, cols, onlyPositive, onlyNegative, Enzymes)
describeReduction(reductionRecord, Filename, EBA = False)
Irrev = findTrueIndices(curirrev)
extraFilename = Filename[:-4] + 'Full' + Filename[-4:]
writeResults(current, Irrev, reductionRecord, extraFilename)
return (current, Irrev) + reductionRecord
def energyBalanceReduce(Matrix, Irrev, External, Filename = "EnergyReduction.txt", signs = False):
m, n = getSize(Matrix)
curirrev = [bool(x in Irrev) for x in range(n)]
# create a list of external reactions
Internal = [x for x in range(n) if x not in External]
# remove the external reactions from Matrix
current = [filterOut(Matrix[x], External) for x in range(len(Matrix))]
# rows[i], cols[i] contains the iteration at which the i-th row, column was deleted
m, n = getSize(current)
rows, cols = [0]*m, [0]*n
# currirrev is True for the current irreversible reactions, False otherwise
# remove the external reactions from curirrev
curirrev = filterOut(curirrev, External)
# currows[i], curcols[i] is the index of the current i-th row, column in original matrix
currows, curcols = list(range(m)), list(range(n))
# Step 1) find and remove any zero loops that are present in the network
Iter = 1
(loopInds, current) = processZeroLoops(current)
loops = mapList(loopInds, curcols)
Loops = mapList(loops, Internal)
updateRecord(cols, curcols, loopInds, Iter)
curcols = filterOut(curcols, loopInds)
curirrev = filterOut(curirrev, loopInds)
# Step 2) find and remove any energy-blocked irreversible reactions
Iter = 2
(EBlocked, current) = processEBlocked(current, curirrev)
energyBlocked = mapList(EBlocked, curcols)
EnergyBlocked = mapList(energyBlocked, Internal)
updateRecord(cols, curcols, EBlocked, Iter)
curcols = filterOut(curcols, EBlocked)
curirrev = filterOut(curirrev, EBlocked)
# Step 3) find and process the unidirectional reactions
Iter = 3
(onlyPos, onlyNeg, current) = processUnidirectional(current, curirrev, option = 'row')
onlyPositive = mapList(onlyPos, curcols)
OnlyPositive = mapList(onlyPositive, Internal)
onlyNegative = mapList(onlyNeg, curcols)
OnlyNegative = mapList(onlyNegative, Internal)
updateRecord(cols, curcols, onlyPos + onlyNeg, Iter)
for i in onlyPos + onlyNeg:
curirrev[i] = True
# Step 4) find and group together all isozyme subsets
Iter = 4
(Isozymes, deletedReacts, current, curirrev) = processIsozymes(current, curirrev, True)
Isozymes = [mapList(subset, curcols) for subset in Isozymes]
updateRecord(cols, curcols, deletedReacts, Iter)
curcols = filterOut(curcols, deletedReacts)
curirrev = filterOut(curirrev, deletedReacts)
# Step 5) find all possible sign combinations
allSigns = []
if signs:
curIrrev = findTrueIndices (curirrev)
curRev = findFalseIndices(curirrev)
r = len(curRev)
if (r > 0 and r <= 20): # NOTE: Do NOT use if r > 20, as it might take too long!
allSigns = checkAllSigns(current, curRev)
else:
print('No or too many internal reversible reactions!')
# Step 6) put back the original exchange reactions
Iter = 5
origMat = transpose(Matrix)
for y in OnlyPositive: # multiply OnlyPositive by -1 (see the paper for the explanation)
origMat[y] = [-origMat[y][x] for x in range(m)]
allInds = sorted([Internal[x] for x in curcols] + External)
fullMat = [origMat[x] for x in allInds]
current = transpose(fullMat)
curirrev = [bool(x in Irrev + OnlyPositive + OnlyNegative) for x in allInds]
cols = [cols[Internal.index(x)] if x in Internal else 0 for x in range(len(origMat))]
# Step 7) remove any redundant constraints
Iter = 6
(redRows, current) = processRedundant(current)
updateRecord(rows, currows, redRows, Iter)
currows = filterOut(currows, redRows)
reductionRecord = (rows, cols, OnlyPositive, OnlyNegative, Isozymes)
describeReduction(reductionRecord, Filename, EBA = True)
Irrev = findTrueIndices(curirrev)
newExternal = [i for i,x in enumerate(allInds) if x in External]
extraFilename = Filename[:-4] + 'Full' + Filename[-4:]
writeResults(current, Irrev, tuple([newExternal] + [allSigns]) + reductionRecord, extraFilename)
return (current, Irrev, newExternal, allSigns) + reductionRecord
def fullIterativeReduce(Matrix, Irrev, External, Filename = "IterativeReduction.txt"):
# Iteratively reduces a system by applying flux-balance and energy-balance constraints
Iter = 1
(mF,nF) = getSize(Matrix)
while(True):
print(('Performing iteration ' + str(Iter)))
curFilename = Filename[:-4] + 'Energy' + str(Iter) + Filename[-4:]
result = energyBalanceReduce(Matrix, Irrev, External, curFilename, False)
Matrix, Irrev, External, Record = result[0], result[1], result[2], result[4:]
(mE,nE) = getSize(Matrix)
print(("The current size of the system is " + str(mE) + " by " + str(nE)))
if mE == mF and nE == nF and not (Record[2] + Record[3]):
break
curFilename = Filename[:-4] + 'Flux' + str(Iter) + Filename[-4:]
result = reduceMatrix(Matrix, Irrev, curFilename)
Matrix, Irrev, Record = result[0], result[1], result[2:]
External = findExternal(External, Record)
(mF,nF) = getSize(Matrix)
print(("The current size of the system is " + str(mF) + " by " + str(nF)))
if mF == mE and nF == nE and not (Record[2] + Record[3]):
break
Iter += 1
if len(Matrix) > 0:
allRev = [x for x in range(len(Matrix[0])) if x not in Irrev + External]
if len(allRev) < 20:
print("Extracting the signs")
Iter += 1
curFilename = Filename[:-4] + 'Energy' + str(Iter) + Filename[-4:]
result = energyBalanceReduce(Matrix, Irrev, External, curFilename, True)
Matrix, Irrev, External = result[0], result[1], result[2]
return (Matrix, Irrev, External)
def findExternal(initExternal, reductionRecord):
External = []
(rows, cols, onlyPositive, onlyNegative, Enzymes) = reductionRecord
anchor_to_subset = {}
for i, subset in enumerate(Enzymes):
anchor = subset[0][0]
anchor_to_subset[anchor] = subset
anchors = list(anchor_to_subset.keys())
index = 0
for i, status in enumerate(cols):
if status in [0,4]:
if i in initExternal:
External.append(index)
index += 1
if status == 5 and i in anchors:
subset = anchor_to_subset[i]
subsetReacts = [x[0] for x in subset]
if any([j in initExternal for j in subsetReacts]):
External.append(index)
index += 1
return External
def writeResults(Matrix, Irrev, Record, Filename, sep = '\t'):
f = open(Filename, 'w')
for row in Matrix:
f.write(sep.join([str(x) for x in row]) + '\n')
f.write('\n')
f.write(sep.join([str(x) for x in Irrev]) + '\n')
f.write('\n')
for record in Record:
f.write(sep.join([str(x) for x in record]) + '\n')
f.write('\n')
f.close()
return
def describeReduction(reductionRecord, Filename, EBA = False):
# This function creates a description of the reduction (based on a given record) in the specified filename
# (rows, cols, onlyPositive, onlyNegative, Enzymes, loops, Isozymes) = reductionRecord
(rows, cols, onlyPositive, onlyNegative, Enzymes) = reductionRecord
f = open(Filename,'w')
if EBA:
f.write('Deleted ' + str(cols.count(1)) + ' zero loops\n')
f.write('Removed ' + str(cols.count(2)) + ' energy blocked reactions!\n')
else:
f.write('Deleted ' + str(rows.count(1)) + ' topologically blocked rows!\n')
f.write('Deleted ' + str(cols.count(1)) + ' topologically blocked columns!\n')
f.write('Removed ' + str(cols.count(2)) + ' thermodynamically blocked reactions!\n')
f.write('Removed ' + str(cols.count(3)) + ' stoichiometrically blocked reactions!\n')
f.write('Identified ' + str(len(onlyPositive)) + ' reactions that can only go forward!\n')
f.write('Identified and flipped ' + str(len(onlyNegative)) + ' reactions that can only go backward!\n')
f.write('Grouped ' + str(sum([len(x) for x in Enzymes])) + ' reactions into ' + str(len(Enzymes)) + ' subsets!\n')
f.write('Removed ' + str(rows.count(6)) + ' redundant constraints!\n')
f.close()
return
def matrixToGraph(Matrix):
# This function creates a graph structure for the bipartite graph represented by Matrix.
# The graph structure is a dictionary of incidence lists; 'R' for rows, 'C' for columns.
D = {}
for ind0, row in enumerate(Matrix):
for ind1, elt in enumerate(row):
if elt:
str0 = 'R' + str(ind0)
str1 = 'C' + str(ind1)
myAdd(D, str0, str1)
myAdd(D, str1, str0)
return D
def reduceByDegree(D, dmin, specialStart, Causes = None):
# This function removes all nodes of a graph structure with degree not exceeding dmin.
# An additional argument specialStart specifies the prefix of the nodes to be examined.
# The optional Causes dictionary contains the causes of deletion for each deleted node.
for x in list(D.keys()):
if x.startswith(specialStart):
curList = D[x]
if len(curList) <= dmin:
for y in curList:
for z in D[y]:
D[z].remove(y)
if Causes is not None and len(D[z]) <= dmin and z not in Causes:
Causes[z] = y
if Causes is not None and y not in Causes:
Causes[y] = x
del D[y]
del D[x]
return
def traceBack(item, record):
# This function traces back the item through a dictionary of records
trace = []
while item in record:
if item in trace:
break
else:
trace.append(item)
item = record[item]
trace = list(reversed(trace))
return trace
def fullReduce(D, dmin, specialStart, details):
# This function iteratively prunes a graph structure until all degrees exceed dmin.
# An additional argument specialStart specifies the prefix of the nodes to be examined.
# If details = True, additionally returns the causes of deletion for each of the nodes.
numNodes = len(D)
prevNodes = numNodes + 1
if details:
Causes = {}
else:
Causes = None
while (numNodes < prevNodes):
reduceByDegree(D, dmin, specialStart, Causes)
prevNodes = numNodes
numNodes = len(D)
return (D, Causes)
def processDeadEnds(Matrix, details):
# This function finds and returns the dead ends (rows and columns) of a given
# stoichiometric matrix, as well as a reduced version of the stoichiometric
# matrix; if the details flag is True, also returns the cause of each "death"
m, n = getSize(Matrix)
D = matrixToGraph(Matrix)
(reducedD, Causes) = fullReduce(D, 1, 'R', details)
goodRows = sorted([int(x[1:]) for x in reducedD if x.startswith('R')])
goodCols = sorted([int(x[1:]) for x in reducedD if x.startswith('C')])
badRows = [x for x in range(m) if x not in goodRows]
badCols = [x for x in range(n) if x not in goodCols]
reducedMatrix = [[Matrix[x][y] for y in goodCols] for x in goodRows]
if details:
return (badRows, badCols, reducedMatrix, Causes)
else:
return (badRows, badCols, reducedMatrix)
def findIsozymes(N, Full = False):
# This function determines all the sets of isozymes in a given stoichiometric matrix.
# Isozymes are defined as reactions that are identical (not reverses of each other).
# If Full = True, groups together all reactions that differ by a constant multiple.
ISubsets = {}
if Full:
ID = findSubsets(transpose(N), True)
for x in ID:
ISubsets[x] = [y[0] for y in ID[x]]
else:
ID = groupIdentical(transpose(N))
for x in ID:
if len(x) > 1:
ISubsets[x[0]] = x[1:]
return ISubsets
def processIsozymes(N, irrev, Full = False):
# This function processes all the sets of isozymes in a given stoichiometric matrix.
# It returns an isozyme list and a set of duplicated columns, as well as the proper
# reversibility information for each isozyme subset and a new stoichiometric matrix.
ISubsets = findIsozymes(N, Full = Full)
badCols = sum(list(ISubsets.values()), [])
Isozymes = [[]]*len(ISubsets)
for (ind, key) in enumerate(ISubsets.keys()):
Isozymes[ind] = [key] + ISubsets[key]
irrev[key] = all([irrev[x] for x in Isozymes[ind]])
newIrrev = filterOut(irrev, badCols)
(m, n) = getSize(N)
newN = [filterOut(N[x], badCols) for x in range(m)]
return (Isozymes, badCols, newN, newIrrev)
def processZeroLoops(N):
# This function finds the indices of zero loops and removes them from a given matrix.
(m, n) = getSize(N)
loopInds = [y for y in range(n) if not any([N[x][y] for x in range(m)])]
if len(loopInds) == n:
loopInds = list(range(1,n))
newN = [filterOut(N[x], loopInds) for x in range(m)]
return (loopInds, newN)
def findRedundant(N):
# This function returns a list of non-redundant rows in a given matrix.
# These non-redundant rows form a basis for the rowspace of the matrix.
Nt = transpose(N)
(A, active) = GaussJordan(Nt, Gauss = True)
return active
def processRedundant(N):
# This function returns a list of redundant rows and a matrix without them.
m, n = getSize(N)
zeroRows = [x for x in range(m) if not any(N[x])]
goodRows = filterOut(list(range(m)), zeroRows)
if goodRows:
newN = filterOut(N, zeroRows)
active = findRedundant(newN)
inactive = filterOut(list(range(len(newN))), active)
inactive = mapList(inactive, goodRows)
redRows = zeroRows + inactive
newN = [newN[x] for x in active]
else:
redRows = list(range(1,m))
if n > 0:
newN = [N[0]]
else:
newN = N
return (redRows, newN)
def findTBlocked(N, Irrev, basename = 'TBlocked.lp', restricted = True, option = 'row', negated = False, rev = False):
# This function finds all the thermodynamically blocked reactions in a metabolic network
# given by its stoichiometric matrix and a list of irreversible reactions. See paper for
# a detailed justification of the algorithm. Note: returned reactions are irreversible!
# For the meaning of the "restricted" parameter, see the function findPosSupport below.
Iter = 0
index = basename.find('.lp')
weight = [-1 if negated else 1]*len(Irrev)
found = set()
Min = -1 if rev else 0
while len(found) < len(Irrev):
curName = basename[:index] + str(Iter) + basename[index:]
# print("FINE")
(val, vec) = findPosSupport(N, Irrev, weight, curName, Min = Min, restricted = restricted, option = option)
# print("STILL FINE")
if val > 0:
Iter = Iter + 1
if rev:
newlyFound = set([int(y[1:]) for y,v in vec.items() if y.startswith('Y') and (-1 if negated else 1) * v > 0])
else:
newlyFound = set([int(y[1:]) for y in list(vec.keys()) if y.startswith('Y')])
found.update(newlyFound.intersection(Irrev))
# Recall that the solution contains only the variables whose values are > 0 (< 0 if negated and rev are True)
for y in found:
x = Irrev.index(y) # translate into the original indices!
weight[x] = 0
else:
break
found = sorted(list(set(found)))
return found
def processTBlocked(N, irrev):
# This function returns a list of thermodynamically blocked reactions and a matrix without them.
Irrev = findTrueIndices(irrev)
TBlocked = findTBlocked(N, Irrev)
newN = [filterOut(N[x], TBlocked) for x in range(len(N))]
return (TBlocked, newN)
def processEBlocked(N, irrev):
# This function returns a list of energy blocked reactions and a matrix without them.
Irrev = findTrueIndices(irrev)
EUnblocked = findTBlocked(N, Irrev, basename = 'EBlocked.lp', restricted = False)
EBlocked = [x for x in Irrev if x not in EUnblocked]
newN = [filterOut(N[x], EBlocked) for x in range(len(N))]
return (EBlocked, newN)
def prepareForCplex(Matrix):
Matrix, Mults = makeIntegral(transpose(Matrix), mults = True, decim = True) # integralize column-wise
Matrix = transpose(Matrix)
return [[Decimal(x.numerator)/Decimal(x.denominator) for x in y] for y in Matrix], Mults
def findPosSupport(N, support, weight = [1], Filename = 'trial.lp', Min = 0, restricted = True, option = 'row'):
used[0] = 1
# This function finds the vector optimizing a given weight in the row/nullspace of N whose
# support is restricted to a given set of entries; those entries must be non-negative!
# Note: if the weight vector has a single component, it is automatically taken to be 1!
# If a nonzero Min value is specified, the components in the support are at least Min.
# If restricted = False, same but support is NOT restricted to the given set of entries.
# print("NEW fps!")
m, n = getSize(N)
p = qsoptex.ExactProblem()
p.set_objective_sense(qsoptex.ObjectiveSense.MAXIMIZE)
variables = set([])
for i in range(n): variables.add('Y' + str(i))
if Min:
if Min > 0:
curLower, curUpper = Min, None
else:
curLower, curUpper = Min, -Min
else:
curLower, curUpper = 0, 1
if len(weight) == len(support):
for ind, item in enumerate(support):
p.add_variable(name='Y' + str(item), objective=weight[ind], lower=curLower, upper=curUpper)
elif len(weight) == 1:
for ind, item in enumerate(support):
p.add_variable(name='Y' + str(item), objective=1, lower=curLower, upper=curUpper)
else:
print('Error: the weight vector is not of the right length!')
for ind in range(n):
if ind not in support:
p.add_variable(name='Y' + str(ind), objective=0, lower=0, upper=(0 if restricted else None))
if option == 'row':
for i in range(m):
if [_f for _f in N[i] if _f]:
p.add_variable(name = 'X' + str(i), objective = 0, lower = None, upper = None)
variables.add('X' + str(i))
for j in range(n):
curDict = {}
for i in range(m):
if N[i][j] != 0:
curDict.update({'X'+str(i): N[i][j]})
curDict.update({'Y'+str(j): -1})
p.add_linear_constraint(qsoptex.ConstraintSense.EQUAL, curDict, rhs = 0)
else: # option == 'null'
for i in range(m):
p.add_linear_constraint(qsoptex.ConstraintSense.EQUAL, {'Y' + str(j): N[i][j] for j in range(n)}, rhs=0)
return processProblem(p, variables, True)
def findSBlocked(N, NB = False):
# This function finds all the stoichiometrically blocked reactions in a metabolic network
# given by its stoichiometric matrix. NB is True if the nullspace basis is to be returned.
SBlocked = []
(A,R) = GaussJordan(N)
B = NullspaceBasis((A,R), True)
SBlocked = [j for j in range(len(B)) if not any(B[j])]
if NB:
return (SBlocked, B)
else:
return SBlocked
def processSBlocked(N):
# This function returns a list of stoichiometrically blocked reactions and a matrix without them.
(SBlocked, B) = findSBlocked(N, True)
newN = [filterOut(N[x], SBlocked) for x in range(len(N))]
newB = filterOut(B, SBlocked)
return (SBlocked, newN, newB)
def checkAllSigns(N, Rev):
# This function produces a list of all possible sign combinations for the reversible reactions
# in the row combinations of N, assuming that the irreversible reactions must be non-negative.
r = len(Rev)
allSigns = []
counter = 0
maxCounter = 2**r
print(('There are ' + str(maxCounter) + ' combinations to process'))
for signs in itertools.product('+-', repeat = r):
counter += 1
if (counter % 100 == 0):
print(('Processed ' + str(counter) + ' combinations so far'))
result = checkSigns(N, Rev, signs, Filename = 'signs' + str(counter) + '.lp')
if result:
allSigns.append(flipSigns(signs)) # need to flip them because entropy increases!
return allSigns
def flipSigns(signs):
# This function flips - to + and + to - in a given tuple (first converted to string)
signs = ''.join(signs)
signsF = signs.replace('-','p').replace('+','-').replace('p','+')
return signsF
def checkSigns(N, Rev, signs, Filename = 'signs.lp', Cplex = False):
used[1] = 1
# Thus function checks whether a particular sign pattern on the reversible reactions gives
# a feasible vector in the rowspace of the input matrix; signs is a string of '+' and '-'.
m, n = getSize(N)
negIndices = [i for i, x in enumerate(signs) if x == '-']
negatives = mapList(negIndices, Rev)
posIndices = [i for i, x in enumerate(signs) if x == '+']
positives = mapList(posIndices, Rev)
p = qsoptex.ExactProblem()
p.set_objective_sense(qsoptex.ObjectiveSense.MINIMIZE)
variables = set([])
for j in range(n):
variables.add('Y' + str(j))
for j in range(n):
curDict = {}
for i in range(m):
if N[i][j]:
curDict.update({'X'+str(i): N[i][j]})
curDict.update({'Y'+str(j): -1})
p.add_linear_constraint(qsoptex.ConstraintSense.EQUAL, curDict, rhs=0)
for i in range(m):
if [_f for _f in N[i] if _f]:
p.add_variable('X' + str(i), objective = 0, lower=None, upper=None)
variables.add('X' + str(i))
for j in positives:
p.add_variable('Y' + j , obejective=0, lower=1, upper=None)
for j in negatives:
p.add_variable('Y' + j, obejective=0, lower=None, upper=-1)
val = processProblem(p, variables, False)
if (type(val) == type([]) and len(val) == 0): # infeasible
return False
else:
return True
def vectorInSpan(N, vec, Filename = 'trial.lp', Cplex = False):
used[2] = 1
# This function determines whether a given vector vec is in the row span of a matrix.
# This is achieved by solving a linear program using QSOpt_ex and checking its value.
m, n = getSize(N)
n1 = len(vec)
if n != n1:
print('Problem: the vector is not compatible with the matrix!')
return False
p = qsoptex.ExactProblem()
p.set_objective_sense(qsoptex.ObjectiveSense.MAXIMIZE)
p.add_variable('Y', objective=1, lower=0, upper=None)
variables = set(['Y'])
for j in range(n):
curDict = {}
for i in range(m):
if N[i][j]:
curDict.update({'X'+str(i): N[i][j]})
if bool(vec[j]):
curDict.update({'Y': vec[j]})
p.add_linear_constraint(qsoptex.ConstraintSense.EQUAL, curDict, rhs=0)
for i in range(m):
if [_f for _f in N[i] if _f]:
variables.add('X' + str(i))
p.add_variable('X'+str(i), objective=0, lower=-1, upper=1)
val = processProblem(p, variables, False)
if val:
return True
else:
return False
def computeDistance(N, vec, norm = 'inf', Irrev = [], Filename = 'Distance.lp', Cplex = False):
used[3] = 1
# This function computes the distance from a given vector vec to the row span of a matrix.
# The possible options for norm are 'one', 'two' and 'inf'; 'two' is currently unavailable.
# The optional list Irrev specifies the set of row coefficients required to be nonnegative.
m, n = getSize(N)
n1 = len(vec)
if n != n1:
print('Problem: the vector is not compatible with the matrix!')
return
p = qsoptex.ExactProblem()
p.set_objective_sense(qsoptex.ObjectiveSense.MINIMIZE)
variables = set(['Y'])
p.add_variable('Y', objective=1, lower=0, upper=None)
for i in range(m):
if [_f for _f in N[i] if _f]:
variables.add('X' + str(i))
if i not in Irrev:
p.add_variable('X'+str(i),objective=0, lower=None, upper=None)
else:
p.add_variable('X' + str(i), objective=0, lower=0, upper=None)
for j in range(n):
variables.add('V' + str(j))
p.add_variable('V'+str(j), objective=0, lower=None, upper=None)
variables.add('T' + str(j))
p.add_variable('T' + str(j), objective=0, lower=None, upper=None)
variables.add('Y' + str(j))
p.add_variable('Y' + str(j), objective=0, lower=0, upper=None)
for j in range(n):
curDict = {}
for i in range(m):
if N[i][j]:
curDict.update({'X' + str(i): N[i][j]})
curDict.update({'V'+str(j): -1})
p.add_linear_constraint(qsoptex.ConstraintSense.EQUAL, curDict, rhs=0)
for j in range(n):
curDict={'V'+str(j): 1, 'T'+str(j): -1}
p.add_linear_constraint(qsoptex.ConstraintSense.EQUAL, curDict, rhs=vec[j])
for j in range(n):
curDict={'Y'+str(j): 1, 'T'+str(j): 1}
p.add_linear_constraint(qsoptex.ConstraintSense.GREATER, curDict, rhs=0)
for j in range(n):
curDict={'Y'+str(j): 1, 'T'+str(j): -1}
p.add_linear_constraint(qsoptex.ConstraintSense.GREATER, curDict, rhs=0)
if norm == 'one':
for j in range(n):
curDict.update({'Y'+str(j): 1})
curDict.update({'Y': -1})
p.add_linear_constraint(qsoptex.ConstraintSense.EQUAL, curDict, rhs=0)
elif norm == 'inf':
for j in range(n):
curDict.update({'Y'+str(j): 1})
curDict.update({'Y': -1})
p.add_linear_constraint(qsoptex.ConstraintSense.LESS, curDict, rhs=0)
else:
print(('Error: the option ' + norm + ' is not a valid option for the norm!'))
return
return processProblem(p, variables, True)
def findDistance(N, special, Irrev, norm = 'inf'):
# This function determines the smallest change in biomass coefficients (reaction indexed
# by special) needed in a stoichiometric matrix N to enable a unit of biomass production
# while respecting irreversibility constraints specified by the vector of indices Irrev.
Nt = transpose(N)
negBiomass = [-x for x in Nt[special]]
Nt = filterOut(Nt, [special])
# recompute the indices of the irreversible reactions for Nt
redIrrev = [x for x in Irrev if x < special] + [x - 1 for x in Irrev if x > special]
val, vec = computeDistance(Nt, negBiomass, norm = norm, Irrev = redIrrev)
redVec = {}
for x in list(vec.keys()):
if x.startswith('V'):
redVec[x] = vec[x]
return val, redVec
####parallelization
def parallelizedNegFindFeasible(N, Irrev, pos ,index, option,subList,out_q_neg):
count = len(subList) * index
outdict = {}
for ind, react in enumerate(subList): # changed from subOnlyNeg to subList!
(val1, vec1) = findFeasible(N, react, Irrev, pos, ('sub+' + str(index) + 'V' + str(ind) + 'sets.lp'), option=option)
if (type(val1) == type([]) and len(val1) == 0): # infeasible
outdict[count+ind] = react #puts infeasible reactions into dictionary
out_q_neg.put(outdict) #places dictionary into queue asynchronously
def parallelizedPosFindFeasible(N, Irrev, pos ,index, option,subList,out_q_pos):
outdict = {}
count = len(subList) * index
for ind, react in enumerate(subList):# changed from subOnlyPos to subList!
(val0, vec0) = findFeasible(N, react, Irrev, pos, ('sub-' + str(index) + 'V' + str(ind) + 'sets.lp'), option=option)
if (type(val0) == type([]) and len(vec0) == 0): # infeasible
outdict[count+ind] = react #puts infeasible reactions into dictionary
out_q_pos.put(outdict) #places dictionary into queue asynchronously
##end functions for parallelization
def findUnidirectional(N, Irrev, option = 'null', verbose = False, parallel = 0):
# This function finds all unidirectional (effectively irreversible) reactions.
# NOTE: It assumes that all the reactions in the network can have nonzero flux;
# otherwise may incorrectly classify blocked reactions as only negative.
# The option can be 'null' for nullspace (default) or 'row' for rowspace.
# If parallel > 0, this specifies the number of cores available for processing.
m, n = getSize(N)
onlyPos, onlyNeg = [], []
allRev = [i for i in range(n) if not i in Irrev]
canBePositive = findTBlocked(N, allRev, basename = 'canBePositive.lp', restricted = False, option = option, rev = True)
canBeNegative = findTBlocked(N, allRev, basename = 'canBeNegative.lp', restricted = False, option = option, rev = True, negated = True)
onlyPosCandidates = [i for i in allRev if i not in canBeNegative]
onlyNegCandidates = [i for i in allRev if i not in canBePositive]
parallel = 0 #testing
if len(onlyNegCandidates) <= 1:
onlyNeg = onlyNegCandidates
else:
if parallel>0: # this is to be distributed between the threads
splitNegPairs = [onlyNegCandidates[i::parallel] for i in range(parallel)]
# Each process will get a queue to put its outdict into
out_q_neg = multiprocessing.Queue()
procs = []
for index, subList in enumerate(splitNegPairs):
p = multiprocessing.Process(
target=parallelizedNegFindFeasible,
args=(N, Irrev, True, index, option,subList,out_q_neg))
procs.append(p)
p.daemon = True
p.start()
# Collect all results into a single result dict. We know how many dicts
# with results to expect.
resultdict = {}
for i in range(parallel):
resultdict.update(out_q_neg.get())
# Wait for all worker processes to finish
for p in procs:
p.join()
onlyNeg = list(resultdict.values())
else:
for ind, react in enumerate(onlyNegCandidates):
(val1, vec1) = findFeasible(N, react, Irrev, True, 'sub+' + str(ind) + 'sets.lp', option = option)
if (type(val1) == type([]) and len(val1) == 0): # infeasible
onlyNeg.append(react)
onlyPosCandidates = [x for x in onlyPosCandidates if x not in onlyNeg]
if len(onlyPosCandidates) <= 1:
onlyPos = onlyPosCandidates
else:
if parallel>0: # this is to be distributed between the threads
splitPosPairs = [onlyPosCandidates[i::parallel] for i in range(parallel)]
# Each process will get queue to put its out dict into
out_q_pos = multiprocessing.Queue()
procs = []
for index, subList in enumerate(splitPosPairs):
p = multiprocessing.Process(
target=parallelizedPosFindFeasible,
args=(N, Irrev, False, index,option, subList,out_q_pos))
procs.append(p)
p.daemon = True
p.start()
# Collect all results into a single result dict. We know how many dicts
# with results to expect.
resultdict = {}
for i in range(parallel):
resultdict.update(out_q_pos.get())
# Wait for all worker processes to finish
for p in procs:
p.join()
onlyPos = list(resultdict.values())
else:
for ind, react in enumerate(onlyPosCandidates):
(val0, vec0) = findFeasible(N, react, Irrev, False, 'sub-' + str(ind) + 'sets.lp', option = option)
if (type(val0) == type([]) and len(val0) == 0): # infeasible
onlyPos.append(react)
if verbose:
print('This required ' + str(len(onlyNegCandidates) + len(onlyPosCandidates)) + ' linear programs')
return (onlyPos, onlyNeg)
def processUnidirectional(N, irrev, option = 'null'):
# This function returns a list of unidirectional reactions and a matrix without them.
# The option can be 'null' for nullspace (default) or 'row' for rowspace.
m, n = getSize(N)
Irrev = findTrueIndices(irrev)
(onlyPos, onlyNeg) = findUnidirectional(N, Irrev, option = option)
newN = [[N[x][y] for y in range(n)] for x in range(m)]
for x in range(m):
for y in onlyNeg:
newN[x][y] = -newN[x][y]
return (onlyPos, onlyNeg, newN)
def findSubsets(N, NB = False):
# This function returns all enzyme subsets as well as the multipliers for each reaction.
# If NB is True, the input is assumed to be the nullspace basis; if not, it is computed.
ESubsets = {}
if NB:
Matrix = N
else:
Matrix = NullspaceBasis(N)
n = len(Matrix)
redMatrix = [[] for i in range(n)]
Mults = [0 for i in range(n)]
for i in range(n):
nzRow = [_f for _f in Matrix[i] if _f]
if not nzRow:
print('Error: zero row found!')
return ESubsets
else:
# save the multiplier, make first non-zero entry 1
Mults[i] = convertToFraction(nzRow[0])
redMatrix[i] = [x / nzRow[0] for x in Matrix[i]]
groups = groupIdentical(redMatrix)
for group in groups:
if len(group) > 1:
mults = [Mults[x] for x in group]
ESubsets[group[0]] = [[group[x], mults[x] / mults[0]] for x in range(1, len(group))]
return ESubsets
def processSubsets(N, curB):
# This function returns a list of reactions in enzyme subsets and a matrix without them.
# It requires a basis for the nullspace of the matrix N as well as the matrix N itself!
m, n = getSize(N)
ESubsets = findSubsets(curB, True)
newN = [[N[x][y] for y in range(n)] for x in range(m)]
lumpedReacts = []
Enzymes = [[]]*len(ESubsets)
anchors = list(ESubsets.keys())
for (ind, key) in enumerate(anchors):
value = ESubsets[key]
(reacts, ratios) = list(zip(*value))
Enzymes[ind] = (tuple([key]) + reacts, tuple([one]) + ratios)
lumpedReacts += list(reacts)
for num, react in enumerate(reacts):
ratio = ratios[num]
for x in range(m):
newN[x][key] += (ratio * newN[x][react])
newN = [filterOut(newN[x], lumpedReacts) for x in range(m)]
subsetReacts = anchors + lumpedReacts
return (Enzymes, lumpedReacts, subsetReacts, newN)
def reconfigureNetwork(N, irrev):
# This function returns a reconfigured version of the given stoichiometric matrix
# along with a list of reversible reaction indices that have been split into two.
m, n = getSize(N)
rev = findFalseIndices(irrev)
newN = [[N[x][y] for y in range(n)] for x in range(m)]
for x in range(m):
newN[x] += [-newN[x][y] for y in rev]
m, n0 = getSize(newN)
newIrrev = [True] * n0
# find columns duplicated by the reconfiguration and remove them!
(Isozymes, badCols, newN, newIrrev) = processIsozymes(newN, newIrrev)
if any([len(subset) != 2 for subset in Isozymes]):
print('Error: all isozyme subsets should have size 2 during reconfiguration!')
badInds = [subset[1] - n for subset in Isozymes]
rev = filterOut(rev, badInds)
return newN, rev
def findFeasible(N, special, Irrev = [], pos = True, Filename = 'trial.lp', disable = [], negative = [], option = 'null', Cplex = False):
used[4]=1
# This function finds a feasible vector in the row/nullspace of N whose set of irreversible
# reactions is given. The entry corresponding to special is 1 if pos is True, -1 otherwise.
# Additional features: it is now possible to specify a subset of reactions to be disabled
# as well as a subset of reactions which are constrained to have only negative flux.
# The option can be 'null' for nullspace (default) or 'row' for rowspace.
m, n = getSize(N)
Rev = [x for x in range(n) if x not in Irrev]
p = qsoptex.ExactProblem()
p.set_objective_sense(qsoptex.ObjectiveSense.MAXIMIZE)
variables = set([])
# note: we are only looking for a feasible vector, hence no objective function required!
if option == 'row':
for i in range(m):
if [_f for _f in N[i] if _f]:
p.add_variable(name='X'+str(i), objective=0, lower=None, upper=None)
variables.add('X'+str(i))
for i in Rev:
if i not in negative and [_f for _f in [N[k][i] for k in range(m)] if _f]:
p.add_variable(name='V' + str(i), objective=1, lower=None, upper=None)
variables.add('V' + str(i))
for i in negative:
p.add_variable(name='V' + str(i), objective=0, lower=None, upper=0)
variables.add('V' + str(i))
for i in range(n):
if i not in Rev and i not in negative:
p.add_variable(name='V' + str(i), objective=0, lower=0, upper=None)
variables.add('V' + str(i))
if option == 'row':
for j in range(n):
curDict = {'V'+str(j):-1}
for i in range(m):
if N[i][j]:
curDict.update({'X'+str(i): N[i][j]})
p.add_linear_constraint(qsoptex.ConstraintSense.EQUAL, curDict, rhs=0)
else: # assumes option = 'null'
for i in range(m):
curDict = {}
# print('HERE + ' + str(i))
for j in range(n):
if N[i][j]:
curDict.update({'V'+str(j): N[i][j]})
p.add_linear_constraint(qsoptex.ConstraintSense.EQUAL, curDict, rhs=0)
if pos:
p.add_linear_constraint(qsoptex.ConstraintSense.EQUAL, {'V' + str(special): 1}, rhs=1)
else:
p.add_linear_constraint(qsoptex.ConstraintSense.EQUAL, {'V' + str(special): 1}, rhs=-1)
if disable:
for i in disable:
p.add_linear_constraint(qsoptex.ConstraintSense.EQUAL, {'V' + str(i): 1}, rhs=0)
return processProblem(p, variables, True)
def findRatio(N, react1, react2, Irrev, Max = True, ratio = 0, Filename = 'trial.lp', Cplex = False):
used[5] = 1
# This function finds the minimum or the maximum ratio of two given entries in the nullspace
# of N whose set of irreversible reactions is given. Maximize if Max is True, minimize if not.
# If a ratio is specified, checks whether the difference react1 - ratio * react2 can equal 1.
m, n = getSize(N)
Rev = [x for x in range(n) if x not in Irrev]
p = qsoptex.ExactProblem()
variables = set([])
if Max:
p.set_objective_sense(qsoptex.ObjectiveSense.MAXIMIZE)
else:
p.set_objective_sense(qsoptex.ObjectiveSense.MINIMIZE)
if ratio != 0:
num, den = ratio.numerator, ratio.denominator
p.add_variable('V'+ str(react1), objective = den, lower=None, upper=None)
p.add_variable('V' + str(react2), objective = -num, lower=None, upper=None)
else:
p.add_variable('V' + str(react1), objective=0, lower=None, upper=None)
p.add_variable('V' + str(react2), objective=1, lower=None, upper=None)
variables.add('V' + str(react1))
variables.add('V' + str(react2))
if ratio != 0:
p.add_linear_constraint(qsoptex.ConstraintSense.EQUAL, {'V'+str(react1):den, 'V'+str(react2):-num}, rhs=1)
else:
p.add_linear_constraint(qsoptex.ConstraintSense.EQUAL, {'V' + str(react1): 1}, rhs=1)
# const += ''.join([' + '.join([str(N[i][j]) + ' V' + str(j) for j in range(n) if N[i][j]]) + ' = 0\n' for i in range(m)])
for i in range(m):
curDicrt = {}
for j in range(n):
if N[i][j]:
curDict = {'V'+str(j): N[i][j]}
p.add_linear_constraint(qsoptex.ConstraintSense.EQUAL, curDict, rhs=0)
# bounds += ''.join(['V' + str(i) + ' free\n' for i in Rev if [_f for _f in [N[k][i] for k in range(m)] if _f]])
for i in range(n):
if i != react1 or i != react2:
p.add_variable('V'+str(i), objective=0, lower=None, upper=None)
variables.add('V'+str(i))
return processProblem(p, variables)
def processFile(Filename, opt = False, destroyIn = True, destroyOut = True, suppressOutput = True):
# This function processes the linear programming problem described in the specified file.