##############################################################################
# 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".
##############################################################################
"""
A simple GUI viewer for Pyomo models.
"""
from __future__ import division
from __future__ import print_function
from __future__ import absolute_import #disable implicit relative imports
__author__ = "John Eslick"
__version__ = "1.0.0"
import time
import os
import warnings
import logging
import threading
import datetime
import jupyter_client
import json
from IPython import get_ipython
import idaes.ui.report as rpt
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 PyQt4.QtGui import QFileDialog, QMessageBox
from PyQt4 import uic
except:
logging.exception("Cannot import PyQt4")
else:
try:
from PyQt5.QtWidgets import QFileDialog, QMessageBox
from PyQt5 import uic
except:
logging.exception("Cannot import PyQt5")
from idaes.ui.model_browser import ModelBrowser
from idaes.ui.flowsheet import DrawFlowsheet
from idaes.ui.util import insert_jupyter_code, \
toggle_jupyter_header, close_page, save_notebook, show_jupyter_header, \
kernel_interupt, cut_cell, run_cells_below, add_cell, move_cell_up, \
move_cell_down, kernel_execute
_mypath = os.path.dirname(__file__)
try:
_MainWindowUI, _MainWindow = \
uic.loadUiType(os.path.join(_mypath, "main.ui"))
except:
logging.exception("Failed to load UI files.")
# This lets the file still be imported, but you won't be able to use it
class _MainWindowUI(object):
pass
class _MainWindow(object):
pass
[docs]def get_mainwindow_nb(model=None, model_str="model", show=True):
"""
Create a UI MainWindow to be used with a Jupyter notebook.
Args:
model: A Pyomo model to work with
model_str: A string that is the model variable in the notebook
show: show the window after it is created
"""
ui = MainWindow(model=model, model_str=model_str)
get_ipython().events.register('post_execute', ui.refresh_on_execute)
if show: ui.show()
return ui
[docs]def setup_environment(kexec=True):
"""
Setup the standard environment
"""
lines = [
"import logging",
"import pandas as pd",
"from IPython.display import display, Markdown",
"import pyomo.environ as pe",
"from pyomo.environ import SolverFactory, TransformationFactory",
"from idaes.core.util.model_serializer import from_json, to_json",
"import idaes.core.util.model_serializer as model_serial",
"from idaes.ui.ui import get_mainwindow_nb",
"import idaes.ui.report as rpt",
"from idaes.core.util import make_stream_table",
"%gui qt"]
if kexec: # keeping this for now so I don't break anyone else's stuff
for l in lines:
kernel_execute(l)
else:
del lines[-1]
lines = "\n".join(lines)
print(lines)
return(lines)
[docs]class UISetup(QtCore.QObject):
updated = QtCore.pyqtSignal()
def __init__(self, model=None, model_str="model"):
"""
This class holds the basic UI setup
Args:
model: The Pyomo model to view
model_str: The model string to use when writing commands
"""
super(UISetup, self).__init__()
self._model = None
self._model_str = None
self._begin_update = False
self.begin_update()
self.model = model
self.model_str = model_str
self.end_update()
[docs] def begin_update(self):
"""
Lets the model setup be changed without emitting the updated signal
until the end_update function is called.
"""
self._begin_update = True
[docs] def end_update(self, noemit=False):
"""
Start automatically emitting update signal again when properties are
changed and emit update for changes made between begin_update and
end_update
"""
self._begin_update = False
if not noemit: self.emit_update()
[docs] def emit_update(self):
if not self._begin_update: self.updated.emit()
@property
def model(self):
return self._model
@model.setter
def model(self, value):
self._model = value
self.emit_update()
@property
def model_str(self):
return self._model_str
@model_str.setter
def model_str(self, value):
self._model_str = value
self.emit_update()
[docs]class MainWindow(_MainWindow, _MainWindowUI):
def __init__(self, *args, **kwargs):
model = self.model = kwargs.pop("model", None)
model_str = self.model = kwargs.pop("model_str", "model")
flags = kwargs.pop("flags", 0)
if kwargs.pop("ontop", False):
kwargs[flags] = flags | QtCore.Qt.WindowStaysOnTopHint
self.ui_setup = UISetup(model=model, model_str=model_str)
self.ui_setup.updated.connect(self.update_model)
super(MainWindow, self).__init__(*args, **kwargs)
self.setupUi(self)
# Create model browsers these are dock widgets
uis = self.ui_setup
self.vars = ModelBrowser(parent=self, standard="Var", ui_setup=uis)
self.cons = ModelBrowser(parent=self, standard="Constraint", ui_setup=uis)
self.params = ModelBrowser(parent=self, standard="Param", ui_setup=uis)
self.exprs = ModelBrowser(parent=self, standard="Expression", ui_setup=uis)
# Dock the wigetes allong the bottom and tabify them
self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.vars)
self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.cons)
self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.params)
self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.exprs)
self.tabifyDockWidget(self.vars, self.params)
self.tabifyDockWidget(self.vars, self.exprs)
self.tabifyDockWidget(self.vars, self.cons)
self.vars.raise_()
self.flowsheet = DrawFlowsheet(parent=self, ui_setup=uis)
self.layout_tab_0.addWidget(self.flowsheet)
# Set menu actions (rembeber the menu items are defined in the ui file)
# you can edit the menus in qt-designer
self.actionSave_Model_State.triggered.connect(self.save_state_action)
self.actionLoad_Model_State.triggered.connect(self.load_state_action)
self.action_Save.triggered.connect(save_notebook)
self.action_Exit.triggered.connect(self.exit_action)
self.action_toggle_variables.triggered.connect(self.vars.toggle)
self.action_toggle_constraints.triggered.connect(self.cons.toggle)
self.action_toggle_parameters.triggered.connect(self.params.toggle)
self.action_toggle_expressions.triggered.connect(self.exprs.toggle)
self.action_toggle_jupyter_header.triggered.connect(toggle_jupyter_header)
self.actionToggle_Always_on_Top.triggered.connect(self.toggle_always_on_top)
self.actionInformation.triggered.connect(self.model_information)
self.actionCalculateConstraints.triggered.connect(self.cons.calculate_all)
self.actionCalculateExpressions.triggered.connect(self.exprs.calculate_all)
[docs] def update_model(self):
pass
[docs] def model_information(self):
"""
Put some useful model information into a message box
Displays:
* number of active equality constraints
* number of free variables in equality constraints
* degrees of freedom
Other things that could be added
* number of deactivated equalities
* number of active inequlaity constraints
* number of deactivated inequality constratins
* number of free variables not appearing in active constraints
* number of fixed variables not appearing in active constraints
* number of free variables not appearing in any constraints
* number of fixed variables not appearing in any constraints
* number of fixed variables appearing in constraints
"""
active_eq = rpt.count_equality_constraints(self.ui_setup.model)
free_vars = rpt.count_free_variables(self.ui_setup.model)
dof = free_vars - active_eq
if dof == 1:
doftext = "Degree"
else:
doftext = "Degrees"
QMessageBox.information(
self,
"Model Information",
"{} -- Active equalities\n"
"{} -- Free variables in active equalities\n"
"{} -- {} of freedom\n"\
.format(active_eq, free_vars, dof, doftext))
[docs] def toggle_always_on_top(self):
"""
This toggles the always on top hint. Weather this has any effect after
the main window is created probably depends on the system.
"""
self.setWindowFlags(window.windowFlags() ^ QtCore.Qt.WindowStaysOnTopHint)
[docs] def refresh_on_execute(self):
"""
This is the call back function that happens when code is executed in the
ipython kernel. The main purpose of this right now it to refresh the
UI display so that it matches the current state of the model.
"""
self.vars.refresh()
self.cons.refresh()
self.exprs.refresh()
self.params.refresh()
[docs] def save_state_action(self):
"""
File dialog to save the model state to json file
"""
f = QFileDialog.getSaveFileName(self, "Save model state", "", "*.json;; *.*")
if f and len(f[0]) > 0:
if f[0].endswith(f[1][1:]):
f = f[0]
else:
f = "".join([f[0], f[1][1:]])
insert_jupyter_code(code='tmp_json = to_json({}, fname="{}")'\
.format(self.ui_setup.model_str, f))
[docs] def load_state_action(self):
"""
File dialog to load the model state from json file
"""
f = QFileDialog.getOpenFileName(
self, "Load model state", "", "*.json;; *.*")
if f and len(f[0]) > 0:
f = f[0]
insert_jupyter_code(code='from_json({}, fname="{}")'\
.format(self.ui_setup.model_str, f))
[docs] def exit_action(self):
"""
Selecting exit from the UI, triggers the close event on this mainwindow
"""
self.close()
[docs] def closeEvent(self, event):
"""
Handel the colse event by asking for confirmation
"""
result = QMessageBox.question(self,
"Exit?",
"Are you sure you want to exit ?",
QMessageBox.Yes| QMessageBox.No)
if result == QMessageBox.Yes:
event.accept()
else:
event.ignore()