8
8
from straindesign .names import *
9
9
from typing import Dict , Tuple
10
10
from pandas import DataFrame
11
- from numpy import floor , sign , mod , nan , isnan , unique , inf , isinf , zeros , full , linspace , prod
11
+ from numpy import floor , sign , mod , nan , isnan , unique , inf , isinf , full , linspace , \
12
+ prod , array , mean , flip , ceil , floor
13
+ from numpy .linalg import matrix_rank
12
14
from os import cpu_count
13
15
from contextlib import redirect_stdout , redirect_stderr
14
16
from io import StringIO
15
17
import matplotlib .pyplot as plt
16
- from scipy .spatial import ConvexHull
17
- import mpl_toolkits . mplot3d as a3
18
+ from scipy .spatial import ConvexHull , Delaunay
19
+ from matplotlib . cm import ScalarMappable , get_cmap
18
20
19
21
from straindesign .parse_constr import linexpr2mat , linexprdict2str
20
22
@@ -496,13 +498,13 @@ def yopt(model,**kwargs):
496
498
else :
497
499
status = INFEASIBLE
498
500
499
- def phase_plane (model , axes , ** kwargs ):
501
+ def plot_flux_space (model , axes , ** kwargs ):
500
502
reaction_ids = model .reactions .list_attr ("id" )
501
503
502
504
if CONSTRAINTS in kwargs :
503
505
kwargs [CONSTRAINTS ] = parse_constraints (kwargs [CONSTRAINTS ],reaction_ids )
504
506
else :
505
- kwargs [CONSTRAINTS ] = None
507
+ kwargs [CONSTRAINTS ] = []
506
508
507
509
if SOLVER not in kwargs :
508
510
kwargs [SOLVER ] = None
@@ -513,7 +515,7 @@ def phase_plane(model, axes, **kwargs):
513
515
else :
514
516
show = True
515
517
516
- axes = list (axes ) # cast to list of lists
518
+ axes = [ list (ax ) if not isinstance ( ax , str ) else [ ax ] for ax in axes ] # cast to list of lists
517
519
num_axes = len (axes )
518
520
if num_axes not in [2 ,3 ]:
519
521
raise Exception ('Please define 2 or 3 axes as a list of tuples [ax1, ax2, (optional) ax3] with ax1 = (den,num).\n ' + \
@@ -529,28 +531,54 @@ def phase_plane(model, axes, **kwargs):
529
531
530
532
ax_name = ["" for _ in range (num_axes )]
531
533
ax_limits = [(nan ,nan ) for _ in range (num_axes )]
534
+ ax_type = ["" for _ in range (num_axes )]
532
535
for i ,ax in enumerate (axes ):
533
- ax = parse_linexpr (ax ,reaction_ids )[0 ]
534
- ax_name [i ] = linexprdict2str (ax )
535
- sol_min = fba (model ,obj = ax ,constraints = kwargs [CONSTRAINTS ],solver = solver ,obj_sense = 'minimize' )
536
- sol_max = fba (model ,obj = ax ,constraints = kwargs [CONSTRAINTS ],solver = solver ,obj_sense = 'maximize' )
537
- # abort if any of the fluxes are unbounded or undefined
538
- unbnd = [i + 1 for i ,v in enumerate ([sol_min ,sol_max ]) if v .status == UNBOUNDED ]
539
- if any (unbnd ):
540
- raise Exception ('One of the specified reactions is unbounded or undefined. Phase plane cannot be generated.' )
541
- ax_limits [i ] = [min ((0 ,sol_min .objective_value )),max ((0 ,sol_max .objective_value ))]
542
- axes [i ] = ax
536
+ if len (ax ) == 1 :
537
+ ax_type [i ] = 'rate'
538
+ elif len (ax ) == 2 :
539
+ ax_type [i ] = 'yield'
540
+ if ax_type [i ] == 'rate' :
541
+ ax [0 ] = parse_linexpr (ax [0 ],reaction_ids )[0 ]
542
+ ax_name [i ] = linexprdict2str (ax [0 ])
543
+ sol_min = fba (model ,obj = ax [0 ],constraints = kwargs [CONSTRAINTS ],solver = solver ,obj_sense = 'minimize' )
544
+ sol_max = fba (model ,obj = ax [0 ],constraints = kwargs [CONSTRAINTS ],solver = solver ,obj_sense = 'maximize' )
545
+ # abort if any of the fluxes are unbounded or undefined
546
+ inval = [i + 1 for i ,v in enumerate ([sol_min ,sol_max ]) if v .status == UNBOUNDED or v .status == INFEASIBLE ]
547
+ if any (inval ):
548
+ raise Exception ('One of the specified reactions is unbounded or problem is infeasible. Plot cannot be generated.' )
549
+ ax_limits [i ] = [min ((0 ,sol_min .objective_value )),max ((0 ,sol_max .objective_value ))]
550
+ elif ax_type [i ] == 'yield' :
551
+ ax [0 ] = parse_linexpr (ax [0 ],reaction_ids )[0 ]
552
+ ax [1 ] = parse_linexpr (ax [1 ],reaction_ids )[0 ]
553
+ ax_name [i ] = '(' + linexprdict2str (ax [0 ])+ ') / (' + linexprdict2str (ax [1 ])+ ')'
554
+ sol_min = yopt (model ,obj_num = ax [0 ],obj_den = ax [1 ],constraints = kwargs [CONSTRAINTS ],solver = solver ,obj_sense = 'minimize' )
555
+ sol_max = yopt (model ,obj_num = ax [0 ],obj_den = ax [1 ],constraints = kwargs [CONSTRAINTS ],solver = solver ,obj_sense = 'maximize' )
556
+ # abort if any of the yields are unbounded or undefined
557
+ inval = [i + 1 for i ,v in enumerate ([sol_min ,sol_max ]) if v .status == UNBOUNDED or v .status == INFEASIBLE ]
558
+ if any (inval ):
559
+ raise Exception ('One of the specified yields is unbounded or undefined or problem is infeasible. Plot cannot be generated.' )
560
+ ax_limits [i ] = [min ((0 ,ceil_dec (sol_min .objective_value ,9 ))),max ((0 ,floor_dec (sol_max .objective_value ,9 )))]
543
561
544
562
# compute points
545
563
x_space = linspace (ax_limits [0 ][0 ], ax_limits [0 ][1 ], num = points )
546
564
lb = full (points , nan )
547
565
ub = full (points , nan )
548
566
for i ,x in enumerate (x_space ):
549
- constr = [axes [0 ],'=' ,x ]
550
- sol_vmin = fba (model ,constraints = constr ,obj = axes [1 ],obj_sense = 'minimize' )
551
- lb [i ] = sol_vmin .objective_value
552
- sol_vmax = fba (model ,constraints = constr ,obj = axes [1 ],obj_sense = 'maximize' )
553
- ub [i ] = sol_vmax .objective_value
567
+ constr = kwargs [CONSTRAINTS ].copy ()
568
+ if ax_type [0 ] == 'rate' :
569
+ constr += [[axes [0 ][0 ],'=' ,x ]]
570
+ elif ax_type [0 ] == 'yield' :
571
+ constr += [[{** axes [0 ][0 ], ** {k :- v * x for k ,v in axes [0 ][1 ].items ()}},'=' ,0 ]]
572
+ if ax_type [1 ] == 'rate' :
573
+ sol_vmin = fba (model ,constraints = constr ,obj = axes [1 ][0 ],obj_sense = 'minimize' )
574
+ lb [i ] = ceil_dec (sol_vmin .objective_value ,9 )
575
+ sol_vmax = fba (model ,constraints = constr ,obj = axes [1 ][0 ],obj_sense = 'maximize' )
576
+ ub [i ] = floor_dec (sol_vmax .objective_value ,9 )
577
+ elif ax_type [1 ] == 'yield' :
578
+ sol_vmin = yopt (model ,constraints = constr ,obj_num = axes [1 ][0 ],obj_den = axes [1 ][1 ],obj_sense = 'minimize' )
579
+ lb [i ] = ceil_dec (sol_vmin .objective_value ,9 )
580
+ sol_vmax = yopt (model ,constraints = constr ,obj_num = axes [1 ][0 ],obj_den = axes [1 ][1 ],obj_sense = 'maximize' )
581
+ ub [i ] = floor_dec (sol_vmax .objective_value ,9 )
554
582
555
583
if num_axes == 2 :
556
584
x = [v for v in x_space ] + [v for v in reversed (x_space )]
@@ -559,6 +587,7 @@ def phase_plane(model, axes, **kwargs):
559
587
x .extend ([x_space [0 ], x_space [0 ]])
560
588
y .extend ([lb [0 ], ub [0 ]])
561
589
plot1 = plt .fill (x , y )
590
+ # plot1 = plt.plot(x, y)
562
591
plot1 [0 ].axes .set_xlabel (ax_name [0 ])
563
592
plot1 [0 ].axes .set_ylabel (ax_name [1 ])
564
593
plot1 [0 ].axes .set_xlim (ax_limits [0 ][0 ]* 1.05 ,ax_limits [0 ][1 ]* 1.05 )
@@ -570,22 +599,87 @@ def phase_plane(model, axes, **kwargs):
570
599
elif num_axes == 3 :
571
600
max_diff_y = max ([abs (l - u ) for l ,u in zip (lb ,ub )])
572
601
datapoints = []
573
- for x ,l ,u in zip (x_space ,lb ,ub ):
602
+ datapoints_top = []
603
+ datapoints_bottom = []
604
+ for i ,(x ,l ,u ) in enumerate (zip (x_space ,lb ,ub )):
574
605
if l - u != 0 :
575
606
y_space = linspace (l ,u ,int (- (- points // (max_diff_y / abs (l - u )))))
576
607
else :
577
608
y_space = [0.0 ]
578
- for y in y_space :
579
- constr = [[axes [0 ],'=' ,x ], [axes [1 ],'=' ,y ]]
580
- sol_vmin = fba (model ,constraints = constr ,obj = axes [2 ],obj_sense = 'minimize' )
581
- datapoints += [[x ,y ,sol_vmin .objective_value ]]
582
- sol_vmax = fba (model ,constraints = constr ,obj = axes [2 ],obj_sense = 'maximize' )
583
- datapoints += [[x ,y ,sol_vmax .objective_value ]]
584
- hull = ConvexHull (datapoints )
609
+ datapoints_top += [[]]
610
+ datapoints_bottom += [[]]
611
+ for j ,y in enumerate (y_space ):
612
+ constr = kwargs [CONSTRAINTS ].copy ()
613
+ if ax_type [0 ] == 'rate' :
614
+ constr += [[axes [0 ][0 ],'=' ,x ]]
615
+ elif ax_type [0 ] == 'yield' :
616
+ constr += [[{** axes [0 ][0 ], ** {k :- v * x for k ,v in axes [0 ][1 ].items ()}},'=' ,0 ]]
617
+ if ax_type [1 ] == 'rate' :
618
+ constr += [[axes [1 ][0 ],'=' ,y ]]
619
+ elif ax_type [1 ] == 'yield' :
620
+ constr += [[{** axes [1 ][0 ], ** {k :- v * y for k ,v in axes [1 ][1 ].items ()}},'=' ,0 ]]
621
+ if ax_type [2 ] == 'rate' :
622
+ sol_vmin = fba (model ,constraints = constr ,obj = axes [2 ][0 ],obj_sense = 'minimize' )
623
+ sol_vmax = fba (model ,constraints = constr ,obj = axes [2 ][0 ],obj_sense = 'maximize' )
624
+ elif ax_type [2 ] == 'yield' :
625
+ sol_vmin = yopt (model ,constraints = constr ,obj_num = axes [2 ][0 ],obj_den = axes [2 ][1 ],obj_sense = 'minimize' )
626
+ sol_vmax = yopt (model ,constraints = constr ,obj_num = axes [2 ][0 ],obj_den = axes [2 ][1 ],obj_sense = 'maximize' )
627
+ datapoints_top [i ] += [len (datapoints )]
628
+ datapoints += [[x ,y ,floor_dec (sol_vmax .objective_value ,9 )]]
629
+ datapoints_bottom [i ] += [len (datapoints )]
630
+ datapoints += [[x ,y ,ceil_dec (sol_vmin .objective_value ,9 )]]
631
+
632
+ # Construct Denaunay triangles for plotting from all 6 perspectives
633
+ triang = []
634
+ # triangles top
635
+ for i in range (len (datapoints_top )- 1 ):
636
+ temp_points = datapoints_top [i ]+ datapoints_top [i + 1 ]
637
+ pts = [[datapoints [idx_p ][0 ],datapoints [idx_p ][1 ]] for idx_p in temp_points ]
638
+ if matrix_rank (pts ) > 1 :
639
+ triang_temp = Delaunay (pts ).simplices
640
+ triang += [[temp_points [idx ] for idx in p ] for p in triang_temp ]
641
+ # triangles bottom
642
+ for i in range (len (datapoints_bottom )- 1 ):
643
+ temp_points = datapoints_bottom [i ]+ datapoints_bottom [i + 1 ]
644
+ pts = [[datapoints [idx_p ][0 ],datapoints [idx_p ][1 ]] for idx_p in temp_points ]
645
+ if matrix_rank (pts ) > 1 :
646
+ triang_temp = Delaunay (pts ).simplices
647
+ triang += [[temp_points [idx ] for idx in flip (p )] for p in triang_temp ]
648
+ # triangles front
649
+ for i in range (len (x_space )- 1 ):
650
+ temp_points = [datapoints_top [i ][0 ],datapoints_top [i + 1 ][0 ],datapoints_bottom [i ][0 ],datapoints_bottom [i + 1 ][0 ]]
651
+ pts = [[datapoints [idx_p ][0 ],datapoints [idx_p ][2 ]] for idx_p in temp_points ]
652
+ if matrix_rank (pts ) > 1 :
653
+ triang_temp = Delaunay (pts ).simplices
654
+ triang += [[temp_points [idx ] for idx in flip (p )] for p in triang_temp ]
655
+ # triangles back
656
+ for i in range (len (x_space )- 1 ):
657
+ temp_points = [datapoints_top [i ][- 1 ],datapoints_top [i + 1 ][- 1 ],datapoints_bottom [i ][- 1 ],datapoints_bottom [i + 1 ][- 1 ]]
658
+ pts = [[datapoints [idx_p ][0 ],datapoints [idx_p ][2 ]] for idx_p in temp_points ]
659
+ if matrix_rank (pts ) > 1 :
660
+ triang_temp = Delaunay (pts ).simplices
661
+ triang += [[temp_points [idx ] for idx in flip (p )] for p in triang_temp ]
662
+ # triangles left
663
+ for i in range (len (datapoints_top [0 ])- 1 ):
664
+ temp_points = [datapoints_top [0 ][i ],datapoints_top [0 ][i + 1 ],datapoints_bottom [0 ][i ],datapoints_bottom [0 ][i + 1 ]]
665
+ pts = [[datapoints [idx_p ][1 ],datapoints [idx_p ][2 ]] for idx_p in temp_points ]
666
+ if matrix_rank (pts ) > 1 :
667
+ triang_temp = Delaunay (pts ).simplices
668
+ triang += [[temp_points [idx ] for idx in p ] for p in triang_temp ]
669
+ # triangles right
670
+ for i in range (len (datapoints_top [- 1 ])- 1 ):
671
+ temp_points = [datapoints_top [- 1 ][i ],datapoints_top [- 1 ][i + 1 ],datapoints_bottom [- 1 ][i ],datapoints_bottom [- 1 ][i + 1 ]]
672
+ pts = [[datapoints [idx_p ][1 ],datapoints [idx_p ][2 ]] for idx_p in temp_points ]
673
+ if matrix_rank (pts ) > 1 :
674
+ triang_temp = Delaunay (pts ).simplices
675
+ triang += [[temp_points [idx ] for idx in p ] for p in triang_temp ]
676
+
677
+ # hull = ConvexHull(datapoints)
585
678
x = [d [0 ] for d in datapoints ]
586
679
y = [d [1 ] for d in datapoints ]
587
680
z = [d [2 ] for d in datapoints ]
588
- ax = a3 .Axes3D (plt .figure ())
681
+ # ax = a3.Axes3D(plt.figure())
682
+ ax = plt .figure ().add_subplot (projection = '3d' )
589
683
ax .dist = 10
590
684
ax .azim = 30
591
685
ax .elev = 10
@@ -595,112 +689,27 @@ def phase_plane(model, axes, **kwargs):
595
689
ax .set_xlabel (ax_name [0 ])
596
690
ax .set_ylabel (ax_name [1 ])
597
691
ax .set_zlabel (ax_name [2 ])
598
- plot1 = ax .plot_trisurf (x ,y ,z ,triangles = hull .simplices ,linewidth = 0.2 , antialiased = True , alpha = 0.67 )
692
+ # set higher color value for 'larger' values in all dimensions
693
+ # use middle value of triangles instead of means and then norm
694
+ colors = [nan for _ in triang ]
695
+ for i ,t in enumerate (triang ):
696
+ p = [datapoints [i ] for i in t ]
697
+ interior_point = [0.0 , 0.0 , 0.0 ]
698
+ for d in range (3 ):
699
+ v = [q [d ] for q in p ]
700
+ interior_point [d ] = mean ([max (v ),min (v )])/ max (abs (array (ax_limits [d ])))
701
+ colors [i ] = sum (interior_point ) # norm(interior_point)
702
+ lw = min ([1 ,6.0 / len (triang )])
703
+ plot1 = ax .plot_trisurf (x ,y ,z ,triangles = triang ,linewidth = lw ,edgecolors = 'black' , antialiased = True , alpha = 0.90 ) # array=colors, cmap=plt.cm.winter
704
+ colors = colors / max (colors )
705
+ colors = get_cmap ("Spectral" )(colors )
706
+ plot1 .set_fc (colors )
599
707
if show :
600
708
plt .show ()
601
709
return plot1
602
-
603
- def yield_space (model , axes , ** kwargs ):
604
- reaction_ids = model .reactions .list_attr ("id" )
605
-
606
- if CONSTRAINTS in kwargs :
607
- kwargs [CONSTRAINTS ] = parse_constraints (kwargs [CONSTRAINTS ],reaction_ids )
608
- else :
609
- kwargs [CONSTRAINTS ] = None
610
-
611
- if SOLVER not in kwargs :
612
- kwargs [SOLVER ] = None
613
- solver = select_solver (kwargs [SOLVER ],model )
614
-
615
- if 'show' in kwargs :
616
- show = kwargs ['show' ]
617
- else :
618
- show = True
619
-
620
- axes = [list (ax ) for ax in axes ] # cast to list of lists
621
- num_axes = len (axes )
622
- if num_axes not in [2 ,3 ]:
623
- raise Exception ('Please define 2 or 3 axes as a list of tuples [ax1, ax2, (optional) ax3] with ax1 = (den,num).\n ' + \
624
- '"den" and "num" being linear expressions.' )
625
-
626
- if 'points' in kwargs :
627
- points = kwargs ['points' ]
628
- else :
629
- if num_axes == 2 :
630
- points = 40
631
- else :
632
- points = 25
633
-
634
- ax_name = ["" for _ in range (num_axes )]
635
- ax_limits = [(nan ,nan ) for _ in range (num_axes )]
636
- for i ,ax in enumerate (axes ):
637
- ax [0 ] = parse_linexpr (ax [0 ],reaction_ids )[0 ]
638
- ax [1 ] = parse_linexpr (ax [1 ],reaction_ids )[0 ]
639
- ax_name [i ] = '(' + linexprdict2str (ax [0 ])+ ') / (' + linexprdict2str (ax [1 ])+ ')'
640
- sol_min = yopt (model ,obj_num = ax [0 ],obj_den = ax [1 ],constraints = kwargs [CONSTRAINTS ],solver = solver ,obj_sense = 'minimize' )
641
- sol_max = yopt (model ,obj_num = ax [0 ],obj_den = ax [1 ],constraints = kwargs [CONSTRAINTS ],solver = solver ,obj_sense = 'maximize' )
642
- # abort if any of the yields are unbounded or undefined
643
- unbnd = [i + 1 for i ,v in enumerate ([sol_min ,sol_max ]) if v .status == UNBOUNDED ]
644
- if any (unbnd ):
645
- raise Exception ('One of the specified yields is unbounded or undefined. Yield space cannot be generated.' )
646
- ax_limits [i ] = [min ((0 ,sol_min .objective_value )),max ((0 ,sol_max .objective_value ))]
647
-
648
- # compute points
649
- x_space = linspace (ax_limits [0 ][0 ], ax_limits [0 ][1 ], num = points )
650
- lb = full (points , nan )
651
- ub = full (points , nan )
652
- for i ,x in enumerate (x_space ):
653
- constr = [{** axes [0 ][0 ], ** {k :- v * x for k ,v in axes [0 ][1 ].items ()}},'=' ,0 ]
654
- sol_vmin = yopt (model ,constraints = constr ,obj_num = axes [1 ][0 ],obj_den = axes [1 ][1 ],obj_sense = 'minimize' )
655
- lb [i ] = sol_vmin .objective_value
656
- sol_vmax = yopt (model ,constraints = constr ,obj_num = axes [1 ][0 ],obj_den = axes [1 ][1 ],obj_sense = 'maximize' )
657
- ub [i ] = sol_vmax .objective_value
658
710
659
- if num_axes == 2 :
660
- x = [v for v in x_space ] + [v for v in reversed (x_space )]
661
- y = [v for v in lb ] + [v for v in reversed (ub )]
662
- if lb [0 ] != ub [0 ]:
663
- x .extend ([x_space [0 ], x_space [0 ]])
664
- y .extend ([lb [0 ], ub [0 ]])
665
- plot1 = plt .fill (x , y )
666
- plot1 [0 ].axes .set_xlabel (ax_name [0 ])
667
- plot1 [0 ].axes .set_ylabel (ax_name [1 ])
668
- plot1 [0 ].axes .set_xlim (ax_limits [0 ][0 ]* 1.05 ,ax_limits [0 ][1 ]* 1.05 )
669
- plot1 [0 ].axes .set_ylim (ax_limits [1 ][0 ]* 1.05 ,ax_limits [1 ][1 ]* 1.05 )
670
- if show :
671
- plt .show ()
672
- return plot1
711
+ def ceil_dec (v ,n ):
712
+ return ceil (v * (10 ** n ))/ (10 ** n )
673
713
674
- elif num_axes == 3 :
675
- max_diff_y = max ([abs (l - u ) for l ,u in zip (lb ,ub )])
676
- datapoints = []
677
- for x ,l ,u in zip (x_space ,lb ,ub ):
678
- if l - u != 0 :
679
- y_space = linspace (l ,u ,int (- (- points // (max_diff_y / abs (l - u )))))
680
- else :
681
- y_space = [0.0 ]
682
- for y in y_space :
683
- constr = [[{** axes [0 ][0 ], ** {k :- v * x for k ,v in axes [0 ][1 ].items ()}},'=' ,0 ],\
684
- [{** axes [1 ][0 ], ** {k :- v * y for k ,v in axes [1 ][1 ].items ()}},'=' ,0 ]]
685
- sol_vmin = yopt (model ,constraints = constr ,obj_num = axes [2 ][0 ],obj_den = axes [2 ][1 ],obj_sense = 'minimize' )
686
- datapoints += [[x ,y ,sol_vmin .objective_value ]]
687
- sol_vmax = yopt (model ,constraints = constr ,obj_num = axes [2 ][0 ],obj_den = axes [2 ][1 ],obj_sense = 'maximize' )
688
- datapoints += [[x ,y ,sol_vmax .objective_value ]]
689
- hull = ConvexHull (datapoints )
690
- x = [d [0 ] for d in datapoints ]
691
- y = [d [1 ] for d in datapoints ]
692
- z = [d [2 ] for d in datapoints ]
693
- ax = a3 .Axes3D (plt .figure ())
694
- ax .dist = 10
695
- ax .azim = 30
696
- ax .elev = 10
697
- ax .set_xlim (ax_limits [0 ])
698
- ax .set_ylim (ax_limits [1 ])
699
- ax .set_zlim (ax_limits [2 ])
700
- ax .set_xlabel (ax_name [0 ])
701
- ax .set_ylabel (ax_name [1 ])
702
- ax .set_zlabel (ax_name [2 ])
703
- plot1 = ax .plot_trisurf (x ,y ,z ,triangles = hull .simplices ,linewidth = 0.2 , antialiased = True , alpha = 0.67 )
704
- if show :
705
- plt .show ()
706
- return plot1
714
+ def floor_dec (v ,n ):
715
+ return floor (v * (10 ** n ))/ (10 ** n )
0 commit comments