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