Skip to content

Commit 962e130

Browse files
Merge branch 'main' into pyros-custom-subproblem-formats
2 parents bfe850a + 14a1c10 commit 962e130

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+3309
-1020
lines changed

.github/workflows/test_branches.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,10 @@ jobs:
4444
python-version: '3.10'
4545
- name: Black Formatting Check
4646
run: |
47-
# Note v24.4.1 fails due to a bug in the parser
47+
# Note v24.4.1 fails due to a bug in the parser. Project-level
48+
# configuration is inherited from pyproject.toml.
4849
pip install 'black!=24.4.1'
49-
black . -S -C --check --diff --exclude examples/pyomobook/python-ch/BadIndent.py
50+
black . --check --diff
5051
- name: Spell Check
5152
uses: crate-ci/typos@master
5253
with:

.github/workflows/test_pr_and_main.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,10 @@ jobs:
5555
python-version: '3.10'
5656
- name: Black Formatting Check
5757
run: |
58-
# Note v24.4.1 fails due to a bug in the parser
58+
# Note v24.4.1 fails due to a bug in the parser. Project-level
59+
# configuration is inherited from pyproject.toml.
5960
pip install 'black!=24.4.1'
60-
black . -S -C --check --diff --exclude examples/pyomobook/python-ch/BadIndent.py
61+
black . --check --diff
6162
- name: Spell Check
6263
uses: crate-ci/typos@master
6364
with:

doc/OnlineDocs/contribution_guide.rst

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,17 @@ run:
3737

3838
# Auto-apply correct formatting
3939
pip install black
40-
black -S -C <path> --exclude examples/pyomobook/python-ch/BadIndent.py
40+
black <path>
4141
# Find typos in files
4242
conda install typos
4343
typos --config .github/workflows/typos.toml <path>
4444
45-
If the spell-checker returns a failure for a word that is spelled correctly,
46-
please add the word to the ``.github/workflows/typos.toml`` file.
45+
If the spell-checker returns a failure for a word that is spelled
46+
correctly, please add the word to the ``.github/workflows/typos.toml``
47+
file. Note also that ``black`` reads from ``pyproject.toml`` to
48+
determine correct configuration, so if you are running ``black``
49+
indirectly (for example, using an IDE integration), please ensure you
50+
are not overriding the project-level configuration set in that file.
4751

4852
Online Pyomo documentation is generated using `Sphinx <https://www.sphinx-doc.org/en/master/>`_
4953
with the ``napoleon`` extension enabled. For API documentation we use of one of these

doc/OnlineDocs/explanation/modeling/math_programming/sets.rst

Lines changed: 79 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,30 @@ and postpones creation of its members:
2929

3030
The :class:`Set` class takes optional arguments such as:
3131

32-
- ``dimen`` = Dimension of the members of the set
33-
- ``doc`` = String describing the set
34-
- ``filter`` = A Boolean function used during construction to indicate if a
35-
potential new member should be assigned to the set
36-
- ``initialize`` = An iterable containing the initial members of the Set, or
37-
function that returns an iterable of the initial members the set.
38-
- ``ordered`` = A Boolean indicator that the set is ordered; the default is ``True``
39-
- ``validate`` = A Boolean function that validates new member data
40-
- ``within`` = Set used for validation; it is a super-set of the set being declared.
32+
``dimen``
33+
Dimension of the members of the set; ``None`` for "jagged" sets
34+
(where members do not have a uniform length).
35+
36+
``doc``
37+
String describing the set
38+
39+
``filter``
40+
A Boolean function used during construction to indicate if a
41+
potential new member should be assigned to the set
42+
43+
``initialize``
44+
An iterable containing the initial members of the Set, or
45+
function that returns an iterable of the initial members the set.
46+
47+
``ordered``
48+
A Boolean indicator that the set is ordered; the default is ``True``
49+
(Set is ordered by insertion order)
50+
51+
``validate``
52+
A Boolean function that validates new member data
53+
54+
``within``
55+
Set used for validation; it is a super-set of the set being declared.
4156

4257
In general, Pyomo attempts to infer the "dimensionality" of Set
4358
components (that is, the number of apparent indices) when they are
@@ -451,8 +466,40 @@ for this model, a toy data file (in AMPL "``.dat``" format) would be:
451466

452467
>>> inst = model.create_instance('src/scripting/Isinglecomm.dat')
453468

454-
This can also be done somewhat more efficiently, and perhaps more clearly,
455-
using a :class:`BuildAction` (for more information, see :ref:`BuildAction`):
469+
A similar result can be accomplished more efficiently (because we only
470+
iterate over the Arcs twice) using initialization functions that accept
471+
only a model block and return a ``dict`` with all the information needed
472+
for the indexed set:
473+
474+
.. doctest::
475+
:hide:
476+
477+
>>> model = inst
478+
>>> del model.NodesIn
479+
>>> del model.NodesOut
480+
481+
.. testcode::
482+
483+
def NodesIn_init(m):
484+
# Create a dict to show NodesIn list for every node
485+
d = {i: [] for i in m.Nodes}
486+
# loop over the arcs and record the end points
487+
for i, j in model.Arcs:
488+
d[j].append(i)
489+
return d
490+
model.NodesIn = pyo.Set(model.Nodes, initialize=NodesIn_init)
491+
492+
def NodesOut_init(m):
493+
d = {i: [] for i in m.Nodes}
494+
for i, j in model.Arcs:
495+
d[i].append(j)
496+
return d
497+
model.NodesOut = pyo.Set(model.Nodes, initialize=NodesOut_init)
498+
499+
Alternatively, this can also be done even more efficiently, and perhaps
500+
more clearly, outside the context of Set initialization. For concrete
501+
models, scripts can explicitly add elements to the Sets after
502+
declaration:
456503

457504
.. doctest::
458505
:hide:
@@ -463,8 +510,29 @@ using a :class:`BuildAction` (for more information, see :ref:`BuildAction`):
463510

464511
.. testcode::
465512

513+
model.NodesIn = pyo.Set(model.Nodes, within=model.Nodes)
466514
model.NodesOut = pyo.Set(model.Nodes, within=model.Nodes)
515+
516+
# loop over the arcs and record the end points
517+
for i, j in model.Arcs:
518+
model.NodesIn[j].add(i)
519+
model.NodesOut[i].add(j)
520+
521+
For abstract models, that action must be deferred to instance
522+
construction time using a :class:`BuildAction` (for more information,
523+
see :ref:`BuildAction`):
524+
525+
.. doctest::
526+
:hide:
527+
528+
>>> model = inst
529+
>>> del model.NodesIn
530+
>>> del model.NodesOut
531+
532+
.. testcode::
533+
467534
model.NodesIn = pyo.Set(model.Nodes, within=model.Nodes)
535+
model.NodesOut = pyo.Set(model.Nodes, within=model.Nodes)
468536

469537
def Populate_In_and_Out(model):
470538
# loop over the arcs and record the end points

examples/doc/samples/case_studies/diet/DietProblem.tex

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ \subsection*{Build the model}
6262

6363
At this point we must start defining the rules associated with our parameters and variables. We begin with the most important rule, the cost rule, which will tell the model to try and minimize the overall cost. Logically, the total cost is going to be the sum of how much is spent on each food, and that value in turn is going to be determined by the cost of the food and how much of it is purchased. For example, if three \$5 hamburgers and two \$1 apples are purchased, than the total cost would be $3 \cdot 5 + 2 \cdot 1 = 17$. Note that this process is the same as taking the dot product of the amounts vector and the costs vector.
6464

65-
To input this, we must define the cost rule, which we creatively call costRule as
65+
To input this, we must define the cost rule, which we creatively call costRule as
6666

6767
\begin{verbatim}def costRule(model):
6868
return sum(model.costs[n]*model.amount[n] for n in model.foods)
@@ -75,10 +75,10 @@ \subsection*{Build the model}
7575

7676
This line defines the objective of the model as the costRule, which Pyomo interprets as the value it needs to minimize; in this case it will minimize our costs. Also, as a note, we defined the objective as ``model.cost'' which is not to be confused with the parameter we defined earlier as ``model.costs,'' despite their similar names. These are two different values and accidentally giving them the same name will cause problems when trying to solve the problem.
7777

78-
We must also create a rule for the volume consumed. The construction of this rule is similar to the cost rule as once again we take the dot product, this time between the volume and amount vectors.
78+
We must also create a rule for the volume consumed. The construction of this rule is similar to the cost rule as once again we take the dot product, this time between the volume and amount vectors.
7979

8080
\begin{verbatim}def volumeRule(model):
81-
return sum(model.volumes[n]*model.amount[n] for n in
81+
return sum(model.volumes[n]*model.amount[n] for n in
8282
model.foods) <= model.max_volume
8383
8484
model.volume = pyo.Constraint(rule=volumeRule)
@@ -90,7 +90,7 @@ \subsection*{Build the model}
9090

9191
\begin{verbatim}
9292
def nutrientRule(n, model):
93-
value = sum(model.nutrient_value[n,f]*model.amount[f]
93+
value = sum(model.nutrient_value[n,f]*model.amount[f]
9494
for f in model.foods)
9595
return (model.min_nutrient[n], value, model.max_nutrient[n])
9696
@@ -160,7 +160,7 @@ \subsection*{Data entry}
160160

161161
The amount of spaces between each element is irrelevant (as long as there is at least one) so the matrix should be formatted for ease of reading.
162162

163-
Now that we have finished both the model and the data file save them both. It's convention to give the model file a .py extension and the data file a .dat extension.
163+
Now that we have finished both the model and the data file save them both. It's convention to give the model file a .py extension and the data file a .dat extension.
164164

165165
\subsection*{Solution}
166166

examples/doc/samples/case_studies/diet/README.txt

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ to import the Pyomo package for use in the code. The next step is to create an
1414

1515
{{{
1616
#!python
17-
model = pyo.AbstractModel()
17+
model = pyo.AbstractModel()
1818
}}}
1919
The rest of our work will be contained within this object.
2020

@@ -79,7 +79,7 @@ We restrict our domain to the non-negative reals. If we accepted negative numbe
7979

8080
At this point we must start defining the rules associated with our parameters and variables. We begin with the most important rule, the cost rule, which will tell the model to try and minimize the overall cost. Logically, the total cost is going to be the sum of how much is spent on each food, and that value in turn is going to be determined by the cost of the food and how much of it is purchased. For example, if three !$5 hamburgers and two !$1 apples are purchased, than the total cost would be 3*5 + 2*1 = 17. Note that this process is the same as taking the dot product of the amounts vector and the costs vector.
8181

82-
To input this, we must define the cost rule, which we creatively call costRule as
82+
To input this, we must define the cost rule, which we creatively call costRule as
8383

8484
{{{
8585
#!python
@@ -95,7 +95,7 @@ model.cost=pyo.Objective(rule=costRule
9595

9696
This line defines the objective of the model as the costRule, which Pyomo interprets as the value it needs to minimize; in this case it will minimize our costs. Also, as a note, we defined the objective as "model.cost" which is not to be confused with the parameter we defined earlier as `"model.costs" despite their similar names. These are two different values and accidentally giving them the same name will cause problems when trying to solve the problem.
9797

98-
We must also create a rule for the volume consumed. The construction of this rule is similar to the cost rule as once again we take the dot product, this time between the volume and amount vectors.
98+
We must also create a rule for the volume consumed. The construction of this rule is similar to the cost rule as once again we take the dot product, this time between the volume and amount vectors.
9999

100100
{{{
101101
#!python
@@ -112,7 +112,7 @@ Finally, we need to add the constraint that ensures we obtain proper amounts of
112112
{{{
113113
#!python
114114
def nutrientRule(n, model):
115-
value = sum(model.nutrient_value[n,f]*model.amount[f]
115+
value = sum(model.nutrient_value[n,f]*model.amount[f]
116116
for f in model.foods)
117117
return (model.min_nutrient[n], value, model.max_nutrient[n])
118118

@@ -179,7 +179,7 @@ vc 0 30 0;
179179

180180
The amount of spaces between each element is irrelevant (as long as there is at least one) so the matrix should be formatted for ease of reading.
181181

182-
Now that we have finished both the model and the data file save them both. It's convention to give the model file a .py extension and the data file a .dat extension.
182+
Now that we have finished both the model and the data file save them both. It's convention to give the model file a .py extension and the data file a .dat extension.
183183

184184
== Solution ==
185185

@@ -193,7 +193,7 @@ Using Pyomo we quickly find the solution to our diet problem. Simply run Pyomo
193193
# ----------------------------------------------------------
194194
# Problem Information
195195
# ----------------------------------------------------------
196-
Problem:
196+
Problem:
197197
- Lower bound: 29.44055944
198198
Upper bound: inf
199199
Number of objectives: 1
@@ -205,28 +205,28 @@ Problem:
205205
# ----------------------------------------------------------
206206
# Solver Information
207207
# ----------------------------------------------------------
208-
Solver:
208+
Solver:
209209
- Status: ok
210210
Termination condition: unknown
211211
Error rc: 0
212212

213213
# ----------------------------------------------------------
214214
# Solution Information
215215
# ----------------------------------------------------------
216-
Solution:
216+
Solution:
217217
- number of solutions: 1
218218
number of solutions displayed: 1
219219
- Gap: 0.0
220220
Status: optimal
221-
Objective:
222-
f:
221+
Objective:
222+
f:
223223
Id: 0
224224
Value: 29.44055944
225-
Variable:
226-
amount[rice]:
225+
Variable:
226+
amount[rice]:
227227
Id: 0
228228
Value: 9.44056
229-
amount[apple]:
229+
amount[apple]:
230230
Id: 2
231231
Value: 10
232232

pyomo/contrib/appsi/solvers/highs.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from pyomo.common.collections import ComponentMap
1515
from pyomo.common.dependencies import attempt_import
1616
from pyomo.common.errors import PyomoException
17+
from pyomo.common.flags import NOTSET
1718
from pyomo.common.timing import HierarchicalTimer
1819
from pyomo.common.config import ConfigValue, NonNegativeInt
1920
from pyomo.common.tee import TeeStream, capture_output
@@ -684,9 +685,13 @@ def _postsolve(self, timer: HierarchicalTimer):
684685
results.termination_condition = TerminationCondition.maxTimeLimit
685686
elif status == highspy.HighsModelStatus.kIterationLimit:
686687
results.termination_condition = TerminationCondition.maxIterations
688+
elif status == getattr(highspy.HighsModelStatus, "kSolutionLimit", NOTSET):
689+
# kSolutionLimit was introduced in HiGHS v1.5.3 for MIP-related limits
690+
results.termination_condition = TerminationCondition.maxIterations
687691
elif status == highspy.HighsModelStatus.kUnknown:
688692
results.termination_condition = TerminationCondition.unknown
689693
else:
694+
logger.warning(f'Received unhandled {status=} from solver HiGHS.')
690695
results.termination_condition = TerminationCondition.unknown
691696

692697
timer.start('load solution')

pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
from pyomo.contrib.appsi.solvers.highs import Highs
2121
from pyomo.contrib.appsi.base import TerminationCondition
2222

23+
from pyomo.contrib.solver.tests.solvers import instances
24+
2325

2426
opt = Highs()
2527
if not opt.available():
@@ -183,3 +185,10 @@ def test_warm_start(self):
183185
pyo.SolverFactory("appsi_highs").solve(m, tee=True, warmstart=True)
184186
log = output.getvalue()
185187
self.assertIn("MIP start solution is feasible, objective value is 25", log)
188+
189+
def test_node_limit_term_cond(self):
190+
opt = Highs()
191+
opt.highs_options.update({"mip_max_nodes": 1})
192+
mod = instances.multi_knapsack()
193+
res = opt.solve(mod)
194+
assert res.termination_condition == TerminationCondition.maxIterations

0 commit comments

Comments
 (0)