plotter.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. import numpy as np
  2. from matplotlib.figure import Figure
  3. from mpl_toolkits.mplot3d import Axes3D
  4. from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
  5. from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
  6. from PyQt5.QtCore import Qt
  7. from PyQt5.QtWidgets import QVBoxLayout, QLabel, QSlider, QSpinBox, QPushButton, QSplitter
  8. from visma.io.checks import getVariables, getTokensType
  9. from visma.io.tokenize import getLHSandRHS
  10. from visma.functions.constant import Constant
  11. from visma.functions.operator import Binary
  12. from visma.functions.structure import FuncOp
  13. from visma.functions.variable import Variable
  14. def graphPlot(workspace, again, tokens):
  15. """Function for plotting graphs in 2D and 3D space
  16. 2D graphs are plotted for expression in one variable and equations in two variables. 3D graphs are plotted for expressions in two variables and equations in three variables.
  17. Arguments:
  18. workspace {QtWidgets.QWidget} -- main layout
  19. Returns:
  20. graphVars {list} -- variables to be plotted on the graph
  21. func {numpy.array(2D)/function(3D)} -- equation converted to compatible data type for plotting
  22. variables {list} -- variables in given equation
  23. again {bool} -- True when an equation can be plotted in 2D and 3D both else False
  24. Note:
  25. The func obtained from graphPlot() function is of different type for 2D and 3D plots. For 2D, func is a numpy array, and for 3D, func is a function.
  26. """
  27. if tokens is None:
  28. axisRange = workspace.axisRange
  29. tokens = workspace.eqToks[-1]
  30. else:
  31. axisRange = [10, 10, 10, 30]
  32. eqType = getTokensType(tokens)
  33. LHStok, RHStok = getLHSandRHS(tokens)
  34. variables = sorted(getVariables(LHStok, RHStok))
  35. dim = len(variables)
  36. if (dim == 1) or ((dim == 2) and eqType == "equation"):
  37. if again:
  38. variables.append('f(' + variables[0] + ')')
  39. graphVars, func = plotIn3D(LHStok, RHStok, variables, axisRange)
  40. else:
  41. graphVars, func = plotIn2D(LHStok, RHStok, variables, axisRange)
  42. if dim == 1:
  43. variables.append('f(' + variables[0] + ')')
  44. elif (dim == 2 and eqType == "expression") or ((dim == 3) and eqType == "equation"):
  45. graphVars, func = plotIn3D(LHStok, RHStok, variables, axisRange)
  46. if dim == 2:
  47. variables.append('f(' + variables[0] + ',' + variables[1] + ')')
  48. else:
  49. return [], None, None
  50. return graphVars, func, variables
  51. def plotIn2D(LHStok, RHStok, variables, axisRange):
  52. """Returns function array for 2D plots
  53. Arguments:
  54. LHStok {list} -- expression tokens
  55. RHStok {list} -- expression tokens
  56. variables {list} -- variables in equation
  57. axisRange {list} -- axis limits
  58. Returns:
  59. graphVars {list} -- variables for plotting
  60. func {numpy.array} -- equation to be plotted in 2D
  61. """
  62. xmin = -axisRange[0]
  63. xmax = axisRange[0]
  64. ymin = -axisRange[1]
  65. ymax = axisRange[1]
  66. xdelta = 0.01 * (xmax - xmin)
  67. ydelta = 0.01 * (ymax - ymin)
  68. xrange = np.arange(xmin, xmax, xdelta)
  69. yrange = np.arange(ymin, ymax, ydelta)
  70. graphVars = np.meshgrid(xrange, yrange)
  71. function = getFunction(LHStok, RHStok, variables, graphVars, 2)
  72. return graphVars, function
  73. def plotIn3D(LHStok, RHStok, variables, axisRange):
  74. """Returns function for 3D plots
  75. Arguments:
  76. LHStok {list} -- expression tokens
  77. RHStok {list} -- expression tokens
  78. variables {list} -- variables in equation
  79. axisRange {list} -- axis limits
  80. Returns:
  81. graphVars {list} -- variables for plotting
  82. func {function} -- equation to be plotted in 3D
  83. """
  84. xmin = -axisRange[0]
  85. xmax = axisRange[0]
  86. ymin = -axisRange[1]
  87. ymax = axisRange[1]
  88. zmin = -axisRange[2]
  89. zmax = axisRange[2]
  90. meshLayers = axisRange[3]
  91. xrange = np.linspace(xmin, xmax, meshLayers)
  92. yrange = np.linspace(ymin, ymax, meshLayers)
  93. zrange = np.linspace(zmin, zmax, meshLayers)
  94. graphVars = [xrange, yrange, zrange]
  95. def func(x, y, z):
  96. graphVars = [x, y, z]
  97. return getFunction(LHStok, RHStok, variables, graphVars, 3)
  98. return graphVars, func
  99. def getFunction(LHStok, RHStok, eqnVars, graphVars, dim):
  100. """Returns function for plotting
  101. Arguments:
  102. LHStok {list} -- expression tokens
  103. RHStok {list} -- expression tokens
  104. eqnVars {list} -- variables in equation
  105. graphVars {list} -- variables for plotting
  106. dim {int} -- dimenion of plot
  107. Returns:
  108. (LHS - RHS) {numpy.array(2D)/function(3D)} -- equation converted to compatible data type for plotting
  109. """
  110. LHS = getFuncExpr(LHStok, eqnVars, graphVars)
  111. if len(eqnVars) == dim:
  112. RHS = getFuncExpr(RHStok, eqnVars, graphVars)
  113. elif len(eqnVars) == dim - 1:
  114. RHS = graphVars[-1]
  115. return LHS - RHS
  116. def getFuncExpr(exprTok, eqnVars, graphVars):
  117. """Allocates variables in equation to graph variables to give final function compatible for plotting
  118. Arguments:
  119. exprTok {list} -- expression tokens
  120. eqnVars {list} -- variables in equation
  121. graphVars {list} -- variables for plotting
  122. Returns:
  123. expr {numpy.array(2D)/function(3D)} -- expression converted to compatible data type for plotting
  124. """
  125. expr = 0
  126. coeff = 1
  127. for token in exprTok:
  128. if isinstance(token, Variable):
  129. varProduct = 1
  130. for value, power in zip(token.value, token.power):
  131. varProduct *= graphVars[eqnVars.index(value)]**power
  132. expr += coeff * token.coefficient * varProduct
  133. elif isinstance(token, Constant):
  134. expr += coeff * token.value
  135. elif isinstance(token, FuncOp):
  136. pass
  137. elif isinstance(token, Binary) and token.value == '-':
  138. coeff = -1
  139. elif isinstance(token, Binary) and token.value == '+':
  140. coeff = 1
  141. return expr
  142. #######
  143. # GUI #
  144. #######
  145. def plotFigure2D(workspace):
  146. """GUI layout for plot figure
  147. Arguments:
  148. workspace {QtWidgets.QWidget} -- main layout
  149. Returns:
  150. layout {QtWidgets.QVBoxLayout} -- contains matplot figure
  151. """
  152. workspace.figure2D = Figure()
  153. workspace.canvas2D = FigureCanvas(workspace.figure2D)
  154. # workspace.figure2D.patch.set_facecolor('white')
  155. class NavigationCustomToolbar(NavigationToolbar):
  156. toolitems = [t for t in NavigationToolbar.toolitems if t[0] in ()]
  157. workspace.toolbar2D = NavigationCustomToolbar(workspace.canvas2D, workspace)
  158. layout = QVBoxLayout()
  159. layout.addWidget(workspace.canvas2D)
  160. layout.addWidget(workspace.toolbar2D)
  161. return layout
  162. def plotFigure3D(workspace):
  163. """GUI layout for plot figure
  164. Arguments:
  165. workspace {QtWidgets.QWidget} -- main layout
  166. Returns:
  167. layout {QtWidgets.QVBoxLayout} -- contains matplot figure
  168. """
  169. workspace.figure3D = Figure()
  170. workspace.canvas3D = FigureCanvas(workspace.figure3D)
  171. # workspace.figure3D.patch.set_facecolor('white')
  172. class NavigationCustomToolbar(NavigationToolbar):
  173. toolitems = [t for t in NavigationToolbar.toolitems if t[0] in ()]
  174. workspace.toolbar3D = NavigationCustomToolbar(workspace.canvas3D, workspace)
  175. layout = QVBoxLayout()
  176. layout.addWidget(workspace.canvas3D)
  177. layout.addWidget(workspace.toolbar3D)
  178. return layout
  179. def renderPlot(workspace, graphVars, func, variables, tokens=None):
  180. """Renders plot for functions in 2D and 3D
  181. Maps points from the numpy arrays for variables in given equation on the 2D/3D plot figure
  182. Arguments:
  183. workspace {QtWidgets.QWidget} -- main layout
  184. graphVars {list} -- variables for plotting
  185. dim {int} -- dimenion of plot
  186. variables {list} -- variables in equation
  187. """
  188. if len(graphVars) == 2:
  189. X, Y = graphVars[0], graphVars[1]
  190. ax = workspace.figure2D.add_subplot(111)
  191. ax.clear()
  192. ax.contour(X, Y, func, [0])
  193. ax.grid()
  194. ax.set_xlabel(r'$' + variables[0] + '$')
  195. ax.set_ylabel(r'$' + variables[1] + '$')
  196. workspace.figure2D.set_tight_layout({"pad": 1}) # removes extra padding
  197. workspace.canvas2D.draw()
  198. workspace.tabPlot.setCurrentIndex(0)
  199. elif len(graphVars) == 3:
  200. xrange = graphVars[0]
  201. yrange = graphVars[1]
  202. zrange = graphVars[2]
  203. ax = Axes3D(workspace.figure3D)
  204. for z in zrange:
  205. X, Y = np.meshgrid(xrange, yrange)
  206. Z = func(X, Y, z)
  207. ax.contour(X, Y, Z + z, [z], zdir='z')
  208. for y in yrange:
  209. X, Z = np.meshgrid(xrange, zrange)
  210. Y = func(X, y, Z)
  211. ax.contour(X, Y + y, Z, [y], zdir='y')
  212. for x in xrange:
  213. Y, Z = np.meshgrid(yrange, zrange)
  214. X = func(x, Y, Z)
  215. ax.contour(X + x, Y, Z, [x], zdir='x')
  216. if tokens is None:
  217. axisRange = workspace.axisRange
  218. else:
  219. axisRange = [10, 10, 10, 30]
  220. xmin = -axisRange[0]
  221. xmax = axisRange[0]
  222. ymin = -axisRange[1]
  223. ymax = axisRange[1]
  224. zmin = -axisRange[2]
  225. zmax = axisRange[2]
  226. ax.set_xlim3d(xmin, xmax)
  227. ax.set_ylim3d(ymin, ymax)
  228. ax.set_zlim3d(zmin, zmax)
  229. ax.set_xlabel(r'$' + variables[0] + '$')
  230. ax.set_ylabel(r'$' + variables[1] + '$')
  231. ax.set_zlabel(r'$' + variables[2] + '$')
  232. workspace.canvas3D.draw()
  233. workspace.tabPlot.setCurrentIndex(1)
  234. def plot(workspace, tokens=None):
  235. """When called from window.py it initiates rendering of equations.
  236. Arguments:
  237. workspace {QtWidgets.QWidget} -- main layout
  238. """
  239. from visma.io.tokenize import tokenizer
  240. workspace.figure2D.clear()
  241. workspace.figure3D.clear()
  242. if tokens is None:
  243. tokens = workspace.eqToks[-1]
  244. eqType = getTokensType(tokens)
  245. LHStok, RHStok = getLHSandRHS(tokens)
  246. variables = sorted(getVariables(LHStok, RHStok))
  247. dim = len(variables)
  248. graphVars, func, variables = graphPlot(workspace, False, tokens)
  249. renderPlot(workspace, graphVars, func, variables, tokens)
  250. if (dim == 1):
  251. var2, var3 = selectAdditionalVariable(variables[0])
  252. if tokens is None:
  253. workspace.eqToks[-1] += tokenizer("0" + var2 + "+" + "0" + var3)
  254. else:
  255. tokens += tokenizer("0" + var2 + "+" + "0" + var3)
  256. if (((dim == 2) or (dim == 1)) & (eqType == 'equation')):
  257. graphVars, func, variables = graphPlot(workspace, True, tokens)
  258. renderPlot(workspace, graphVars, func, variables, tokens)
  259. def selectAdditionalVariable(var1):
  260. if var1 == 'z':
  261. var2 = 'a'
  262. var3 = 'b'
  263. return var2, var3
  264. if var1 == 'Z':
  265. var2 = 'A'
  266. var3 = 'B'
  267. return var2, var3
  268. var2 = chr(ord(var1) + 1)
  269. var3 = chr(ord(var1) + 2)
  270. return var2, var3
  271. def refreshPlot(workspace):
  272. if workspace.resultOut is True and workspace.showPlotter is True:
  273. plot(workspace)
  274. ###############
  275. # preferences #
  276. ###############
  277. # TODO: Add status tips, Fix docstrings
  278. def plotPref(workspace):
  279. prefLayout = QSplitter(Qt.Horizontal)
  280. workspace.xLimitValue = QLabel(
  281. "X-axis range: (-" + str(workspace.axisRange[0]) + ", " + str(workspace.axisRange[0]) + ")")
  282. workspace.yLimitValue = QLabel(
  283. "Y-axis range: (-" + str(workspace.axisRange[1]) + ", " + str(workspace.axisRange[1]) + ")")
  284. workspace.zLimitValue = QLabel(
  285. "Z-axis range: (-" + str(workspace.axisRange[2]) + ", " + str(workspace.axisRange[2]) + ")")
  286. def customSlider():
  287. limitSlider = QSlider(Qt.Horizontal)
  288. limitSlider.setMinimum(-3)
  289. limitSlider.setMaximum(3)
  290. limitSlider.setValue(1)
  291. limitSlider.setTickPosition(QSlider.TicksBothSides)
  292. limitSlider.setTickInterval(1)
  293. limitSlider.valueChanged.connect(lambda: valueChange(workspace))
  294. limitSlider.setStatusTip("Change axes limit")
  295. return limitSlider
  296. workspace.xLimitSlider = customSlider()
  297. workspace.yLimitSlider = customSlider()
  298. workspace.zLimitSlider = customSlider()
  299. workspace.meshDensityValue = QLabel(
  300. "Mesh Layers: " + str(workspace.axisRange[3]))
  301. workspace.meshDensityValue.setStatusTip("Increment for a denser mesh in 3D plot")
  302. workspace.meshDensity = QSpinBox()
  303. workspace.meshDensity.setFixedSize(200, 30)
  304. workspace.meshDensity.setRange(10, 75)
  305. workspace.meshDensity.setValue(30)
  306. workspace.meshDensity.valueChanged.connect(lambda: valueChange(workspace))
  307. workspace.meshDensity.setStatusTip("Incrementing mesh density may affect performance")
  308. refreshPlotterText = QLabel("Apply plotter settings")
  309. refreshPlotter = QPushButton('Apply')
  310. refreshPlotter.setFixedSize(200, 30)
  311. refreshPlotter.clicked.connect(lambda: refreshPlot(workspace))
  312. refreshPlotter.setStatusTip("Apply modified settings to plotter.")
  313. axisPref = QSplitter(Qt.Vertical)
  314. axisPref.addWidget(workspace.xLimitValue)
  315. axisPref.addWidget(workspace.xLimitSlider)
  316. axisPref.addWidget(workspace.yLimitValue)
  317. axisPref.addWidget(workspace.yLimitSlider)
  318. axisPref.addWidget(workspace.zLimitValue)
  319. axisPref.addWidget(workspace.zLimitSlider)
  320. plotSetPref = QSplitter(Qt.Vertical)
  321. plotSetPref.addWidget(workspace.meshDensityValue)
  322. plotSetPref.addWidget(workspace.meshDensity)
  323. plotSetPref.addWidget(refreshPlotterText)
  324. plotSetPref.addWidget(refreshPlotter)
  325. prefLayout.addWidget(plotSetPref)
  326. prefLayout.addWidget(axisPref)
  327. prefLayout.setFixedWidth(400)
  328. return prefLayout
  329. def valueChange(workspace):
  330. xlimit = 10**workspace.xLimitSlider.value()
  331. ylimit = 10**workspace.yLimitSlider.value()
  332. zlimit = 10**workspace.zLimitSlider.value()
  333. meshLayers = workspace.meshDensity.value()
  334. workspace.axisRange = [xlimit, ylimit, zlimit, meshLayers]
  335. workspace.xLimitValue.setText(
  336. "X-axis range: (-" + str(workspace.axisRange[0]) + ", " + str(workspace.axisRange[0]) + ")")
  337. workspace.yLimitValue.setText(
  338. "Y-axis range: (-" + str(workspace.axisRange[1]) + ", " + str(workspace.axisRange[1]) + ")")
  339. workspace.zLimitValue.setText(
  340. "Z-axis range: (-" + str(workspace.axisRange[2]) + ", " + str(workspace.axisRange[2]) + ")")
  341. workspace.meshDensityValue.setText(
  342. "Mesh Layers: " + str(workspace.axisRange[3]))