Skip to content

Commit

Permalink
Merge branch 'main' into cyipopt-extend-callback
Browse files Browse the repository at this point in the history
  • Loading branch information
mrmundt authored Jun 25, 2024
2 parents 170df3e + ae354ae commit 9c91aa2
Show file tree
Hide file tree
Showing 16 changed files with 181 additions and 72 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/test_branches.yml
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,9 @@ jobs:
token: ${{ secrets.PYOMO_CODECOV_TOKEN }}
name: ${{ matrix.TARGET }}
flags: ${{ matrix.TARGET }}
# downgrading after v0.7.0 broke tokenless upload
# see codecov/codecov-action#1487
version: v0.6.0
fail_ci_if_error: true

- name: Upload other coverage reports
Expand All @@ -867,4 +870,7 @@ jobs:
token: ${{ secrets.PYOMO_CODECOV_TOKEN }}
name: ${{ matrix.TARGET }}/other
flags: ${{ matrix.TARGET }},other
# downgrading after v0.7.0 broke tokenless upload
# see codecov/codecov-action#1487
version: v0.6.0
fail_ci_if_error: true
6 changes: 6 additions & 0 deletions .github/workflows/test_pr_and_main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,9 @@ jobs:
token: ${{ secrets.PYOMO_CODECOV_TOKEN }}
name: ${{ matrix.TARGET }}
flags: ${{ matrix.TARGET }}
# downgrading after v0.7.0 broke tokenless upload
# see codecov/codecov-action#1487
version: v0.6.0
fail_ci_if_error: true

- name: Upload other coverage reports
Expand All @@ -911,4 +914,7 @@ jobs:
token: ${{ secrets.PYOMO_CODECOV_TOKEN }}
name: ${{ matrix.TARGET }}/other
flags: ${{ matrix.TARGET }},other
# downgrading after v0.7.0 broke tokenless upload
# see codecov/codecov-action#1487
version: v0.6.0
fail_ci_if_error: true
10 changes: 8 additions & 2 deletions pyomo/common/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -999,10 +999,13 @@ def _finalize_numpy(np, available):
# registration here (to bypass the deprecation warning) until we
# finally remove all support for it
numeric_types._native_boolean_types.add(t)
_floats = [np.float_, np.float16, np.float32, np.float64]
_floats = [np.float16, np.float32, np.float64]
# float96 and float128 may or may not be defined in this particular
# numpy build (it depends on platform and version).
# Register them only if they are present
if hasattr(np, 'float_'):
# Prepend to preserve previous functionality
_floats.insert(0, np.float_)
if hasattr(np, 'float96'):
_floats.append(np.float96)
if hasattr(np, 'float128'):
Expand All @@ -1013,10 +1016,13 @@ def _finalize_numpy(np, available):
# registration here (to bypass the deprecation warning) until we
# finally remove all support for it
numeric_types._native_boolean_types.add(t)
_complex = [np.complex_, np.complex64, np.complex128]
_complex = [np.complex64, np.complex128]
# complex192 and complex256 may or may not be defined in this
# particular numpy build (it depends on platform and version).
# Register them only if they are present
if hasattr(np, 'np.complex_'):
# Prepend to preserve functionality
_complex.insert(0, np.complex_)
if hasattr(np, 'complex192'):
_complex.append(np.complex192)
if hasattr(np, 'complex256'):
Expand Down
35 changes: 28 additions & 7 deletions pyomo/common/unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,7 @@ def filter_fcn(self, line):
return False

def filter_file_contents(self, lines, abstol=None):
_numpy_scalar_re = re.compile(r'np.(int|float)\d+\(([^\)]+)\)')
filtered = []
deprecated = None
for line in lines:
Expand All @@ -807,29 +808,49 @@ def filter_file_contents(self, lines, abstol=None):
item_list = []
items = line.strip().split()
for i in items:
# Split up lists, dicts, and sets
while i and i[0] in '[{':
item_list.append(i[0])
i = i[1:]
tail = []
while i and i[-1] in ',:]}':
tail.append(i[-1])
i = i[:-1]

# A few substitutions to get tests passing on pypy3
if ".inf" in i:
i = i.replace(".inf", "inf")
if "null" in i:
i = i.replace("null", "None")

try:
item_list.append(float(i))
# Numpy 2.x changed the repr for scalars. Convert
# the new scalar reprs back to the original (which
# were indistinguishable from python floats/ints)
np_match = _numpy_scalar_re.match(i)
if np_match:
item_list.append(float(np_match.group(2)))
else:
item_list.append(float(i))
except:
item_list.append(i)
if tail:
tail.reverse()
item_list.extend(tail)

# We can get printed results objects where the baseline is
# exactly 0 (and omitted) and the test is slightly non-zero.
# We will look for the pattern of values printed from
# results objects and remote them if they are within
# tolerance of 0
if (
len(item_list) == 2
and item_list[0] == 'Value:'
and type(item_list[1]) is float
and abs(item_list[1]) < (abstol or 0)
and len(filtered[-1]) == 1
and filtered[-1][0][-1] == ':'
len(item_list) == 3
and item_list[0] == 'Value'
and item_list[1] == ':'
and type(item_list[2]) is float
and abs(item_list[2]) < (abstol or 0)
and len(filtered[-1]) == 2
and filtered[-1][1] == ':'
):
filtered.pop()
else:
Expand Down
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
Loading

0 comments on commit 9c91aa2

Please sign in to comment.