Skip to content

Commit 21bf05a

Browse files
authored
Merge branch 'main' into mindtpy-docs
2 parents 233b597 + b953cf9 commit 21bf05a

25 files changed

Lines changed: 169 additions & 39 deletions

doc/OnlineDocs/explanation/solvers/pyros/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ projects.
3131
Citing PyROS
3232
============
3333
If you use PyROS in your research,
34-
please acknowledge PyROS by citing [IAE+21]_.
34+
please acknowledge PyROS by citing [SIS+26]_ and [IAE+21]_.
3535

3636

3737
Feedback and Reporting Issues

doc/OnlineDocs/reference/bibliography.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,11 @@ Bibliography
167167
1032–1037. 2003. DOI `10.1016/S1570-7946(03)80444-3
168168
<https://doi.org/10.1016/S1570-7946(03)80444-3>`_
169169
170+
.. [SIS+26] J. A. F. Sherman, N. M. Isenberg, J. D. Siirola,
171+
and C. E. Gounaris. "PyROS: The Pyomo Robust Optimization Solver".
172+
Optimization Online, Preprint. 2026.
173+
https://optimization-online.org/2026/06/pyros-the-pyomo-robust-optimization-solver/
174+
170175
.. [TG15] F. Trespalacios and I. E. Grossmann. "Improved Big-M
171176
reformulation for generalized disjunctive programs", *Computers and
172177
Chemical Engineering*, 76, 98–103. 2015. DOI

pyomo/common/env.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ class CtypesEnviron:
393393
`os.environ` reflects the current python environment variables, and
394394
will be passed to subprocesses. However, it does not reflect the C
395395
Runtime Library (MSVCRT) environment on Windows platforms. This can
396-
be problemmatic as DLLs loaded through the CTYPES interface will see
396+
be problematic as DLLs loaded through the CTYPES interface will see
397397
the MSVCRT environment and not os.environ. This class provides a
398398
way to manage environment variables and pass changes to both
399399
os.environ and the MSVCRT runtime.

pyomo/contrib/parmest/parmest.py

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,16 +1074,13 @@ def __init__(
10741074
assert isinstance(experiment_list, list)
10751075
self.exp_list = experiment_list
10761076

1077-
# get the number of experiments
1078-
self.number_exp = _count_total_experiments(self.exp_list)
1079-
10801077
# check if the experiment has a ``get_labeled_model`` function
10811078
model = _get_labeled_model(self.exp_list[0])
10821079

10831080
# check if the model has all the required suffixes
10841081
_check_model_labels(model)
10851082

1086-
# populate keyword argument options
1083+
# check if the objective function is supplied correctly
10871084
if isinstance(obj_function, str):
10881085
try:
10891086
self.obj_function = ObjectiveType(obj_function)
@@ -1113,6 +1110,18 @@ def __init__(
11131110
f"{[e.value for e in RegularizationType]}."
11141111
)
11151112

1113+
# get the number of data points
1114+
# this is used to compute the covariance matrix for the case
1115+
# when the measurement errors are not supplied by the user
1116+
get_measurement_error = getattr(model, "measurement_error", None)
1117+
1118+
all_unknown_errors = get_measurement_error is None or all(
1119+
model.measurement_error[y_hat] is None for y_hat in model.experiment_outputs
1120+
)
1121+
1122+
if self.obj_function == ObjectiveType.SSE and all_unknown_errors:
1123+
self.number_exp = _count_total_experiments(self.exp_list)
1124+
11161125
self.tee = tee
11171126
self.diagnostic_mode = diagnostic_mode
11181127
self.solver_options = solver_options
@@ -1691,7 +1700,11 @@ def _cov_at_theta(self, method, solver, step):
16911700

16921701
# fix the value of the unknown parameters to the estimated values
16931702
for param in model.unknown_parameters:
1694-
param.fix(self.estimated_theta[param.name])
1703+
if param.is_indexed():
1704+
for idx in param:
1705+
param[idx].fix(self.estimated_theta[param[idx].name])
1706+
else:
1707+
param.fix(self.estimated_theta[param.name])
16951708

16961709
# re-solve the model with the estimated parameters
16971710
results = pyo.SolverFactory(solver).solve(model, tee=self.tee)
@@ -1720,12 +1733,6 @@ def _cov_at_theta(self, method, solver, step):
17201733
f"The sum of squared errors at the estimated parameter(s) is: {sse}"
17211734
)
17221735

1723-
# Number of data points considered
1724-
n = self.number_exp
1725-
1726-
# Extract the number of fitted parameters
1727-
l = len(self.estimated_theta)
1728-
17291736
"""Calculate covariance assuming experimental observation errors are
17301737
independent and follow a Gaussian distribution with constant variance.
17311738
@@ -1763,6 +1770,17 @@ def _cov_at_theta(self, method, solver, step):
17631770

17641771
# check if the user supplied the values of the measurement errors
17651772
if all(item is None for item in meas_error):
1773+
# Number of data points considered
1774+
n = self.number_exp
1775+
1776+
# Extract the number of fitted parameters
1777+
l = len(self.estimated_theta)
1778+
1779+
assert n > l, (
1780+
"The number of datapoints must be greater than the "
1781+
"number of parameters to estimate."
1782+
)
1783+
17661784
if cov_method == CovarianceMethod.reduced_hessian:
17671785
# in the "reduced_hessian" method, use the objective value
17681786
# to calculate the measurement error variance because this
@@ -2027,18 +2045,6 @@ def cov_est(self, method="finite_difference", solver="ipopt", step=1e-3):
20272045
if not isinstance(step, float):
20282046
raise TypeError("Expected a float for the step, e.g., 1e-2")
20292047

2030-
# number of unknown parameters
2031-
num_unknowns = max(
2032-
[
2033-
len(_expanded_unknown_parameter_info(experiment.get_labeled_model())[0])
2034-
for experiment in self.exp_list
2035-
]
2036-
)
2037-
assert self.number_exp > num_unknowns, (
2038-
"The number of datapoints must be greater than the "
2039-
"number of parameters to estimate."
2040-
)
2041-
20422048
return self._cov_at_theta(method=method, solver=solver, step=step)
20432049

20442050
def theta_est_bootstrap(

pyomo/contrib/parmest/tests/test_parmest.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2272,9 +2272,12 @@ def test_indexed_unknown_parameter_names_are_expanded_consistently(self):
22722272

22732273
self.assertEqual(pest._return_theta_names(), ["theta[a]", "theta[b]"])
22742274

2275+
@unittest.skipUnless(ipopt_available, "Test requires ipopt")
22752276
def test_cov_est_counts_expanded_indexed_unknown_parameters(self):
22762277
pest = _build_indexed_theta_estimator([(1.0, 2.0), (2.0, 4.0)])
22772278

2279+
obj, theta = pest.theta_est()
2280+
22782281
with self.assertRaisesRegex(
22792282
AssertionError,
22802283
"The number of datapoints must be greater than the number of parameters to estimate.",

pyomo/contrib/piecewise/tests/common_tests.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
import pyomo.contrib.piecewise.tests.models as models
1111
from pyomo.core import Var
1212
from pyomo.core.base import TransformationFactory
13-
from pyomo.environ import value
13+
from pyomo.core.expr.compare import assertExpressionsEqual
14+
from pyomo.environ import value, Expression
1415
from pyomo.gdp import Disjunct, Disjunction
1516

1617

@@ -85,3 +86,25 @@ def check_descend_into_expressions_objective_target(test, transformation, m=None
8586
test.check_pw_log(m)
8687
# And check that the paraboloid was *not* transformed.
8788
test.assertIsNone(m.pw_paraboloid.get_transformation_var(m.paraboloid_expr))
89+
90+
91+
def check_single_segment_no_disjunction(test, transformation, m=None):
92+
if m is None:
93+
m = models.make_single_segment_log_x_model()
94+
transform = TransformationFactory(transformation)
95+
transform.apply_to(m)
96+
97+
# A single segment doesn't require making a discrete choice, so this
98+
# should not have created any GDP components.
99+
test.assertEqual(len(list(m.component_data_objects(Disjunct))), 0)
100+
test.assertEqual(len(list(m.component_data_objects(Disjunction))), 0)
101+
# Nor should it have introduced any new Vars (such as a substitute_var
102+
# or an indicator_var)--m.x should be the only Var in the model.
103+
model_vars = list(m.component_data_objects(Var))
104+
test.assertEqual(len(model_vars), 1)
105+
test.assertIs(model_vars[0], m.x)
106+
107+
z = m.pw_log.get_transformation_var(m.log_expr)
108+
test.assertIsInstance(z, Expression)
109+
assertExpressionsEqual(test, z.expr, m.f1(m.x), places=7)
110+
test.assertIs(m.log_expr.expr, z)

pyomo/contrib/piecewise/tests/models.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def f3(x):
4545
m.x1 = Var(bounds=(0, 3))
4646
m.x2 = Var(bounds=(1, 7))
4747

48-
## apprximates paraboloid x1**2 + x2**2
48+
## approximates paraboloid x1**2 + x2**2
4949
def g1(x1, x2):
5050
return 3 * x1 + 5 * x2 - 4
5151

@@ -72,3 +72,19 @@ def c_rule(m, i):
7272
m.indexed_c = Constraint([0, 1], rule=c_rule)
7373

7474
return m
75+
76+
77+
def make_single_segment_log_x_model():
78+
m = ConcreteModel()
79+
m.x = Var(bounds=(1, 10))
80+
m.pw_log = PiecewiseLinearFunction(points=[1, 10], function=log)
81+
82+
def f1(x):
83+
return (log(10) / 9) * x - log(10) / 9
84+
85+
m.f1 = f1
86+
87+
m.log_expr = m.pw_log(m.x)
88+
m.obj = Objective(expr=m.log_expr)
89+
90+
return m

pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,11 @@ def test_descend_into_expressions_objective_target(self):
263263
self, 'contrib.piecewise.disaggregated_logarithmic'
264264
)
265265

266+
def test_single_segment_no_disjunction(self):
267+
ct.check_single_segment_no_disjunction(
268+
self, 'contrib.piecewise.disaggregated_logarithmic'
269+
)
270+
266271
# Check solution of the log(x) model
267272
@unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi is not available')
268273
@unittest.skipUnless(SolverFactory('gurobi').license_is_valid(), 'No license')

pyomo/contrib/piecewise/tests/test_incremental.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,9 @@ def test_descend_into_expressions_objective_target(self):
174174
make_log_x_model(simplices=self.ordered_simplices),
175175
)
176176

177+
def test_single_segment_no_disjunction(self):
178+
ct.check_single_segment_no_disjunction(self, 'contrib.piecewise.incremental')
179+
177180
@unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi is not available')
178181
@unittest.skipUnless(SolverFactory('gurobi').license_is_valid(), 'No license')
179182
def test_solve_log_model(self):

pyomo/contrib/piecewise/tests/test_inner_repn_gdp.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ def test_descend_into_expressions_objective_target(self):
106106
self, 'contrib.piecewise.inner_repn_gdp'
107107
)
108108

109+
def test_single_segment_no_disjunction(self):
110+
ct.check_single_segment_no_disjunction(self, 'contrib.piecewise.inner_repn_gdp')
111+
109112
@unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi is not available')
110113
@unittest.skipUnless(SolverFactory('gurobi').license_is_valid(), 'No license')
111114
def test_solve_disaggregated_convex_combo_model(self):

0 commit comments

Comments
 (0)