Skip to content

Commit 7bbe3a7

Browse files
authored
Merge pull request Pyomo#3271 from eslickj/vmodel
Add a model viewer option to specify the model by variable name in __main__
2 parents c783037 + 19df8be commit 7bbe3a7

File tree

6 files changed

+83
-20
lines changed

6 files changed

+83
-20
lines changed

pyomo/contrib/viewer/README.md

+18
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,24 @@ ui = get_mainwindow(model=model)
4242
# Do model things, the viewer will stay in sync with the Pyomo model
4343
```
4444

45+
If you are working in Jupyter notebook, Jupyter qtconsole, or other Jupyter-
46+
based IDEs, and your model is in the __main__ namespace (this is the usual case),
47+
you can specify the model by its variable name as below. The advantage of this
48+
is that if you replace the model with a new model having the same variable name,
49+
the UI will automatically update without having to manually reset the model pointer.
50+
51+
```python
52+
%gui qt #Enables IPython's GUI event loop integration.
53+
# Execute the above in its own cell and wait for it to finish before moving on.
54+
from pyomo.contrib.viewer.ui import get_mainwindow
55+
import pyomo.environ as pyo
56+
57+
model = pyo.ConcreteModel() # could import an existing model here
58+
ui = get_mainwindow(model_var_name_in_main="model")
59+
60+
# Do model things, the viewer will stay in sync with the Pyomo model
61+
```
62+
4563
**Note:** the ```%gui qt``` cell must be executed in its own cell and execution
4664
must complete before running any other cells (you can't use "run all").
4765

pyomo/contrib/viewer/model_select.py

+8-6
Original file line numberDiff line numberDiff line change
@@ -60,31 +60,33 @@ def select_model(self):
6060
items = self.tableWidget.selectedItems()
6161
if len(items) == 0:
6262
return
63-
self.ui_data.model = self.models[items[0].row()]
63+
self.ui_data.model_var_name_in_main = self.models[items[0].row()][1]
64+
self.ui_data.model = self.models[items[0].row()][0]
6465
self.close()
6566

6667
def update_models(self):
6768
import __main__
6869

69-
s = __main__.__dict__
70+
s = dir(__main__)
7071
keys = []
7172
for k in s:
72-
if isinstance(s[k], pyo.Block):
73+
if isinstance(getattr(__main__, k), pyo.Block):
7374
keys.append(k)
7475
self.tableWidget.clearContents()
7576
self.tableWidget.setRowCount(len(keys))
7677
self.models = []
7778
for row, k in enumerate(sorted(keys)):
79+
model = getattr(__main__, k)
7880
item = myqt.QTableWidgetItem()
7981
item.setText(k)
8082
self.tableWidget.setItem(row, 0, item)
8183
item = myqt.QTableWidgetItem()
8284
try:
83-
item.setText(s[k].name)
85+
item.setText(model.name)
8486
except:
8587
item.setText("None")
8688
self.tableWidget.setItem(row, 1, item)
8789
item = myqt.QTableWidgetItem()
88-
item.setText(str(type(s[k])))
90+
item.setText(str(type(model)))
8991
self.tableWidget.setItem(row, 2, item)
90-
self.models.append(s[k])
92+
self.models.append((model, k))

pyomo/contrib/viewer/pyomo_viewer.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class QtApp(
4141
model
4242
except NameError:
4343
model=None
44-
ui, model = get_mainwindow(model=model, ask_close=False)
44+
ui = get_mainwindow(model=model, ask_close=False)
4545
ui.setWindowTitle('Pyomo Model Viewer -- {}')"""
4646

4747
_kernel_cmd_hide_ui = """try:

pyomo/contrib/viewer/tests/test_qt.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ def blackbox(a, b):
103103
@unittest.skipIf(not available, "Qt packages are not available.")
104104
def test_get_mainwindow(qtbot):
105105
m = get_model()
106-
mw, m = get_mainwindow(model=m, testing=True)
106+
mw = get_mainwindow(model=m, testing=True)
107107
assert hasattr(mw, "menuBar")
108108
assert isinstance(mw.variables, ModelBrowser)
109109
assert isinstance(mw.constraints, ModelBrowser)
@@ -113,13 +113,13 @@ def test_get_mainwindow(qtbot):
113113

114114
@unittest.skipIf(not available, "Qt packages are not available.")
115115
def test_close_mainwindow(qtbot):
116-
mw, m = get_mainwindow(model=None, testing=True)
116+
mw = get_mainwindow(model=None, testing=True)
117117
mw.exit_action()
118118

119119

120120
@unittest.skipIf(not available, "Qt packages are not available.")
121121
def test_show_model_select_no_models(qtbot):
122-
mw, m = get_mainwindow(model=None, testing=True)
122+
mw = get_mainwindow(model=None, testing=True)
123123
ms = mw.show_model_select()
124124
ms.update_models()
125125
ms.select_model()
@@ -128,7 +128,7 @@ def test_show_model_select_no_models(qtbot):
128128
@unittest.skipIf(not available, "Qt packages are not available.")
129129
def test_model_information(qtbot):
130130
m = get_model()
131-
mw, m = get_mainwindow(model=m, testing=True)
131+
mw = get_mainwindow(model=m, testing=True)
132132
mw.model_information()
133133
assert isinstance(mw._dialog, QMessageBox)
134134
text = mw._dialog.text()
@@ -149,15 +149,15 @@ def test_model_information(qtbot):
149149
@unittest.skipIf(not available, "Qt packages are not available.")
150150
def test_tree_expand_collapse(qtbot):
151151
m = get_model()
152-
mw, m = get_mainwindow(model=m, testing=True)
152+
mw = get_mainwindow(model=m, testing=True)
153153
mw.variables.treeView.expandAll()
154154
mw.variables.treeView.collapseAll()
155155

156156

157157
@unittest.skipIf(not available, "Qt packages are not available.")
158158
def test_residual_table(qtbot):
159159
m = get_model()
160-
mw, m = get_mainwindow(model=m, testing=True)
160+
mw = get_mainwindow(model=m, testing=True)
161161
mw.residuals_restart()
162162
mw.ui_data.calculate_expressions()
163163
mw.residuals.calculate()
@@ -184,7 +184,7 @@ def test_residual_table(qtbot):
184184
@unittest.skipIf(not available, "Qt packages are not available.")
185185
def test_var_tree(qtbot):
186186
m = get_model()
187-
mw, m = get_mainwindow(model=m, testing=True)
187+
mw = get_mainwindow(model=m, testing=True)
188188
qtbot.addWidget(mw)
189189
mw.variables.treeView.expandAll()
190190
root_index = mw.variables.datmodel.index(0, 0)
@@ -218,7 +218,7 @@ def test_var_tree(qtbot):
218218
@unittest.skipIf(not available, "Qt packages are not available.")
219219
def test_bad_view(qtbot):
220220
m = get_model()
221-
mw, m = get_mainwindow(model=m, testing=True)
221+
mw = get_mainwindow(model=m, testing=True)
222222
err = None
223223
try:
224224
mw.badTree = mw._tree_restart(

pyomo/contrib/viewer/ui.py

+36-4
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@ class _MainWindow(object):
6666
_log.error(_err)
6767

6868

69-
def get_mainwindow(model=None, show=True, ask_close=True, testing=False):
69+
def get_mainwindow(
70+
model=None, show=True, ask_close=True, model_var_name_in_main=None, testing=False
71+
):
7072
"""
7173
Create a UI MainWindow.
7274
@@ -79,16 +81,32 @@ def get_mainwindow(model=None, show=True, ask_close=True, testing=False):
7981
(ui, model): ui is the MainWindow widget, and model is the linked Pyomo
8082
model. If no model is provided a new ConcreteModel is created
8183
"""
84+
model_name = model_var_name_in_main
8285
if model is None:
83-
model = pyo.ConcreteModel(name="Default")
84-
ui = MainWindow(model=model, ask_close=ask_close, testing=testing)
86+
import __main__
87+
88+
if model_name in dir(__main__):
89+
if isinstance(getattr(__main__, model_name), pyo.Block):
90+
model = getattr(__main__, model_name)
91+
else:
92+
for s in dir(__main__):
93+
if isinstance(getattr(__main__, s), pyo.Block):
94+
model = getattr(__main__, s)
95+
model_name = s
96+
break
97+
ui = MainWindow(
98+
model=model,
99+
model_var_name_in_main=model_name,
100+
ask_close=ask_close,
101+
testing=testing,
102+
)
85103
try:
86104
get_ipython().events.register("post_execute", ui.refresh_on_execute)
87105
except AttributeError:
88106
pass # not in ipy kernel, so is fine to not register callback
89107
if show:
90108
ui.show()
91-
return ui, model
109+
return ui
92110

93111

94112
class MainWindow(_MainWindow, _MainWindowUI):
@@ -97,6 +115,7 @@ def __init__(self, *args, **kwargs):
97115
main = self.main = kwargs.pop("main", None)
98116
ask_close = self.ask_close = kwargs.pop("ask_close", True)
99117
self.testing = kwargs.pop("testing", False)
118+
model_var_name_in_main = kwargs.pop("model_var_name_in_main", None)
100119
flags = kwargs.pop("flags", 0)
101120
self.ui_data = UIData(model=model)
102121
super().__init__(*args, **kwargs)
@@ -128,6 +147,7 @@ def __init__(self, *args, **kwargs):
128147
self.actionCalculateExpressions.triggered.connect(
129148
self.ui_data.calculate_expressions
130149
)
150+
self.ui_data.model_var_name_in_main = model_var_name_in_main
131151
self.actionTile.triggered.connect(self.mdiArea.tileSubWindows)
132152
self.actionCascade.triggered.connect(self.mdiArea.cascadeSubWindows)
133153
self.actionTabs.triggered.connect(self.toggle_tabs)
@@ -256,6 +276,18 @@ def refresh_on_execute(self):
256276
ipython kernel. The main purpose of this right now it to refresh the
257277
UI display so that it matches the current state of the model.
258278
"""
279+
if self.ui_data.model_var_name_in_main is not None:
280+
import __main__
281+
282+
try:
283+
mname = self.ui_data.model_var_name_in_main
284+
mid = id(getattr(__main__, mname))
285+
if id(self.ui_data.model) != mid:
286+
self.ui_data.model = getattr(__main__, mname)
287+
self.update_model
288+
return
289+
except AttributeError:
290+
pass
259291
for w in self._refresh_list:
260292
try:
261293
w.refresh()

pyomo/contrib/viewer/ui_data.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,27 @@ class UIDataNoUi(object):
3939
UIData. The class is split this way for testing when PyQt is not available.
4040
"""
4141

42-
def __init__(self, model=None):
42+
def __init__(self, model=None, model_var_name_in_main=None):
4343
"""
4444
This class holds the basic UI setup, but doesn't depend on Qt. It
4545
shouldn't really be used except for testing when Qt is not available.
4646
4747
Args:
4848
model: The Pyomo model to view
49+
model_var_name_in_main: if this is set, check that the model variable
50+
which points to a model object in __main__ has the same id when
51+
the UI is refreshed due to a command being executed in jupyter
52+
notebook or QtConsole, if not the same id, then update the model
53+
Since the model viewer is not necessarily pointed at a model in the
54+
__main__ namespace only set this if you want the model to auto
55+
update. Since the model selector dialog lets you choose models
56+
from the __main__ namespace it sets this when you select a model.
57+
This is useful if you run a script repeatedly that replaces a model
58+
preventing you from looking at a previous version of the model.
4959
"""
5060
super().__init__()
5161
self._model = None
62+
self.model_var_name_in_main = model_var_name_in_main
5263
self._begin_update = False
5364
self.value_cache = ComponentMap()
5465
self.value_cache_units = ComponentMap()

0 commit comments

Comments
 (0)