Skip to content

Commit ae00d1a

Browse files
authored
Merge branch 'main' into parmest-bug-fix
2 parents 8304fcf + 874352c commit ae00d1a

37 files changed

Lines changed: 1113 additions & 370 deletions

.github/workflows/test_branches.yml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,13 @@ jobs:
7676
fail-fast: false
7777
matrix:
7878
os: [ubuntu-latest]
79-
python: ['3.13.3'] # FIXME: Remove specific 3.13 version once logging tests are fixed
79+
python: [3.13]
8080
other: [""]
8181
category: [""]
8282

8383
include:
8484
- os: ubuntu-latest
85-
python: '3.13.3' # FIXME: Remove specific 3.13 version once logging tests are fixed
85+
python: 3.13
8686
test_docs: 1
8787
TARGET: linux
8888
PYENV: pip
@@ -534,9 +534,8 @@ jobs:
534534
echo "DYLD_LIBRARY_PATH=${env:DYLD_LIBRARY_PATH}:$GAMS_DIR" `
535535
Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
536536
$INSTALLER = "${env:DOWNLOAD_DIR}/gams_install.exe"
537-
# We are pinning to 29.1.0 because a license is required for
538-
# versions after this in order to run in demo mode.
539-
$URL = "https://d37drm4t2jghv5.cloudfront.net/distributions/29.1.0"
537+
# Demo licenses are included for 5mo from the newest release
538+
$URL = "https://d37drm4t2jghv5.cloudfront.net/distributions/50.1.0"
540539
if ( "${{matrix.TARGET}}" -eq "win" ) {
541540
$URL = "$URL/windows/windows_x64_64.exe"
542541
} elseif ( "${{matrix.TARGET}}" -eq "osx" ) {

.github/workflows/test_pr_and_main.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ jobs:
8888
fail-fast: false
8989
matrix:
9090
os: [ubuntu-latest, macos-latest, windows-latest]
91-
python: [ 3.9, '3.10', 3.11, 3.12, '3.13.3' ] # FIXME: Remove specific 3.13 version once logging tests are fixed
91+
python: [ 3.9, '3.10', 3.11, 3.12, 3.13 ]
9292
other: [""]
9393
category: [""]
9494

@@ -586,9 +586,8 @@ jobs:
586586
echo "DYLD_LIBRARY_PATH=${env:DYLD_LIBRARY_PATH}:$GAMS_DIR" `
587587
Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
588588
$INSTALLER = "${env:DOWNLOAD_DIR}/gams_install.exe"
589-
# We are pinning to 29.1.0 because a license is required for
590-
# versions after this in order to run in demo mode.
591-
$URL = "https://d37drm4t2jghv5.cloudfront.net/distributions/29.1.0"
589+
# Demo licenses are included for 5mo from the newest release
590+
$URL = "https://d37drm4t2jghv5.cloudfront.net/distributions/50.1.0"
592591
if ( "${{matrix.TARGET}}" -eq "win" ) {
593592
$URL = "$URL/windows/windows_x64_64.exe"
594593
} elseif ( "${{matrix.TARGET}}" -eq "osx" ) {

doc/OnlineDocs/explanation/solvers/pyros.rst

Lines changed: 70 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -963,10 +963,10 @@ Observe that the log contains the following information:
963963
:linenos:
964964
965965
==============================================================================
966-
PyROS: The Pyomo Robust Optimization Solver, v1.3.6.
967-
Pyomo version: 6.9.2
968-
Commit hash: 41cd797e0
969-
Invoked at UTC 2025-03-13T16:20:31.105320+00:00
966+
PyROS: The Pyomo Robust Optimization Solver, v1.3.8.
967+
Pyomo version: 6.9.3dev0
968+
Commit hash: unknown
969+
Invoked at UTC 2025-05-05T00:00:00.000000+00:00
970970
971971
Developed by: Natalie M. Isenberg (1), Jason A. F. Sherman (1),
972972
John D. Siirola (2), Chrysanthos E. Gounaris (1)
@@ -1025,34 +1025,33 @@ Observe that the log contains the following information:
10251025
------------------------------------------------------------------------------
10261026
Itn Objective 1-Stg Shift 2-Stg Shift #CViol Max Viol Wall Time (s)
10271027
------------------------------------------------------------------------------
1028-
0 3.5838e+07 - - 5 1.8832e+04 0.693
1029-
1 3.5838e+07 1.2289e-09 1.5876e-12 5 3.7762e+04 1.514
1030-
2 3.6129e+07 2.7244e-01 3.6878e-01 3 1.1093e+02 2.486
1031-
3 3.6269e+07 3.7352e-01 4.3227e-01 1 2.7711e+01 3.667
1032-
4 3.6285e+07 7.6526e-01 2.8426e-11 0 4.3364e-05g 6.291
1028+
0 3.5838e+07 - - 5 1.8832e+04 0.759
1029+
1 3.5838e+07 2.9329e-09 5.0030e-10 5 2.1295e+04 1.573
1030+
2 3.6285e+07 7.6526e-01 2.0398e-01 2 2.2457e+02 2.272
1031+
3 3.6285e+07 7.7212e-13 1.2525e-10 0 7.2940e-08g 5.280
10331032
------------------------------------------------------------------------------
10341033
Robust optimal solution identified.
10351034
------------------------------------------------------------------------------
10361035
Timing breakdown:
10371036
10381037
Identifier ncalls cumtime percall %
10391038
-----------------------------------------------------------
1040-
main 1 6.291 6.291 100.0
1039+
main 1 5.281 5.281 100.0
10411040
------------------------------------------------------
1042-
dr_polishing 4 0.334 0.083 5.3
1043-
global_separation 27 0.954 0.035 15.2
1044-
local_separation 135 3.046 0.023 48.4
1045-
master 5 1.027 0.205 16.3
1046-
master_feasibility 4 0.133 0.033 2.1
1047-
preprocessing 1 0.013 0.013 0.2
1048-
other n/a 0.785 n/a 12.5
1041+
dr_polishing 3 0.155 0.052 2.9
1042+
global_separation 27 1.280 0.047 24.2
1043+
local_separation 108 2.200 0.020 41.7
1044+
master 4 0.727 0.182 13.8
1045+
master_feasibility 3 0.103 0.034 1.9
1046+
preprocessing 1 0.021 0.021 0.4
1047+
other n/a 0.794 n/a 15.0
10491048
======================================================
10501049
===========================================================
10511050
10521051
------------------------------------------------------------------------------
10531052
Termination stats:
1054-
Iterations : 5
1055-
Solve time (wall s) : 6.291
1053+
Iterations : 4
1054+
Solve time (wall s) : 5.281
10561055
Final objective value : 3.6285e+07
10571056
Termination condition : pyrosTerminationCondition.robust_optimal
10581057
------------------------------------------------------------------------------
@@ -1133,6 +1132,58 @@ The constituent columns are defined in the
11331132
current iteration.
11341133

11351134

1135+
Separation Priority Ordering
1136+
----------------------------
1137+
The PyROS solver supports custom prioritization of
1138+
the separation subproblems (and, thus, the constraints)
1139+
that are automatically derived from
1140+
a given model for robust optimization.
1141+
Users may specify separation priorities through:
1142+
1143+
- (Recommended) :class:`~pyomo.core.base.suffix.Suffix` components
1144+
with local name ``pyros_separation_priority``,
1145+
declared on the model or any of its sub-blocks.
1146+
Each entry of every such
1147+
:class:`~pyomo.core.base.suffix.Suffix`
1148+
should map a
1149+
:class:`~pyomo.core.base.var.Var`
1150+
or :class:`~pyomo.core.base.constraint.Constraint`
1151+
component to a value that specifies the separation
1152+
priority of all constraints derived from that component
1153+
- The optional argument ``separation_priority_order``
1154+
to the PyROS :py:meth:`~pyomo.contrib.pyros.pyros.PyROS.solve`
1155+
method. The argument should be castable to a :py:obj:`dict`,
1156+
of which each entry maps the full name of a
1157+
:class:`~pyomo.core.base.var.Var`
1158+
or :class:`~pyomo.core.base.constraint.Constraint`
1159+
component to a value that specifies the
1160+
separation priority of all constraints
1161+
derived from that component
1162+
1163+
Specification via :class:`~pyomo.core.base.suffix.Suffix` components
1164+
takes precedence over specification via the solver argument
1165+
``separation_priority_order``.
1166+
Moreover, the precedence ordering among
1167+
:class:`~pyomo.core.base.suffix.Suffix`
1168+
components is handled by the Pyomo
1169+
:class:`~pyomo.core.base.suffix.SuffixFinder` utility.
1170+
1171+
A separation priority can be either
1172+
a (real) number (i.e., of type :py:class:`int`, :py:class:`float`, etc.)
1173+
or :py:obj:`None`.
1174+
A higher number indicates a higher priority.
1175+
The default priority for all constraints is 0.
1176+
Therefore a constraint can be prioritized [or deprioritized]
1177+
over the default by mapping the constraint to a positive [or negative] number.
1178+
In practice, critical or dominant constraints are often
1179+
prioritized over algorithmic or implied constraints.
1180+
1181+
Constraints that have been assigned a priority of :py:obj:`None`
1182+
are enforced subject to only the nominal uncertain parameter realization
1183+
provided by the user. Therefore, these constraints are not imposed robustly
1184+
and, in particular, are excluded from the separation problems.
1185+
1186+
11361187
Feedback and Reporting Issues
11371188
-------------------------------
11381189
Please provide feedback and/or report any problems by opening an issue on

pyomo/common/dependencies.py

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -443,25 +443,9 @@ def check_min_version(module, min_version):
443443
module = indicator._module
444444
else:
445445
return False
446-
if check_min_version._parser is None:
447-
try:
448-
from packaging import version as _version
449-
450-
_parser = _version.parse
451-
except ImportError:
452-
# pkg_resources is an order of magnitude slower to import than
453-
# packaging. Only use it if the preferred (but optional)
454-
# packaging library is not present
455-
from pkg_resources import parse_version as _parser
456-
check_min_version._parser = _parser
457-
else:
458-
_parser = check_min_version._parser
459446

460447
version = getattr(module, '__version__', '0.0.0')
461-
return _parser(min_version) <= _parser(version)
462-
463-
464-
check_min_version._parser = None
448+
return packaging.version.parse(min_version) <= packaging.version.parse(version)
465449

466450

467451
#
@@ -993,6 +977,12 @@ def _finalize_pympler(module, available):
993977
import pympler.muppy
994978

995979

980+
def _finalize_packaging(module, available):
981+
if available:
982+
# Import key subpackages that we will want to assume are present
983+
import packaging.version
984+
985+
996986
def _finalize_matplotlib(module, available):
997987
if not available:
998988
return
@@ -1094,6 +1084,10 @@ def _pyutilib_importer():
10941084
)
10951085
random, _ = attempt_import('random')
10961086

1087+
# Necessary for minimum version checking for other optional dependencies
1088+
packaging, packaging_available = attempt_import(
1089+
'packaging', deferred_submodules=['version'], callback=_finalize_packaging
1090+
)
10971091
# Commonly-used optional dependencies
10981092
dill, dill_available = attempt_import('dill')
10991093
mpi4py, mpi4py_available = attempt_import(

pyomo/common/download.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,9 +440,20 @@ def filter_fcn(info):
440440
info.mode &= 0o755
441441
return True
442442

443+
if sys.version_info[:2] < (3, 12):
444+
tar_args = {}
445+
else:
446+
# Add a tar filter to suppress deprecation warning in Python 3.12+
447+
#
448+
# Note that starting in Python 3.14 the default is 'data',
449+
# however, this method is already filtering for many of the
450+
# things that 'data' would catch (and raise an exception
451+
# for). For backwards compatibility, we will use the more
452+
# permissive "tar" filter.
453+
tar_args = {'filter': tarfile.tar_filter}
443454
with tarfile.open(fileobj=io.BytesIO(self.retrieve_url(url))) as TAR:
444455
dest = os.path.realpath(self._fname)
445-
TAR.extractall(dest, filter(filter_fcn, TAR.getmembers()))
456+
TAR.extractall(dest, filter(filter_fcn, TAR.getmembers()), **tar_args)
446457

447458
def get_gzipped_binary_file(self, url):
448459
if self._fname is None:

pyomo/common/errors.py

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -117,17 +117,19 @@ class ApplicationError(Exception):
117117
An exception used when an external application generates an error.
118118
"""
119119

120-
pass
121-
122120

123121
class PyomoException(Exception):
124122
"""
125123
Exception class for other Pyomo exceptions to inherit from,
126124
allowing Pyomo exceptions to be caught in a general way
127125
(e.g., in other applications that use Pyomo).
126+
Subclasses can define a class-level `default_message` attribute.
128127
"""
129128

130-
pass
129+
def __init__(self, *args):
130+
if not args and getattr(self, 'default_message', None):
131+
args = (self.default_message,)
132+
return super().__init__(*args)
131133

132134

133135
class DeferredImportError(ImportError):
@@ -137,8 +139,6 @@ class DeferredImportError(ImportError):
137139
138140
"""
139141

140-
pass
141-
142142

143143
class DeveloperError(PyomoException, NotImplementedError):
144144
"""
@@ -163,8 +163,6 @@ class InfeasibleConstraintException(PyomoException):
163163
the course of range reduction).
164164
"""
165165

166-
pass
167-
168166

169167
class IterationLimitError(PyomoException, RuntimeError):
170168
"""A subclass of :py:class:`RuntimeError`, raised by an iterative method
@@ -182,16 +180,12 @@ class IntervalException(PyomoException, ValueError):
182180
Exception class used for errors in interval arithmetic.
183181
"""
184182

185-
pass
186-
187183

188184
class InvalidValueError(PyomoException, ValueError):
189185
"""
190186
Exception class used for value errors in compiled model representations
191187
"""
192188

193-
pass
194-
195189

196190
class MouseTrap(PyomoException, NotImplementedError):
197191
"""
@@ -218,17 +212,13 @@ def __str__(self):
218212
class NondifferentiableError(PyomoException, ValueError):
219213
"""A Pyomo-specific ValueError raised for non-differentiable expressions"""
220214

221-
pass
222-
223215

224216
class TempfileContextError(PyomoException, IndexError):
225217
"""A Pyomo-specific IndexError raised when attempting to use the
226218
TempfileManager when it does not have a currently active context.
227219
228220
"""
229221

230-
pass
231-
232222

233223
class TemplateExpressionError(ValueError):
234224
"""Special ValueError raised by getitem for template arguments

pyomo/common/log.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,10 @@ def is_debug_set(logger):
7373
"""
7474
if logger.manager.disable >= _DEBUG:
7575
return False
76-
_level = logger.getEffectiveLevel()
7776
# Filter out NOTSET and higher levels
78-
return _NOTSET < _level <= _DEBUG
77+
return _NOTSET < logger.getEffectiveLevel() <= _DEBUG
7978

80-
else:
79+
elif sys.version_info[:3] < (3, 13, 4):
8180
# This is inefficient (it indirectly checks effective level twice),
8281
# but is included for [as yet unknown] platforms that ONLY implement
8382
# the API documented in the logging library
@@ -86,6 +85,15 @@ def is_debug_set(logger):
8685
return False
8786
return logger.getEffectiveLevel() > _NOTSET
8887

88+
else:
89+
# Python 3.14 (and backported to python 3.13.4) changed the behavior
90+
# of isEnabledFor() so that it always returns False when called
91+
# while a log record is in flight (learned this from
92+
# https://github.com/hynek/structlog/pull/723). In newer versions
93+
# of Python, we will only rely on getEffectiveLevel().
94+
def is_debug_set(logger):
95+
return _NOTSET < logger.getEffectiveLevel() <= _DEBUG
96+
8997

9098
class WrappingFormatter(logging.Formatter):
9199
_flag = "<<!MSG!>>"
@@ -262,7 +270,7 @@ def filter(self, record):
262270
pyomo_logger = logging.getLogger('pyomo')
263271
pyomo_handler = logging.StreamHandler(sys.stdout)
264272
pyomo_formatter = LegacyPyomoFormatter(
265-
base=PYOMO_ROOT_DIR, verbosity=lambda: pyomo_logger.isEnabledFor(logging.DEBUG)
273+
base=PYOMO_ROOT_DIR, verbosity=lambda: is_debug_set(pyomo_logger)
266274
)
267275
pyomo_handler.setFormatter(pyomo_formatter)
268276
pyomo_handler.addFilter(_GlobalLogFilter())
@@ -469,7 +477,7 @@ def __enter__(self):
469477
# #3587), so we will just handle it explicitly ourselves.
470478
self.local_fd = os.dup(self.fd)
471479
self.handler.stream = os.fdopen(
472-
self.local_fd, mode="a", closefd=False
480+
self.local_fd, mode="w", closefd=False
473481
).__enter__()
474482

475483
def __exit__(self, et, ev, tb):
@@ -495,7 +503,7 @@ def __enter__(self):
495503
# #3587), so we will just handle it explicitly ourselves.
496504
self.local_fd = os.dup(self.fd)
497505
logging.lastResort = logging.StreamHandler(
498-
os.fdopen(self.local_fd, mode="a", closefd=False).__enter__()
506+
os.fdopen(self.local_fd, mode="w", closefd=False).__enter__()
499507
)
500508

501509
def __exit__(self, et, ev, tb):

0 commit comments

Comments
 (0)