Skip to content

Commit

Permalink
Merge pull request Pyomo#3271 from eslickj/vmodel
Browse files Browse the repository at this point in the history
Add a model viewer option to specify the model by variable name in __main__
  • Loading branch information
mrmundt authored Jun 14, 2024
2 parents c783037 + 19df8be commit 7bbe3a7
Show file tree
Hide file tree
Showing 6 changed files with 83 additions and 20 deletions.
18 changes: 18 additions & 0 deletions pyomo/contrib/viewer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,24 @@ ui = get_mainwindow(model=model)
# Do model things, the viewer will stay in sync with the Pyomo model
```

If you are working in Jupyter notebook, Jupyter qtconsole, or other Jupyter-
based IDEs, and your model is in the __main__ namespace (this is the usual case),
you can specify the model by its variable name as below. The advantage of this
is that if you replace the model with a new model having the same variable name,
the UI will automatically update without having to manually reset the model pointer.

```python
%gui qt #Enables IPython's GUI event loop integration.
# Execute the above in its own cell and wait for it to finish before moving on.
from pyomo.contrib.viewer.ui import get_mainwindow
import pyomo.environ as pyo

model = pyo.ConcreteModel() # could import an existing model here
ui = get_mainwindow(model_var_name_in_main="model")

# Do model things, the viewer will stay in sync with the Pyomo model
```

**Note:** the ```%gui qt``` cell must be executed in its own cell and execution
must complete before running any other cells (you can't use "run all").

Expand Down
14 changes: 8 additions & 6 deletions pyomo/contrib/viewer/model_select.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,31 +60,33 @@ def select_model(self):
items = self.tableWidget.selectedItems()
if len(items) == 0:
return
self.ui_data.model = self.models[items[0].row()]
self.ui_data.model_var_name_in_main = self.models[items[0].row()][1]
self.ui_data.model = self.models[items[0].row()][0]
self.close()

def update_models(self):
import __main__

s = __main__.__dict__
s = dir(__main__)
keys = []
for k in s:
if isinstance(s[k], pyo.Block):
if isinstance(getattr(__main__, k), pyo.Block):
keys.append(k)
self.tableWidget.clearContents()
self.tableWidget.setRowCount(len(keys))
self.models = []
for row, k in enumerate(sorted(keys)):
model = getattr(__main__, k)
item = myqt.QTableWidgetItem()
item.setText(k)
self.tableWidget.setItem(row, 0, item)
item = myqt.QTableWidgetItem()
try:
item.setText(s[k].name)
item.setText(model.name)
except:
item.setText("None")
self.tableWidget.setItem(row, 1, item)
item = myqt.QTableWidgetItem()
item.setText(str(type(s[k])))
item.setText(str(type(model)))
self.tableWidget.setItem(row, 2, item)
self.models.append(s[k])
self.models.append((model, k))
2 changes: 1 addition & 1 deletion pyomo/contrib/viewer/pyomo_viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class QtApp(
model
except NameError:
model=None
ui, model = get_mainwindow(model=model, ask_close=False)
ui = get_mainwindow(model=model, ask_close=False)
ui.setWindowTitle('Pyomo Model Viewer -- {}')"""

_kernel_cmd_hide_ui = """try:
Expand Down
16 changes: 8 additions & 8 deletions pyomo/contrib/viewer/tests/test_qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def blackbox(a, b):
@unittest.skipIf(not available, "Qt packages are not available.")
def test_get_mainwindow(qtbot):
m = get_model()
mw, m = get_mainwindow(model=m, testing=True)
mw = get_mainwindow(model=m, testing=True)
assert hasattr(mw, "menuBar")
assert isinstance(mw.variables, ModelBrowser)
assert isinstance(mw.constraints, ModelBrowser)
Expand All @@ -113,13 +113,13 @@ def test_get_mainwindow(qtbot):

@unittest.skipIf(not available, "Qt packages are not available.")
def test_close_mainwindow(qtbot):
mw, m = get_mainwindow(model=None, testing=True)
mw = get_mainwindow(model=None, testing=True)
mw.exit_action()


@unittest.skipIf(not available, "Qt packages are not available.")
def test_show_model_select_no_models(qtbot):
mw, m = get_mainwindow(model=None, testing=True)
mw = get_mainwindow(model=None, testing=True)
ms = mw.show_model_select()
ms.update_models()
ms.select_model()
Expand All @@ -128,7 +128,7 @@ def test_show_model_select_no_models(qtbot):
@unittest.skipIf(not available, "Qt packages are not available.")
def test_model_information(qtbot):
m = get_model()
mw, m = get_mainwindow(model=m, testing=True)
mw = get_mainwindow(model=m, testing=True)
mw.model_information()
assert isinstance(mw._dialog, QMessageBox)
text = mw._dialog.text()
Expand All @@ -149,15 +149,15 @@ def test_model_information(qtbot):
@unittest.skipIf(not available, "Qt packages are not available.")
def test_tree_expand_collapse(qtbot):
m = get_model()
mw, m = get_mainwindow(model=m, testing=True)
mw = get_mainwindow(model=m, testing=True)
mw.variables.treeView.expandAll()
mw.variables.treeView.collapseAll()


@unittest.skipIf(not available, "Qt packages are not available.")
def test_residual_table(qtbot):
m = get_model()
mw, m = get_mainwindow(model=m, testing=True)
mw = get_mainwindow(model=m, testing=True)
mw.residuals_restart()
mw.ui_data.calculate_expressions()
mw.residuals.calculate()
Expand All @@ -184,7 +184,7 @@ def test_residual_table(qtbot):
@unittest.skipIf(not available, "Qt packages are not available.")
def test_var_tree(qtbot):
m = get_model()
mw, m = get_mainwindow(model=m, testing=True)
mw = get_mainwindow(model=m, testing=True)
qtbot.addWidget(mw)
mw.variables.treeView.expandAll()
root_index = mw.variables.datmodel.index(0, 0)
Expand Down Expand Up @@ -218,7 +218,7 @@ def test_var_tree(qtbot):
@unittest.skipIf(not available, "Qt packages are not available.")
def test_bad_view(qtbot):
m = get_model()
mw, m = get_mainwindow(model=m, testing=True)
mw = get_mainwindow(model=m, testing=True)
err = None
try:
mw.badTree = mw._tree_restart(
Expand Down
40 changes: 36 additions & 4 deletions pyomo/contrib/viewer/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ class _MainWindow(object):
_log.error(_err)


def get_mainwindow(model=None, show=True, ask_close=True, testing=False):
def get_mainwindow(
model=None, show=True, ask_close=True, model_var_name_in_main=None, testing=False
):
"""
Create a UI MainWindow.
Expand All @@ -79,16 +81,32 @@ def get_mainwindow(model=None, show=True, ask_close=True, testing=False):
(ui, model): ui is the MainWindow widget, and model is the linked Pyomo
model. If no model is provided a new ConcreteModel is created
"""
model_name = model_var_name_in_main
if model is None:
model = pyo.ConcreteModel(name="Default")
ui = MainWindow(model=model, ask_close=ask_close, testing=testing)
import __main__

if model_name in dir(__main__):
if isinstance(getattr(__main__, model_name), pyo.Block):
model = getattr(__main__, model_name)
else:
for s in dir(__main__):
if isinstance(getattr(__main__, s), pyo.Block):
model = getattr(__main__, s)
model_name = s
break
ui = MainWindow(
model=model,
model_var_name_in_main=model_name,
ask_close=ask_close,
testing=testing,
)
try:
get_ipython().events.register("post_execute", ui.refresh_on_execute)
except AttributeError:
pass # not in ipy kernel, so is fine to not register callback
if show:
ui.show()
return ui, model
return ui


class MainWindow(_MainWindow, _MainWindowUI):
Expand All @@ -97,6 +115,7 @@ def __init__(self, *args, **kwargs):
main = self.main = kwargs.pop("main", None)
ask_close = self.ask_close = kwargs.pop("ask_close", True)
self.testing = kwargs.pop("testing", False)
model_var_name_in_main = kwargs.pop("model_var_name_in_main", None)
flags = kwargs.pop("flags", 0)
self.ui_data = UIData(model=model)
super().__init__(*args, **kwargs)
Expand Down Expand Up @@ -128,6 +147,7 @@ def __init__(self, *args, **kwargs):
self.actionCalculateExpressions.triggered.connect(
self.ui_data.calculate_expressions
)
self.ui_data.model_var_name_in_main = model_var_name_in_main
self.actionTile.triggered.connect(self.mdiArea.tileSubWindows)
self.actionCascade.triggered.connect(self.mdiArea.cascadeSubWindows)
self.actionTabs.triggered.connect(self.toggle_tabs)
Expand Down Expand Up @@ -256,6 +276,18 @@ def refresh_on_execute(self):
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.
"""
if self.ui_data.model_var_name_in_main is not None:
import __main__

try:
mname = self.ui_data.model_var_name_in_main
mid = id(getattr(__main__, mname))
if id(self.ui_data.model) != mid:
self.ui_data.model = getattr(__main__, mname)
self.update_model
return
except AttributeError:
pass
for w in self._refresh_list:
try:
w.refresh()
Expand Down
13 changes: 12 additions & 1 deletion pyomo/contrib/viewer/ui_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,27 @@ class UIDataNoUi(object):
UIData. The class is split this way for testing when PyQt is not available.
"""

def __init__(self, model=None):
def __init__(self, model=None, model_var_name_in_main=None):
"""
This class holds the basic UI setup, but doesn't depend on Qt. It
shouldn't really be used except for testing when Qt is not available.
Args:
model: The Pyomo model to view
model_var_name_in_main: if this is set, check that the model variable
which points to a model object in __main__ has the same id when
the UI is refreshed due to a command being executed in jupyter
notebook or QtConsole, if not the same id, then update the model
Since the model viewer is not necessarily pointed at a model in the
__main__ namespace only set this if you want the model to auto
update. Since the model selector dialog lets you choose models
from the __main__ namespace it sets this when you select a model.
This is useful if you run a script repeatedly that replaces a model
preventing you from looking at a previous version of the model.
"""
super().__init__()
self._model = None
self.model_var_name_in_main = model_var_name_in_main
self._begin_update = False
self.value_cache = ComponentMap()
self.value_cache_units = ComponentMap()
Expand Down

0 comments on commit 7bbe3a7

Please sign in to comment.