Source code for idaes.ui.flowsheet

##############################################################################
# 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()