diff --git a/pyomo/contrib/viewer/README.md b/pyomo/contrib/viewer/README.md index cfc50b54ce2..93d773e3829 100644 --- a/pyomo/contrib/viewer/README.md +++ b/pyomo/contrib/viewer/README.md @@ -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"). diff --git a/pyomo/contrib/viewer/model_select.py b/pyomo/contrib/viewer/model_select.py index e9c82740708..1e65e91a089 100644 --- a/pyomo/contrib/viewer/model_select.py +++ b/pyomo/contrib/viewer/model_select.py @@ -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)) diff --git a/pyomo/contrib/viewer/pyomo_viewer.py b/pyomo/contrib/viewer/pyomo_viewer.py index 6a24e12aa61..e4f75c86840 100644 --- a/pyomo/contrib/viewer/pyomo_viewer.py +++ b/pyomo/contrib/viewer/pyomo_viewer.py @@ -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: diff --git a/pyomo/contrib/viewer/tests/test_qt.py b/pyomo/contrib/viewer/tests/test_qt.py index e71921500f9..b7250729cd9 100644 --- a/pyomo/contrib/viewer/tests/test_qt.py +++ b/pyomo/contrib/viewer/tests/test_qt.py @@ -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) @@ -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() @@ -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() @@ -149,7 +149,7 @@ 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() @@ -157,7 +157,7 @@ def test_tree_expand_collapse(qtbot): @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() @@ -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) @@ -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( diff --git a/pyomo/contrib/viewer/ui.py b/pyomo/contrib/viewer/ui.py index 374af8a26f0..ac96e58eea9 100644 --- a/pyomo/contrib/viewer/ui.py +++ b/pyomo/contrib/viewer/ui.py @@ -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. @@ -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): @@ -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) @@ -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) @@ -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() diff --git a/pyomo/contrib/viewer/ui_data.py b/pyomo/contrib/viewer/ui_data.py index c716cfeedf6..8d83be91e5f 100644 --- a/pyomo/contrib/viewer/ui_data.py +++ b/pyomo/contrib/viewer/ui_data.py @@ -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()