##############################################################################
# Institute for the Design of Advanced Energy Systems Process Systems
# Engineering Framework (IDAES PSE Framework) Copyright (c) 2018, by the
# software owners: The Regents of the University of California, through
# Lawrence Berkeley National Laboratory, National Technology & Engineering
# Solutions of Sandia, LLC, Carnegie Mellon University, West Virginia
# University Research Corporation, et al. All rights reserved.
#
# Please see the files COPYRIGHT.txt and LICENSE.txt for full copyright and
# license information, respectively. Both files are also available online
# at the URL "https://github.com/IDAES/idaes".
##############################################################################
"""
DrawFlowsheet.py
* Widget to display/edit the flowsheet
John Eslick, 2017
"""
from __future__ import division
from __future__ import print_function
from __future__ import absolute_import # disable implicit relative imports
import math
import types
import six
import logging
try:
from PyQt5 import QtCore
except:
logging.exception("Cannot import PyQt5.QtCore")
try:
from PyQt4 import QtCore
except:
logging.exception("Cannot import PyQt4.QtCore")
else:
try:
from PyQt5.QtGui import QColor, QFont, QPen, QBrush, QPainter,\
QPainterPath, QPainterPathStroker, QGraphicsScene, QGraphicsView
except:
logging.exception("Cannot import PyQt4")
else:
try:
from PyQt5.QtGui import QColor, QFont, QPen, QBrush, QPainter,\
QPainterPath, QPainterPathStroker
from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView
except:
logging.exception("Cannot import PyQt5")
from pyomo.environ import *
from idaes.core import Stream
import idaes.ui.report as rpt
[docs]class FlowsheetScene(QGraphicsScene):
"""
QGraphicsScene class for viewing and editing a Pyomo model block stucture
"""
# Mouse Modes
MODE_SELECT = 1
MODE_ADDNODE = 2
MODE_ADDEDGE = 3
# Item types
ITEM_NONE = 0
ITEM_NODE = 1
ITEM_EDGE = 2
def __init__(self, parent=None, ui_setup=None):
"""
Initialize the flowsheet scene
"""
super(FlowsheetScene, self).__init__(parent)
# Location of mouse events and whether the mouse button is down
self.press_x = 0.0
self.press_y = 0.0
# A lot of settings, these things are just constant now, but this
# makes it easy to modify the look or add user options later
# Initial mode/selections
self.mode = self.MODE_SELECT # Mouse mode
self.p = parent # Parent object
self.node1 = None # Node 1 for drawing edges
self.node2 = None # Node 2 for drawing edges
self.selectedNodes = [] # Set of selected nodes
self.selectedEdges = [] # Set of selected edges
# Grid settings
self.grid_minor_step = 20 # Minor grid spacing
self.grid_major_step = 100 # Major grid spacing
self.grid_max_x = 2000 # Extent of grid area up x
self.grid_min_x = -2000 # Extent of grid area lo x
self.grid_max_y = 2000 # Extent of grid area up y
self.grid_min_y = -2000 # Extent of grid area lo y
# Node/edge appearance
self.node_size = 20 # size of node
self.arrow_size = 16 # size of the edge arrows
self.show_direction = True # draw connection direction arrows?
# Font Settings
self.font = QFont()
self.font_size = 12
self.load_font()
# Pen settings
self.pens = {}
self.pens["major_grid"] = {"pen":QPen(),
"lc":QColor(150,200,255), "lt":QtCore.Qt.DashLine, "lw":1}
self.pens["minor_grid"] = {"pen":QPen(),
"lc":QColor(190,240,255), "lt":QtCore.Qt.DashLine, "lw":1}
self.pens["edge"] = {"pen":QPen(),
"lc":QColor(0,50,200), "lt":QtCore.Qt.SolidLine, "lw":2}
self.pens["tear_edge"] = {"pen":QPen(),
"lc":QColor(100,200,255), "lt":QtCore.Qt.SolidLine, "lw":2}
self.pens["node"] = {"pen":QPen(),
"lc":QColor(0,0,0), "lt":QtCore.Qt.SolidLine, "lw":2}
self.pens["selection"] = {"pen":QPen(),
"lc":QColor(0,255,0), "lt":QtCore.Qt.SolidLine, "lw":2}
self.pens["edge_selection"] = {"pen":QPen(),
"lc":QColor(0,255,0), "lt":QtCore.Qt.SolidLine, "lw":10}
self.load_pens()
# Brush settings
self.brushes = {}
self.brushes["node"] = {"brush":QBrush(),
"fc":QColor(128,128,128), "fp":QtCore.Qt.SolidPattern}
self.brushes["active_node"] = {"brush":QBrush(),
"fc":QColor(128,250,128), "fp":QtCore.Qt.SolidPattern}
self.brushes["arrow"] = {"brush":QBrush(),
"fc":QColor(0,50,200), "fp":QtCore.Qt.SolidPattern}
self.brushes["white"] = {"brush":QBrush(),
"fc":QColor(240,240,240), "fp":QtCore.Qt.SolidPattern}
self.load_brushes()
# PyQt ui setup
self.ui_setup = ui_setup
[docs] def load_font(self):
"""
Put the font settings into the font used to draw scene
"""
self.font.setPixelSize(self.font_size)
[docs] def load_pens(self):
"""
Put the settings from the pen dict into the pens used to draw scene
"""
for key, d in six.iteritems(self.pens):
d["pen"].setColor(d["lc"])
d["pen"].setStyle(d["lt"])
d["pen"].setWidth(d["lw"])
[docs] def load_brushes(self):
"""
Put the settings from the brush dict into the brushes used to draw scene
"""
for key, d in six.iteritems(self.brushes):
d["brush"].setColor(d["fc"])
d["brush"].setStyle(d["fp"])
[docs] def draw_grid(self):
"""
Draw the grid for the drawing area
"""
# Add vertical minor grids
path = QPainterPath()
for i in range(self.grid_min_x, self.grid_max_x, self.grid_minor_step):
path.moveTo(i, self.grid_min_y)
path.lineTo(i, self.grid_max_y)
self.addPath(path, self.pens["minor_grid"]["pen"])
# Add horizontal minor grids
path = QPainterPath()
for i in range(self.grid_min_y, self.grid_max_y, self.grid_minor_step):
path.moveTo(self.grid_min_x, i)
path.lineTo(self.grid_max_x, i)
self.addPath(path, self.pens["minor_grid"]["pen"])
# Add vertical major grids
path = QPainterPath()
for i in range(self.grid_min_x, self.grid_max_x, self.grid_major_step):
path.moveTo(i, self.grid_min_y)
path.lineTo(i, self.grid_max_y)
self.addPath(path, self.pens["major_grid"]["pen"])
# Add horizontal major grids
path = QPainterPath()
for i in range(self.grid_min_y, self.grid_max_y, self.grid_major_step):
path.moveTo(self.grid_min_x, i)
path.lineTo(self.grid_max_x, i)
self.addPath(path, self.pens["major_grid"]["pen"])
[docs] def addTextCenteredOn(self, x=0, y=0, text="None"):
"""
Add text vertically and horizontally centered on (x, y)
Args:
x: x coordinate of text center
y: y coordinate of text center
text: text to draw
"""
text = self.addText(text, self.font)
text.setPos(x - text.boundingRect().width()/2.0,
y - text.boundingRect().height()/2.0)
return text
[docs] def draw_node(self, x, y, o=None):
"""
Draw a node centered at x,y. Text lines are centered under the node for
the node name and node type
Args:
x: x coordinate of node
y: y coordinate of node
o: subclass of Pyomo Block
"""
nsz = self.node_size
fsz = self.font_size
if o in self.selectedNodes: # plain or selected pen
pen = self.pens["selection"]["pen"]
else:
pen = self.pens["node"]["pen"]
# Path for a square centered on (x, y)
path = QPainterPath()
path.addRect(x - nsz/2.0, y - nsz/2.0, nsz, nsz)
# Add path to scene
item = self.addPath(path, pen, self.brushes["node"]["brush"])
item.setData(1, o) # Pyomo block object
item.setData(2, "node") # This scene item is a node
#Draw text labels
text = self.addTextCenteredOn(x, y + nsz/2 + fsz/2 + 4, o.name)
text.setData(1, o) # Pyomo block object
text.setData(2, "node") # This scene item is a node
"""
# This text is the type of block under the name of the block
text = self.addTextCenteredOn(x, y + nsz/2 + fsz + 16, str(o.__class__))
text.setData(1, o) # Pyomo block object
text.setData(2, "node") # This scene item is a node
"""
return
[docs] def drawEdge(self, x1, y1, x2, y2, index, curve=0):
"""
Draw an edge from x1, y1 to x2, y2. (should connect two blocks)
Args:
index: the edge index
curve: positive or negitive bow in edge (to prevent overlap)
tear: if True draw in tear edge style
"""
if abs(x1 - x2) < 0.01 and abs(y1 - y2) < 0.01:
# Edge connecting a node to itself, or two nodes on top of eachother
path = QPainterPath()
curve = 40
path.addEllipse(x1, y1 - curve/2.0, curve, curve)
gi = self.addPath(path, self.pens["edge"]["pen"])
else:
# mid point of the edge if it is a straight line
xmid = (x1+x2)/2.0
ymid = (y1+y2)/2.0
# get the angle of the edge and the angle perpendicular
ang = math.atan2((y2 - y1),(x2 - x1))
ang_perp = math.atan2((x1 - x2),(y2 - y1))
# calculate the mid point of the curved edge
xcurve = xmid+curve*math.cos(ang_perp)
ycurve = ymid+curve*math.sin(ang_perp)
# calculate control point for drawing quaratic curve
xcontrol = 2*xcurve - xmid
ycontrol = 2*ycurve - ymid
#draw Edge
path = QPainterPath()
path.moveTo(x1,y1)
path.quadTo(xcontrol, ycontrol, x2, y2)
p2 = QPainterPathStroker()
path = p2.createStroke(path)
# if edge is selected draw it highlighted
if index in self.selectedEdges:
self.addPath(path, self.pens["edge_selection"]["pen"])
gi = self.addPath(path, self.pens["edge"]["pen"])
gi.setData(1, index)
gi.setData(2, "edge")
if self.show_direction: # Draw the arrow if desired
path = QPainterPath()
xs = xcurve + self.arrow_size*math.cos(ang)
ys = ycurve + self.arrow_size*math.sin(ang)
path.moveTo(xs,ys)
path.lineTo(
xs - self.arrow_size*math.cos(ang) +\
self.arrow_size/2.0*math.cos(ang_perp),
ys - self.arrow_size*math.sin(ang) +\
self.arrow_size/2.0*math.sin(ang_perp))
path.lineTo(
xs - self.arrow_size*math.cos(ang) -\
self.arrow_size/2.0*math.cos(ang_perp),
ys - self.arrow_size*math.sin(ang) -\
self.arrow_size/2.0*math.sin(ang_perp))
path.lineTo(xs, ys)
gi = self.addPath(path, self.pens["edge"]["pen"],
self.brushes["arrow"]["brush"])
# Add data so selecting the arrow in like selecting the edge
gi.setData(1, index)
gi.setData(2, "edge")
return
[docs] def nearestGrid(self,x,y):
"""
Find the nearest minor grid to a point.
Args:
x: x coord of point to find nearest grid intersection to
y: y coord of point to find nearest grid intersection to
"""
xg = round(x/self.grid_minor_step)*self.grid_minor_step
yg = round(y/self.grid_minor_step)*self.grid_minor_step
return xg, yg
[docs] def deleteSelected(self):
"""
Delete the selected nodes and edges then redraw the flowsheet
"""
# will come back to this
self.p.createScene() #
[docs] def mouseMoveEvent(self, evnt):
"""
Mouse move event handler. If mouse button is down move selected nodes
Args:
evnt: event
"""
# if mouse button is down check if you want to move nodes
if not evnt.buttons() == QtCore.Qt.LeftButton:
return # only move with left button
if self.mode != self.MODE_SELECT:
return # only move in select mode
dx = evnt.scenePos().x() - self.press_x # change in position from orig
dy = evnt.scenePos().y() - self.press_y
for i, node in enumerate(self.selectedNodes): # snap new node location
pos = node.parent_block().dispos[node]
pos["x"], pos["y"] = \
self.nearestGrid(self.ipos[i][0] + dx, self.ipos[i][1] + dy)
self.p.createScene() # update the scene
[docs] def mousePressEvent(self, evnt, dbg_evnt={}):
"""
This handles a mouse press event on the flowsheet scene. The dbg_
arguments allow you to simulate a mouse press event. This could be
handy for testing and scripting.
Args:
evnt: Qt mouse press event
dbg_evnt: simulated press information dict keys: x, y, mod...
"""
# Get the location of the mouse event
if evnt is None:
# this lets you create a mouse event at a location
# for testing
mod = dbg_evnt.get("mod", None)
x = dbg_evnt["x"]
y = dbg_evnt["y"]
else:
# This is an acctual mouse event
mod = evnt.modifiers()
x = evnt.scenePos().x()
y = evnt.scenePos().y()
# Keep the location of the event around for other methods to use
# like move, reclengle select...
self.press_x = x
self.press_y = y
# Check if there is an object that was clicked on
item = self.itemAt(x, y, self.parent().transform())
if item is not None:
item_object = item.data(1) # this would be a pyomo component
item_type = item.data(2) # type: "node", "edge" ...
else:
item_object = None
item_type = None
if self.mode == self.MODE_SELECT:
# Selection mode to select a node or edge. Shift-key mod allows
# selecting multiple. Clicking empty space without the Shift-key
# will clear the selection.
if mod != QtCore.Qt.ShiftModifier: # select only one
self.selectedEdges = []
self.selectedNodes = []
if item_type == "edge":
self.selectedEdges.append(item_object)
elif item_type == "node":
self.selectedNodes.append(item_object)
elif mod != QtCore.Qt.ShiftModifier:
pass
self.p.createScene() # redraw scene (need to update selection marks)
elif self.mode == self.MODE_ADDNODE:
# Mouse press in add mode mode
pass
elif self.mode == self.MODE_ADDEDGE:
# Mouse press in add edge mode
pass
# original locations of selected nodes for moving
self.ipos = [(0,0)]*len(self.selectedNodes)
for i, node in enumerate(self.selectedNodes):
pos = node.parent_block().dispos[node]
self.ipos[i] = (pos["x"], pos["y"])
[docs]class DrawFlowsheet(QGraphicsView):
"""
This is the a QGraphicsView widget for viewing and editing a flowsheet
most of the important stuff is in the accosicated scene object
"""
def __init__(self, parent=None, ui_setup=None):
"""
Initialize
Args:
parent: parent QtWidget
ui_setup: settings for UI contains Pyomo model, ...
"""
super(DrawFlowsheet, self).__init__(parent)
# create and set scene object
self.sc = FlowsheetScene(self, ui_setup=ui_setup)
self.setScene(self.sc)
self.setRenderHint(QPainter.Antialiasing)
self.ui_setup = ui_setup
# draw the flowsheet scene
self.createScene()
[docs] def createScene(self):
"""
Put items in the scene, these items have refences back to the original
Pyomo components for editing and displaying and such
"""
self.scene().clear()
self.scene().draw_grid()
# Add suffix to store block positions if it doesn't exist
model = self.ui_setup.model
if not hasattr(model, "dispos"):
model.dispos = Suffix(direction=Suffix.LOCAL)
pos = model.dispos
# Add geometry for blocks if they don't exist
for o in model.component_objects(Block, descend_into=False):
if not o in pos:
pos[o] = {"x":0, "y":0}
# Draw connections first so they are under nodes
cb = rpt.connections(model)
for i, c in enumerate(cb):
if c[2] == 0: # connections in one direction only
self.scene().drawEdge(pos[c[0]]["x"], pos[c[0]]["y"],
pos[c[1]]["x"], pos[c[1]]["y"], i)
else: # connections in both directions
self.scene().drawEdge(pos[c[0]]["x"], pos[c[0]]["y"],
pos[c[1]]["x"], pos[c[1]]["y"], i, 20)
self.scene().drawEdge(pos[c[1]]["x"], pos[c[1]]["y"],
pos[c[0]]["x"], pos[c[0]]["y"], i, 20)
# Draw nodes
for o in model.component_objects(Block, descend_into=False):
if not isinstance(o, Stream):
self.scene().draw_node(pos[o]["x"], pos[o]["y"], o=o)
self.scene().update()
[docs] def highlightSingleNode(self, o):
self.scene().selectedNodes = [o]
self.scene().selectedEdges = []
self.createScene()
[docs] def clearSelection(self):
self.scene().selectedNodes = []
self.scene().selectedEdges = []
[docs] def center(self):
"""
Center the view on the center of the flowsheet
"""
pass
#cp = self.dat.flowsheet.getCenter()
#self.centerOn(cp[0], cp[1])
[docs] def set_mouse_mode(self, mode):
"""
Set the mouse mode to selection
Args:
mode: The mouse mode for the scene in [FlowsheetScene.MODE_SELECT,
FlowsheetScene.MODE_ADDNODE, FlowsheetScene.MODE_ADDEDGE]
"""
self.scene().mode = mode
[docs] def delete_selected(self):
"""
Delete selected nodes and edges, also deletes any edges
connected to a deleted node.
"""
self.scene().deleteSelected()