diff --git a/README.md b/README.md index 3976674..280e739 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A package for visual analysis of biochemical reaction network models -Copyright 2018-2021 Kiri Choi +Copyright 2018-2022 Kiri Choi ## Introduction diff --git a/netplotlib/VERSION.txt b/netplotlib/VERSION.txt index 867e524..cb174d5 100644 --- a/netplotlib/VERSION.txt +++ b/netplotlib/VERSION.txt @@ -1 +1 @@ -1.2.0 \ No newline at end of file +1.2.1 \ No newline at end of file diff --git a/netplotlib/layout.py b/netplotlib/layout.py index b5c303b..0cd499d 100644 --- a/netplotlib/layout.py +++ b/netplotlib/layout.py @@ -240,9 +240,12 @@ def draw(NetworkClass, show=True, savePath=None, dpi=150): speciesName = speciesName.getId() else: sg = layout.getSpeciesGlyph(tg.getOriginOfTextId()) - speciesName = sg.getSpeciesId() + if sg != None: + speciesName = sg.getSpeciesId() + else: + speciesName = tg.text layoutTextGlyphIds.append(speciesName) - + dim = bbox.getDimensions() mattext = ax.text(tgpos.x_offset, tgpos.y_offset, speciesName, diff --git a/netplotlib/netplotlib.py b/netplotlib/netplotlib.py index c3457f5..ad81ad6 100644 --- a/netplotlib/netplotlib.py +++ b/netplotlib/netplotlib.py @@ -15,6 +15,7 @@ import sympy import itertools import layout +import toolbox import libsbml def getVersion(): @@ -32,17 +33,6 @@ def getVersion(): return version -def getListOfAlgorithms(): - """ - Print list of supported layout algorithms - """ - - algList = ['kamada-kawai', 'spring', 'twopi', 'neato', 'dot'] - algList.sort() - - return algList - - class _Variable(): def __init__(self): @@ -80,6 +70,7 @@ def __init__(self, model): try: self._Var.boundaryId = self.rrInstance.getBoundarySpeciesIds() self._Var.floatingId = self.rrInstance.getFloatingSpeciesIds() + self._Var.compartmentId = self.rrInstance.getCompartmentIds() self._Var.rid = self.rrInstance.getReactionIds() self._Var.stoch = self.rrInstance.getFullStoichiometryMatrix() self._Var.stoch_row = self._Var.stoch.rownames @@ -105,6 +96,7 @@ def reset(self): self.boundaryColor = 'tab:green' self.nodeEdgeColor = 'k' self.nodeEdgelw = 0 + self.edgeType = 'default' self.compartmentColor = 'tab:gray' self.compartmentEdgeColor = 'k' self.compartmentEdgelw = 2 @@ -176,7 +168,7 @@ def getLayout(self, returnState=False): :returns pos: Dictionary of all nodes and corresponding coordinates """ - if self.layoutAlgorithm not in getListOfAlgorithms(): + if self.layoutAlgorithm not in toolbox.getListOfAlgorithms(): raise Exception("Unsupported layout algorithm: '" + str(self.layoutAlgorithm) + "'") avoid = ['C', 'CC', 'Ci', 'E1', 'EX', 'Ei', 'FF', 'GF', 'Ge', 'Gt', 'I', 'LC', @@ -521,6 +513,8 @@ def draw(self, show=True, savePath=None, dpi=150): :param dpi: dpi settings for the diagram """ + toolbox.checkValidity(self) + if self._Var.pos == None: pos = self.getLayout() else: @@ -583,6 +577,19 @@ def draw(self, show=True, savePath=None, dpi=150): max_width = [min(max_width), max(max_width)] max_height = [min(max_height), max(max_height)] + # add compartments first + for com in self._Var.compartmentId: + comBox = FancyBboxPatch((max_width[0]-(max_width[1]-max_width[0])/10, + max_height[0]-(max_height[1]-max_height[0])/10), + max_width[1]-max_width[0]+2*(max_width[1]-max_width[0])/10, + max_height[1]-max_height[0]+2*(max_height[1]-max_height[0])/10, + boxstyle="round,pad=0.01, rounding_size=0.01", + linewidth=self.compartmentEdgelw, + edgecolor=self.compartmentEdgeColor, + facecolor=self.compartmentColor, + alpha=0.5) + ax.add_patch(comBox) + hlInd = 0 # add nodes to the figure for n in self._Var.G: @@ -755,8 +762,17 @@ def draw(self, show=True, savePath=None, dpi=150): (np.abs(n_1) < np.shape(stackXY2)[1] - 75)): n_1 -= 1 - lpath1 = Path(stackXY1.T) - lpath2 = Path(stackXY2.T[:n_1]) + if self.edgeType == 'default': + lpath1 = Path(stackXY1.T) + lpath2 = Path(stackXY2.T[:n_1]) + elif self.edgeType == 'bezier': + bcp = toolbox.computeBezierControlPoints(stackXY1.T[0], X2, stackXY1.T[-1]) + lpath1 = Path([bcp[0], bcp[1], bcp[2], bcp[3]], + [Path.MOVETO, Path.CURVE4, Path.CURVE4, Path.CURVE4]) + bcp = toolbox.computeBezierControlPoints(stackXY2.T[0], X2, stackXY2.T[n_1]) + lpath2 = Path([bcp[0], bcp[1], bcp[2], bcp[3]], + [Path.MOVETO, Path.CURVE4, Path.CURVE4, Path.CURVE4]) + lw1 = (1+self.edgelw) lw2 = (1+self.edgelw) arrowstyle1 = ArrowStyle.CurveFilledA(head_length=0.8, head_width=0.4) @@ -781,8 +797,13 @@ def draw(self, show=True, savePath=None, dpi=150): (stackXY1.T[n_2][1] < (X1top[1]+arrthres_v))) and (np.abs(n_2) < np.shape(stackXY1)[1] - 75)): n_2 += 1 - - lpath1 = Path(stackXY1.T[n_2:]) + + if self.edgeType == 'default': + lpath1 = Path(stackXY1.T[n_2:]) + elif self.edgeType == 'bezier': + bcp = toolbox.computeBezierControlPoints(stackXY1.T[n_2], X2, stackXY1.T[-1]) + lpath1 = Path([bcp[0], bcp[1], bcp[2], bcp[3]], + [Path.MOVETO, Path.CURVE4, Path.CURVE4, Path.CURVE4]) else: arrowstyle1 = ArrowStyle.Curve() @@ -823,12 +844,12 @@ def draw(self, show=True, savePath=None, dpi=150): if j[k][0] in self._Var.floatingId: if (np.abs(self._Var.stoch[self._Var.stoch_row.index(j[k][0])][i]) > 1): # position calculation - slope = ((lpath1.vertices[0][1] - lpath1.vertices[35][1])/ - (lpath1.vertices[0][0] - lpath1.vertices[35][0])) + slope = ((lpath1.vertices[0][1] - lpath1.vertices[int(0.35*len(lpath1.vertices))][1])/ + (lpath1.vertices[0][0] - lpath1.vertices[int(0.35*len(lpath1.vertices))][0])) x_prime = np.sqrt(0.01/(1 + np.square(slope)))*(self.fontsize/20)*max(self.scale/2, 1) y_prime = -slope*x_prime - ax.text(x_prime+lpath1.vertices[35][0], - y_prime+lpath1.vertices[35][1], + ax.text(x_prime+lpath1.vertices[int(0.35*len(lpath1.vertices))][0], + y_prime+lpath1.vertices[int(0.35*len(lpath1.vertices))][1], int(np.abs(self._Var.stoch[self._Var.stoch_row.index(j[k][0])][i])), fontsize=self.fontsize, horizontalalignment='center', @@ -837,12 +858,12 @@ def draw(self, show=True, savePath=None, dpi=150): if j[k][1] in self._Var.floatingId: if (np.abs(self._Var.stoch[self._Var.stoch_row.index(j[k][1])][i]) > 1): - slope = ((lpath2.vertices[0][1] - lpath2.vertices[-35][1])/ - (lpath2.vertices[0][0] - lpath2.vertices[-35][0])) + slope = ((lpath2.vertices[0][1] - lpath2.vertices[int(0.65*len(lpath2.vertices))][1])/ + (lpath2.vertices[0][0] - lpath2.vertices[int(0.65*len(lpath2.vertices))][0])) x_prime = np.sqrt(0.01/(1 + np.square(slope)))*(self.fontsize/20)*max(self.scale/2, 1) y_prime = -slope*x_prime - ax.text(x_prime+lpath2.vertices[-35][0], - y_prime+lpath2.vertices[-35][1], + ax.text(x_prime+lpath2.vertices[int(0.65*len(lpath2.vertices))][0], + y_prime+lpath2.vertices[int(0.65*len(lpath2.vertices))][1], int(np.abs(self._Var.stoch[self._Var.stoch_row.index(j[k][1])][i])), fontsize=self.fontsize, horizontalalignment='center', @@ -963,7 +984,12 @@ def draw(self, show=True, savePath=None, dpi=150): (np.abs(n_2) < np.shape(stackXY)[1] - 75)): n_2 += 1 - lpath = Path(stackXY.T[n_2:n_1]) + if self.edgeType == 'default': + lpath = Path(stackXY.T[n_2:n_1]) + elif self.edgeType == 'bezier': + bcp = toolbox.computeBezierControlPoints(stackXY.T[n_2], X2, stackXY.T[n_1]) + lpath = Path([bcp[0], bcp[1], bcp[2], bcp[3]], + [Path.MOVETO, Path.CURVE4, Path.CURVE4, Path.CURVE4]) if self.analyzeFlux: if self._Var.flux[i] > 0: @@ -1021,8 +1047,14 @@ def draw(self, show=True, savePath=None, dpi=150): e1color = colormap(norm(self._Var.flux[i])) else: e1color = self.reactionColor - - lpath = Path(stackXY.T[:n_1]) + + if self.edgeType == 'default': + lpath = Path(stackXY.T[:n_1]) + elif self.edgeType == 'bezier': + bcp = toolbox.computeBezierControlPoints(stackXY.T[0], X2, stackXY.T[n_1]) + lpath = Path([bcp[0], bcp[1], bcp[2], bcp[3]], + [Path.MOVETO, Path.CURVE4, Path.CURVE4, Path.CURVE4]) + arrowstyle1 = ArrowStyle.CurveFilledB(head_length=0.8, head_width=0.4) lw1 = (1+self.edgelw) e = FancyArrowPatch(path=lpath, @@ -1034,12 +1066,12 @@ def draw(self, show=True, savePath=None, dpi=150): if j[k][0] in self._Var.floatingId: if (np.abs(self._Var.stoch[self._Var.stoch_row.index(j[k][0])][i]) > 1): - slope = ((lpath.vertices[0][1] - lpath.vertices[35][1])/ - (lpath.vertices[0][0] - lpath.vertices[35][0])) + slope = ((lpath.vertices[0][1] - lpath.vertices[int(0.35*len(lpath.vertices))][1])/ + (lpath.vertices[0][0] - lpath.vertices[int(0.35*len(lpath.vertices))][0])) x_prime = np.sqrt(0.01/(1 + np.square(slope)))*max(self.scale/2, 1) y_prime = -slope*x_prime - ax.text(x_prime+lpath.vertices[35][0], - y_prime+lpath.vertices[35][1], + ax.text(x_prime+lpath.vertices[int(0.35*len(lpath.vertices))][0], + y_prime+lpath.vertices[int(0.35*len(lpath.vertices))][1], int(np.abs(self._Var.stoch[self._Var.stoch_row.index(j[k][0])][i])), fontsize=self.fontsize, horizontalalignment='center', @@ -1048,12 +1080,12 @@ def draw(self, show=True, savePath=None, dpi=150): if j[k][1] in self._Var.floatingId: if (np.abs(self._Var.stoch[self._Var.stoch_row.index(j[k][1])][i]) > 1): - slope = ((lpath.vertices[0][1] - lpath.vertices[-25][1])/ - (lpath.vertices[0][0] - lpath.vertices[-25][0])) + slope = ((lpath.vertices[0][1] - lpath.vertices[int(0.75*len(lpath.vertices))][1])/ + (lpath.vertices[0][0] - lpath.vertices[int(0.75*len(lpath.vertices))][0])) x_prime = np.sqrt(0.01/(1 + np.square(slope)))*max(self.scale/2, 1) y_prime = -slope*x_prime - ax.text(x_prime+lpath.vertices[-25][0], - y_prime+lpath.vertices[-25][1], + ax.text(x_prime+lpath.vertices[int(0.75*len(lpath.vertices))][0], + y_prime+lpath.vertices[int(0.75*len(lpath.vertices))][1], int(np.abs(self._Var.stoch[self._Var.stoch_row.index(j[k][1])][i])), fontsize=self.fontsize, horizontalalignment='center', @@ -1127,7 +1159,12 @@ def draw(self, show=True, savePath=None, dpi=150): (np.abs(n_2) < np.shape(stackXY)[1] - 75)): n_2 += 1 - lpath = Path(stackXY.T[n_2:n_1]) + if self.edgeType == 'default': + lpath = Path(stackXY.T[n_2:n_1]) + elif self.edgeType == 'bezier': + bcp = toolbox.computeBezierControlPoints(stackXY.T[n_2], X2, stackXY.T[n_1]) + lpath = Path([bcp[0], bcp[1], bcp[2], bcp[3]], + [Path.MOVETO, Path.CURVE4, Path.CURVE4, Path.CURVE4]) if self.analyzeFlux: if self._Var.flux[i] > 0: @@ -1185,8 +1222,14 @@ def draw(self, show=True, savePath=None, dpi=150): e1color = colormap(norm(self._Var.flux[i])) else: e1color = self.reactionColor - - lpath = Path(stackXY.T[:n_1]) + + if self.edgeType == 'default': + lpath = Path(stackXY.T[:n_1]) + elif self.edgeType == 'bezier': + bcp = toolbox.computeBezierControlPoints(stackXY.T[0], X2, stackXY.T[n_1]) + lpath = Path([bcp[0], bcp[1], bcp[2], bcp[3]], + [Path.MOVETO, Path.CURVE4, Path.CURVE4, Path.CURVE4]) + arrowstyle1 = ArrowStyle.CurveFilledB(head_length=0.8, head_width=0.4) lw1 = (1+self.edgelw) e = FancyArrowPatch(path=lpath, @@ -1198,12 +1241,12 @@ def draw(self, show=True, savePath=None, dpi=150): if j[0] in self._Var.floatingId: if (np.abs(self._Var.stoch[self._Var.stoch_row.index(j[0])][i]) > 1): - slope = ((lpath.vertices[0][1] - lpath.vertices[15][1])/ - (lpath.vertices[0][0] - lpath.vertices[15][0])) + slope = ((lpath.vertices[0][1] - lpath.vertices[int(0.15*len(lpath.vertices))][1])/ + (lpath.vertices[0][0] - lpath.vertices[int(0.15*len(lpath.vertices))][0])) x_prime = np.sqrt(0.01/(1 + np.square(slope)))*(self.fontsize/20)*max(self.scale/2, 1) y_prime = -slope*x_prime - ax.text(x_prime+lpath.vertices[15][0], - y_prime+lpath.vertices[15][1], + ax.text(x_prime+lpath.vertices[int(0.15*len(lpath.vertices))][0], + y_prime+lpath.vertices[int(0.15*len(lpath.vertices))][1], int(np.abs(self._Var.stoch[self._Var.stoch_row.index(j[0])][i])), fontsize=self.fontsize, horizontalalignment='center', @@ -1211,12 +1254,12 @@ def draw(self, show=True, savePath=None, dpi=150): color=self.reactionColor) if j[1] in self._Var.floatingId: if (np.abs(self._Var.stoch[self._Var.stoch_row.index(j[1])][i]) > 1): - slope = ((lpath.vertices[0][1] - lpath.vertices[-20][1])/ - (lpath.vertices[0][0] - lpath.vertices[-20][0])) + slope = ((lpath.vertices[0][1] - lpath.vertices[int(0.8*len(lpath.vertices))][1])/ + (lpath.vertices[0][0] - lpath.vertices[int(0.8*len(lpath.vertices))][0])) x_prime = np.sqrt(0.01/(1 + np.square(slope)))*(self.fontsize/20)*max(self.scale/2, 1) y_prime = -slope*x_prime - ax.text(x_prime+lpath.vertices[-20][0], - y_prime+lpath.vertices[-20][1], + ax.text(x_prime+lpath.vertices[int(0.8*len(lpath.vertices))][0], + y_prime+lpath.vertices[int(0.8*len(lpath.vertices))][1], int(np.abs(self._Var.stoch[self._Var.stoch_row.index(j[1])][i])), fontsize=self.fontsize, horizontalalignment='center', @@ -1425,7 +1468,7 @@ def getLayout(self): Return the layout of the model """ - if self.layoutAlgorithm not in getListOfAlgorithms(): + if self.layoutAlgorithm not in toolbox.getListOfAlgorithms(): raise Exception("Unsupported layout algorithm: '" + str(self.layoutAlgorithm) + "'") avoid = ['C', 'CC', 'Ci', 'E1', 'EX', 'Ei', 'FF', 'GF', 'Ge', 'Gt', 'I', 'LC', @@ -2279,7 +2322,7 @@ def drawNetworkGrid(self, nrows, ncols, auto=False, show=True, savePath=None, dp :param dpi: dpi settings for the diagram """ - if self.layoutAlgorithm not in getListOfAlgorithms(): + if self.layoutAlgorithm not in toolbox.getListOfAlgorithms(): raise Exception("Unsupported layout algorithm: '" + str(self.layoutAlgorithm) + "'") edgelw_backup = self.edgelw diff --git a/netplotlib/toolbox.py b/netplotlib/toolbox.py new file mode 100644 index 0000000..8d46740 --- /dev/null +++ b/netplotlib/toolbox.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- + +import numpy as np +from scipy.special import comb +import matplotlib.pyplot as plt +from matplotlib.colors import is_color_like + +def getListOfAlgorithms(): + """ + Print list of supported layout algorithms + """ + + algList = ['kamada-kawai', 'spring', 'twopi', 'neato', 'dot'] + algList.sort() + + return algList + +def checkValidity(self): + + if not isinstance(self.scale, (int, float)): + raise Exception('scale paramter only accepts number') + elif not isinstance(self.fontsize, (int, float)): + raise Exception('fontsize paramter only accepts number') + elif not isinstance(self.edgelw, (int, float)): + raise Exception('edgelw paramter only accepts number') + elif not is_color_like(self.nodeColor): + raise Exception('nodeColor paramter does not look like a color') + elif not is_color_like(self.reactionNodeColor): + raise Exception('reactionNodeColor paramter does not look like a color') + elif not is_color_like(self.labelColor): + raise Exception('labelColor paramter does not look like a color') + elif type(self.labelReactionIds) is not bool: + raise Exception('labelReactionIds paramter only accepts boolean') + elif not is_color_like(self.reactionColor): + raise Exception('reactionColor paramter does not look like a color') + elif not is_color_like(self.modifierColor): + raise Exception('modifierColor paramter does not look like a color') + elif not is_color_like(self.boundaryColor): + raise Exception('boundaryColor paramter does not look like a color') + elif not is_color_like(self.nodeEdgeColor): + raise Exception('nodeEdgeColor paramter does not look like a color') + elif not isinstance(self.nodeEdgelw, (int, float)): + raise Exception('nodeEdgelw paramter only accepts number') + elif self.edgeType != 'default' and self.edgeType != 'bezier': + raise Exception('unknown edgeType') + elif not is_color_like(self.compartmentColor): + raise Exception('compartmentColor paramter does not look like a color') + elif not is_color_like(self.compartmentEdgeColor): + raise Exception('compartmentEdgeColor paramter does not look like a color') + elif not isinstance(self.compartmentEdgelw, (int, float)): + raise Exception('compartmentEdgelw paramter only accepts number') + elif type(self.highlight) is not list: + raise Exception('highlight paramter only accepts list') + elif not is_color_like(self.hlNodeColor): + raise Exception('hlNodeColor paramter does not look like a color') + elif not is_color_like(self.hlNodeEdgeColor): + raise Exception('hlNodeEdgeColor paramter does not look like a color') + elif type(self.drawReactionNode) is not bool: + raise Exception('drawReactionNode paramter only accepts boolean') + elif type(self.breakBoundary) is not bool: + raise Exception('breakBoundary paramter only accepts boolean') + elif type(self.tightLayout) is not bool: + raise Exception('tightLayout paramter only accepts boolean') + elif type(self.analyzeFlux) is not bool: + raise Exception('analyzeFlux paramter only accepts boolean') + elif type(self.analyzeRates) is not bool: + raise Exception('analyzeRates paramter only accepts boolean') + elif not is_color_like(self.analyzeColorHigh): + raise Exception('analyzeColorHigh paramter does not look like a color') + elif not is_color_like(self.analyzeColorLow): + raise Exception('analyzeColorLow paramter does not look like a color') + elif self.analyzeColorMap not in plt.colormaps(): + raise Exception('analyzeColorMap paramter does not look like a colormap') + elif type(self.analyzeColorScale) is not bool: + raise Exception('analyzeColorScale paramter only accepts boolean') + elif type(self.drawInlineTimeCourse) is not bool: + raise Exception('drawInlineTimeCourse paramter only accepts boolean') + elif not isinstance(self.nodeEdgelw, (int, float)): + raise Exception('nodeEdgelw paramter only accepts number') + elif not isinstance(self.nodeEdgelw, (int, float)): + raise Exception('nodeEdgelw paramter only accepts number') + elif not isinstance(self.simulationStartTime, (int, float)): + raise Exception('simulationStartTime paramter only accepts number') + elif not isinstance(self.simulationEndTime, (int, float)): + raise Exception('simulationEndTime paramter only accepts number') + elif not isinstance(self.numPoints, (int)): + raise Exception('numPoints paramter only accepts integer') + elif type(self.plotStatistics) is not bool: + raise Exception('plotStatistics paramter only accepts boolean') + elif type(self.forceAnalysisAtEndTime) is not bool: + raise Exception('forceAnalysisAtEndTime paramter only accepts boolean') + elif type(self.plotColorbar) is not bool: + raise Exception('plotColorbar paramter only accepts boolean') + elif type(self.inlineTimeCourseSelections) is not list: + raise Exception('inlineTimeCourseSelections paramter only accepts list') + elif type(self.ignoreLayout) is not bool: + raise Exception('ignoreLayout paramter only accepts boolean') + +def computeBezierControlPoints(start, intermediate, end): + + def bernpoly(n, t, k): + return np.power(t,k)*np.power((1 - t), (n - k))*comb(n, k) + + def bernmatrix(T): + return np.matrix([[bernpoly(3, t, k) for k in range(4)] for t in T]) + + def lsfit(points, M): + M_ = np.linalg.pinv(M) + return M_*points + + T = np.linspace(0, 1, 3) + M = bernmatrix(T) + points = np.array([start, intermediate, end]) + + bcp = lsfit(points, M).tolist() + bcp = [tuple(x) for x in bcp] + bcp[0] = tuple(start) + bcp[-1] = tuple(end) + + return bcp