1##############################################################################
2# Institute for the Design of Advanced Energy Systems Process Systems
3# Engineering Framework (IDAES PSE Framework) Copyright (c) 2018-2019, by the
4# software owners: The Regents of the University of California, through
5# Lawrence Berkeley National Laboratory,  National Technology & Engineering
6# Solutions of Sandia, LLC, Carnegie Mellon University, West Virginia
7# University Research Corporation, et al. All rights reserved.
8#
9# This software is distributed under the 3-clause BSD License.
10##############################################################################
11"""
12A simple GUI viewer/editor for Pyomo models.
13"""
14from __future__ import division, print_function, absolute_import
15
16__author__ = "John Eslick"
17
18import os
19import logging
20
21_log = logging.getLogger(__name__)
22
23from pyomo.contrib.viewer.qt import *
24from pyomo.contrib.viewer.report import value_no_exception, get_residual
25
26from pyomo.core.base.block import _BlockData
27from pyomo.core.base.var import _VarData
28from pyomo.core.base.constraint import _ConstraintData
29from pyomo.core.base.param import _ParamData
30from pyomo.environ import Block, Var, Constraint, Param, Expression, value
31
32mypath = os.path.dirname(__file__)
33try:
34    _ModelBrowserUI, _ModelBrowser = \
35        uic.loadUiType(os.path.join(mypath, "model_browser.ui"))
36except:
37    # This lets the file still be imported, but you won't be able to use it
38    class _ModelBrowserUI(object):
39        pass
40    class _ModelBrowser(object):
41        pass
42    class QItemEditorCreatorBase(object):
43        pass
44    class QItemDelegate(object):
45        pass
46
47class LineEditCreator(QItemEditorCreatorBase):
48    """
49    Class to create editor widget for int and floats in a model view type object
50    """
51    def createWidget(self, parent):
52        return QLineEdit(parent=parent)
53
54
55class NumberDelegate(QItemDelegate):
56    """
57    Tree view item delegate. This is used here to change how items are edited.
58    """
59    def __init__(self, parent):
60        super(QItemDelegate, self).__init__(parent=parent)
61        factory = QItemEditorFactory()
62        factory.registerEditor(QtCore.QVariant.Int, LineEditCreator())
63        factory.registerEditor(QtCore.QVariant.Double, LineEditCreator())
64        self.setItemEditorFactory(factory)
65
66    def setModelData(self, editor, model, index):
67        if isinstance(editor, QComboBox):
68            value = editor.currentText()
69        else:
70            value = editor.text()
71        a = model.column[index.column()]
72        isinstance(index.internalPointer().get(a), bool)
73        try: # Recognize ints and floats.
74            if value == "False" or value == "false":
75                index.internalPointer().set(a, False)
76            elif value == "True" or value == "true":
77                index.internalPointer().set(a, True)
78            elif "." in value or "e" in value or "E" in value:
79                index.internalPointer().set(a, float(value))
80            else:
81                index.internalPointer().set(a, int(value))
82        except: # If not a valid number ignore
83            pass
84
85
86class ModelBrowser(_ModelBrowser, _ModelBrowserUI):
87    def __init__(self, ui_data, parent=None, standard="Var"):
88        """
89        Create a dock widdget with a QTreeView of a Pyomo model.
90
91        Args:
92            parent: parent widget
93            ui_data: Contains model and ui information
94            standard: A standard setup for differnt types of model components
95                {"Var", "Constraint", "Param", "Expression"}
96        """
97        super(ModelBrowser, self).__init__(parent=parent)
98        self.setupUi(self)
99        # The default int and double spin boxes are not good for this
100        # application.  So just use regular line edits.
101        number_delegate = NumberDelegate(self)
102        self.ui_data = ui_data
103        self.ui_data.updated.connect(self.update_model)
104        self.treeView.setItemDelegate(number_delegate)
105        if standard == "Var":
106            # This if block sets up standard views
107            components = Var
108            columns =  ["name", "value", "ub", "lb", "fixed", "stale"]
109            editable = ["value", "ub", "lb", "fixed"]
110            self.setWindowTitle("Variables")
111        elif standard == "Constraint":
112            components = Constraint
113            columns = ["name", "value", "ub", "lb", "residual", "active"]
114            editable = ["active"]
115            self.setWindowTitle("Constraints")
116        elif standard == "Param":
117            components = Param
118            columns = ["name", "value", "mutable"]
119            editable = ["value"]
120            self.setWindowTitle("Parameters")
121        elif standard == "Expression":
122            components = Expression
123            columns = ["name", "value"]
124            editable = []
125            self.setWindowTitle("Expressions")
126        else:
127            raise ValueError("{} is not a valid view type".format(standard))
128        # Create a data model.  This is what translates the Pyomo model into
129        # a tree view.
130        datmodel = ComponentDataModel(self, ui_data=ui_data,
131                                      columns=columns, components=components,
132                                      editable=editable)
133        self.datmodel = datmodel
134        self.treeView.setModel(datmodel)
135        self.treeView.setColumnWidth(0,400)
136        # Selection behavior: select a whole row, can select multiple rows.
137        self.treeView.setSelectionBehavior(QAbstractItemView.SelectRows)
138        self.treeView.setSelectionMode(QAbstractItemView.ExtendedSelection)
139
140    def refresh(self):
141        added = self.datmodel._update_tree()
142        self.datmodel.layoutChanged.emit()
143
144    def update_model(self):
145        self.datmodel.update_model()
146
147
148class ComponentDataItem(object):
149    """
150    This is a container for a Pyomo component to be displayed in a model tree
151    view.
152
153    Args:
154        parent: parent data item
155        o: pyomo component object
156        ui_data: a container for data, as of now mainly just pyomo model
157    """
158    def __init__(self, parent, o, ui_data):
159        self.ui_data = ui_data
160        self.data = o
161        self.parent = parent
162        self.children = [] # child items
163        self.ids = {}
164        self.get_callback = {
165            "value": self._get_value_callback,
166            "lb": self._get_lb_callback,
167            "ub": self._get_ub_callback,
168            "expr": self._get_expr_callback,
169            "residual": self._get_residual_callback}
170        self.set_callback = {
171            "value":self._set_value_callback,
172            "lb":self._set_lb_callback,
173            "ub":self._set_ub_callback,
174            "active":self._set_active_callback,
175            "fixed":self._set_fixed_callback}
176
177    @property
178    def _cache_value(self):
179        return self.ui_data.value_cache.get(self.data, None)
180
181    def add_child(self, o):
182        """Add a child data item"""
183        item = ComponentDataItem(self, o, ui_data=self.ui_data)
184        self.children.append(item)
185        self.ids[id(o)] = item
186        return item
187
188    def get(self, a):
189        """Get an attribute"""
190        if a in self.get_callback:
191            return self.get_callback[a]()
192        else:
193            try:
194                return getattr(self.data, a)
195            except:
196                return None
197
198    def set(self, a, val):
199        """set an attribute"""
200        if a in self.set_callback:
201            return self.set_callback[a](val)
202        else:
203            try:
204                return setattr(self.data, a, val)
205            except:
206                _log.exception("Can't set value of {}".format(a))
207                return None
208
209    def _get_expr_callback(self):
210        if hasattr(self.data, "expr"):
211            return str(self.data.expr)
212        else:
213            return None
214
215    def _get_value_callback(self):
216        if isinstance(self.data, _ParamData):
217            v = value(self.data)
218            # Check the param value for numpy float and int, sometimes numpy
219            # values can sneak in especially if you set parameters from data
220            # and for whatever reason numpy values don't display
221            if isinstance(v, float): # includes numpy float
222                v = float(v)
223            elif isinstance(v, int): # includes numpy int
224                v = int(v)
225            return v
226        elif isinstance(self.data, _VarData):
227            return value(self.data, exception=False)
228        elif isinstance(self.data, (float, int)):
229            return self.data
230        else:
231            return self._cache_value
232
233    def _get_lb_callback(self):
234        if isinstance(self.data, _VarData):
235            return self.data.lb
236        elif hasattr(self.data, "lower"):
237            return value_no_exception(self.data.lower, div0="Divide_by_0")
238        else:
239            return None
240
241    def _get_ub_callback(self):
242        if isinstance(self.data, _VarData):
243            return self.data.ub
244        elif hasattr(self.data, "upper"):
245            return value_no_exception(self.data.upper, div0="Divide_by_0")
246        else:
247            return None
248
249    def _get_residual_callback(self):
250        if isinstance(self.data, _ConstraintData):
251            return get_residual(self.ui_data, self.data)
252        else:
253            return None
254
255    def _set_value_callback(self, val):
256        if isinstance(self.data, _VarData):
257            try:
258                self.data.value = val
259            except:
260                return
261        elif isinstance(self.data, _ParamData):
262            if not self.data.parent_component().mutable: return
263            try:
264                self.data.value = val
265            except:
266                return
267
268    def _set_lb_callback(self, val):
269        if isinstance(self.data, _VarData):
270            try:
271                self.data.setlb(val)
272            except:
273                return
274
275    def _set_ub_callback(self, val):
276        if isinstance(self.data, _VarData):
277            try:
278                self.data.setub(val)
279            except:
280                return
281
282    def _set_active_callback(self, val):
283        if not val or val == "False" or val == "false" or val == "0" or \
284            val == "f" or val == "F":
285            # depending on the version of Qt, you may see a combo box that
286            # lets you select true/false or may be able to type the combo
287            # box will return True or False, if you have to type could be
288            # something else
289            val = False
290        else:
291            val = True
292        try:
293            if val:
294                self.data.activate()
295            else:
296                self.data.deactivate()
297        except:
298            return
299
300    def _set_fixed_callback(self, val):
301        if not val or val == "False" or val == "false" or val == "0" or \
302            val == "f" or val == "F":
303            # depending on the version of Qt, you may see a combo box that
304            # lets you select true/false or may be able to type the combo
305            # box will return True or False, if you have to type could be
306            # something else
307            val = False
308        else:
309            val = True
310        try:
311            if val:
312                self.data.fix()
313            else:
314                self.data.unfix()
315        except:
316            return
317
318
319class ComponentDataModel(QAbstractItemModel):
320    """
321    This is a data model to provide the tree structure and information
322    to the tree viewer
323    """
324    def __init__(self, parent, ui_data, columns=["name", "value"],
325                 components=(Var,), editable=[]):
326        super(ComponentDataModel, self).__init__(parent)
327        self.column = columns
328        self._col_editable = editable
329        self.ui_data = ui_data
330        self.components = components
331        self.update_model()
332
333    def update_model(self):
334        self.rootItems = []
335        self._create_tree(o=self.ui_data.model)
336
337    def _update_tree(self, parent=None, o=None):
338        """
339        Check tree structure against the Pyomo model to add or delete
340        components as needed. The arguments are to be used in the recursive
341        function. Entering into this don't specify any args.
342        """
343        # Blocks are special they define the hiarchy of the model, so first
344        # check for blocks. Other comonent can be handled togeter
345        if o is None and len(self.rootItems) > 0: #top level object (no parent)
346            parent = self.rootItems[0] # should be single root node for now
347            o = parent.data # start with root node
348            for no in o.component_objects(descend_into=False):
349                # This will traverse the whole Pyomo model tree
350                self._update_tree(parent=parent, o=no)
351            return
352        elif o is None: # if o is None, but no root nodes (when no model)
353            return
354
355        # past the root node go down here
356        item = parent.ids.get(id(o), None)
357        if item is not None: # check if any children of item where deleted
358            for i in item.children:
359                try:
360                    if i.data.parent_block() is None:
361                        i.parent.children.remove(i)
362                        del(i.parent.ids[id(i.data)])
363                        del(i) # probably should descend down and delete stuff
364                except AttributeError:
365                    # Probably an element of an indexed immutable param
366                    pass
367        if isinstance(o, _BlockData): #single block or element of indexed block
368            if item is None:
369                item = self._add_item(parent=parent, o=o)
370            for no in o.component_objects(descend_into=False):
371                self._update_tree(parent=item, o=no)
372        elif isinstance(o, Block): #indexed block, so need to add elements
373            if item is None:
374                item = self._add_item(parent=parent, o=o)
375            if hasattr(o.index_set(), "is_constructed") and \
376                o.index_set().is_constructed():
377                for key in sorted(o.keys()):
378                    self._update_tree(parent=item, o=o[key])
379        elif isinstance(o, self.components): #anything else
380            if item is None:
381                item = self._add_item(parent=parent, o=o)
382            if hasattr(o.index_set(), "is_constructed") and \
383                o.index_set().is_constructed():
384                for key in sorted(o.keys()):
385                    if key == None: break # Single variable so skip
386                    item2 = item.ids.get(id(o[key]), None)
387                    if item2 is None:
388                        item2 = self._add_item(parent=item, o=o[key])
389                    item2._visited = True
390        return
391
392    def _create_tree(self, parent=None, o=None):
393        """
394        This create a model tree structure to display in a tree view.
395        Args:
396            parent: a ComponentDataItem underwhich to create a TreeItem
397            o: A Pyomo component to add to the tree
398        """
399        # Blocks are special they define the hiarchy of the model, so first
400        # check for blocks. Other comonent can be handled togeter
401        if isinstance(o, _BlockData): #single block or element of indexed block
402            item = self._add_item(parent=parent, o=o)
403            for no in o.component_objects(descend_into=False):
404                self._create_tree(parent=item, o=no)
405        elif isinstance(o, Block): #indexed block, so need to add elements
406            item = self._add_item(parent=parent, o=o)
407            if hasattr(o.index_set(), "is_constructed") and \
408                o.index_set().is_constructed():
409                for key in sorted(o.keys()):
410                    self._create_tree(parent=item, o=o[key])
411        elif isinstance(o, self.components): #anything else
412            item = self._add_item(parent=parent, o=o)
413            if hasattr(o.index_set(), "is_constructed") and \
414                o.index_set().is_constructed():
415                for key in sorted(o.keys()):
416                    if key == None: break #Single variable so skip
417                    self._add_item(parent=item, o=o[key])
418
419    def _add_item(self, parent, o):
420        """
421        Add a root item if parent is None, otherwise add a child
422        """
423        if parent is None:
424            item = self._add_root_item(o)
425        else:
426            item = parent.add_child(o)
427        return item
428
429    def _add_root_item(self, o):
430        """
431        Add a root tree item
432        """
433        item = ComponentDataItem(None, o, ui_data=self.ui_data)
434        self.rootItems.append(item)
435        return item
436
437    def parent(self, index):
438        if not index.isValid():
439            return QtCore.QModelIndex()
440        item = index.internalPointer()
441        if item.parent is None:
442            return QtCore.QModelIndex()
443        else:
444            return self.createIndex(0, 0, item.parent)
445
446    def index(self, row, column, parent=QtCore.QModelIndex()):
447        if not parent.isValid():
448            return self.createIndex(row, column, self.rootItems[row])
449        parentItem = parent.internalPointer()
450        return self.createIndex(row, column, parentItem.children[row])
451
452    def columnCount(self, parent=QtCore.QModelIndex()):
453        """
454        Return the number of columns
455        """
456        return len(self.column)
457
458    def rowCount(self, parent=QtCore.QModelIndex()):
459        if not parent.isValid():
460            return len(self.rootItems)
461        return len(parent.internalPointer().children)
462
463    def data(self, index=QtCore.QModelIndex(), role=QtCore.Qt.DisplayRole):
464        if role==QtCore.Qt.DisplayRole or role==QtCore.Qt.EditRole:
465            a = self.column[index.column()]
466            return index.internalPointer().get(a)
467        elif role==QtCore.Qt.ToolTipRole:
468            if self.column[index.column()] == "name":
469                o = index.internalPointer()
470                if isinstance(o.data, _ConstraintData):
471                    return o.get("expr")
472                else:
473                    return o.get("doc")
474        elif role==QtCore.Qt.ForegroundRole:
475            if isinstance(index.internalPointer().data, (Block, _BlockData)):
476                return QtCore.QVariant(QColor(QtCore.Qt.black))
477            else:
478                return QtCore.QVariant(QColor(QtCore.Qt.blue));
479        else:
480            return
481
482    def headerData(self, i, orientation, role=QtCore.Qt.DisplayRole):
483        """
484        Return the column headings for the horizontal header and
485        index numbers for the vertical header.
486        """
487        if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
488            return self.column[i]
489        return None
490
491    def flags(self, index=QtCore.QModelIndex()):
492        if self.column[index.column()] in self._col_editable:
493            return(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable |
494                   QtCore.Qt.ItemIsEditable)
495        else:
496            return(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable)
497