From f2bf664886d446ad8884d2d4e028e7a2eac958d9 Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 21 Feb 2025 01:33:23 -0500 Subject: [PATCH 01/67] Add math definitions to the `UncertaintySet` subclass docs --- pyomo/contrib/pyros/uncertainty_sets.py | 195 +++++++++++++++++++++++- 1 file changed, 192 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index a4b6ba6aa1a..1cdb2b3a430 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -955,6 +955,24 @@ class BoxSet(UncertaintySet): bounds : (N, 2) array_like Lower and upper bounds for each dimension of the set. + Notes + ----- + The :math:`n`-dimensional box set is defined by + + .. math:: + + \\left\\{ + q \\in \\mathbb{R}^n\\,| + \\,q^\\text{L} \\leq q \\leq q^\\text{U} + \\right\\} + + in which + :math:`q^\\text{L} \\in \\mathbb{R}^n` refers to + ``np.array(bounds[:, 0])``, + and + :math:`q^\\text{U} \\in \\mathbb{R}^n` refers to + ``np.array(bounds[:, 1])``. + Examples -------- 1D box set (interval): @@ -1098,6 +1116,29 @@ class CardinalitySet(UncertaintySet): may realize their maximal deviations from the origin simultaneously. + Notes + ----- + The :math:`n`-dimensional cardinality set is defined by + + .. math:: + + \\left\\{ q \\in \\mathbb{R}^n\\,\\middle| + \\,\\exists\\, \\xi \\in [0, 1]^n \\,:\\, + \\left[ + \\begin{array}{l} + q = q^0 + \\hat{q} \\circ \\xi \\\\ + \\displaystyle \\sum_{i=1}^n \\xi_i \\leq \\Gamma + \\end{array} + \\right] + \\right\\} + + in which + :math:`q^\\text{0} \\in \\mathbb{R}^n` refers to ``origin``, + the quantity :math:`\\hat{q} \\in \\mathbb{R}_{+}^n` + refers to ``positive_deviation``, + and :math:`\\Gamma \\in [0, n]` refers to ``gamma``. + The operator :math:`\\circ` denotes the element-wise product. + Examples -------- A 3D cardinality set: @@ -1354,6 +1395,23 @@ class PolyhedralSet(UncertaintySet): ``lhs_coefficients_mat @ x``, where `x` is an (N,) array representing any point in the polyhedral set. + Notes + ----- + The :math:`n`-dimensional polyhedral set is defined by + + .. math:: + + \\left\\{ + q \\in \\mathbb{R}^n\\, + \\middle| \\, A q \\leq b + \\right\\} + + in which + :math:`A \\in \\mathbb{R}^{m \\times n}` refers to + ``lhs_coefficients_mat``, + and + :math:`b \\in \\mathbb{R}^n` refers to ``rhs_vec``. + Examples -------- 2D polyhedral set with 4 defining inequalities: @@ -1577,6 +1635,28 @@ class BudgetSet(UncertaintySet): Origin of the budget set. If `None` is provided, then the origin is set to the zero vector. + Notes + ----- + The :math:`n`-dimensional budget set is defined by + + .. math:: + + \\left\\{ + q \\in \\mathbb{R}^n\\,\\middle| + \\begin{pmatrix} B \\\\ -I \\end{pmatrix} q + \\leq \\begin{pmatrix} b + Bq^0 \\\\ -q^0 \\end{pmatrix} + \\right\\} + + in which + :math:`B \\in \\{0, 1\\}^{\\ell \\times n}` refers to + ``budget_membership_mat``, + the quantity + :math:`I` denotes the :math:`n \\times n` identity matrix, + the quantity + :math:`b \\in \\mathbb{R}_{+}^\\ell` refers to ``budget_rhs``, + and + :math:`q^0 \\in \\mathbb{R}^n` refers to ``origin``. + Examples -------- 3D budget set with one budget constraint and @@ -1869,6 +1949,33 @@ class FactorModelSet(UncertaintySet): independent factors that can simultaneously attain their extreme values. + Notes + ----- + The :math:`n`-dimensional factor model set is defined by + + .. math:: + + \\left\\{ q \\in \\mathbb{R}^n\\, + \\middle| + \\, + \\exists\\, \\xi \\in [-1, 1]^n \\,:\\, + \\left[ + \\begin{array}{l} + q = q^0 + \\Psi \\xi \\\\ + \\displaystyle + \\bigg| \\sum_{i=1}^n \\xi_i \\bigg| + \\leq \\beta F + \\end{array} + \\right] + \\right\\} + + in which + :math:`q^\\text{0} \\in \\mathbb{R}^n` refers to ``origin``, + the quantity :math:`\\Psi \\in \\mathbb{R}^{n \\times F}` + refers to ``psi_mat``, + and :math:`\\beta \\in [0, 1]` refers to ``beta``. + + Examples -------- A 4D factor model set with a 2D factor space: @@ -2190,7 +2297,7 @@ def point_in_set(self, point): class AxisAlignedEllipsoidalSet(UncertaintySet): """ - An axis-aligned ellipsoid. + An axis-aligned ellipsoidal region. Parameters ---------- @@ -2199,9 +2306,37 @@ class AxisAlignedEllipsoidalSet(UncertaintySet): half_lengths : (N,) array_like Semi-axis lengths of the ellipsoid. + See Also + -------- + EllipsoidalSet : A general ellipsoidal region. + + Notes + ----- + The :math:`n`-dimensional axis-aligned ellipsoidal set is defined by + + .. math:: + + \\left\\{ + q \\in \\mathbb{R}^n\\, + \\middle|\\, + \\begin{array}{l} + \\displaystyle + \\sum_{\\substack{i = 1 \\\\ \\alpha_i > 0}}^n + \\bigg( \\frac{q_i - q_i^0}{\\alpha_i}\\bigg)^2 + \\leq 1 + \\\\ + q_i = q_i^0\\quad\\forall\\,i \\,:\\,\\alpha_i = 0 + \\end{array} + \\right\\} + + in which + :math:`q^0 \\in \\mathbb{R}^n` refers to ``center``, + and :math:`\\alpha \\in \\mathbb{R}_{+}^n` refers to + ``half_lengths``. + Examples -------- - 3D origin-centered unit hypersphere: + 3D origin-centered unit ball: >>> from pyomo.contrib.pyros import AxisAlignedEllipsoidalSet >>> sphere = AxisAlignedEllipsoidalSet( @@ -2367,7 +2502,7 @@ def set_as_constraint(self, uncertain_params=None, block=None): class EllipsoidalSet(UncertaintySet): """ - A general ellipsoid. + A general ellipsoidal region. Parameters ---------- @@ -2387,6 +2522,34 @@ class EllipsoidalSet(UncertaintySet): Exactly one of `scale` and `gaussian_conf_lvl` should be None; otherwise, an exception is raised. + See Also + -------- + AxisAlignedEllipsoidalSet : An axis-aligned ellipsoidal region. + + Notes + ----- + The :math:`n`-dimensional ellipsoidal set is defined by + + .. math:: + + \\left\\{ + q \\in \\mathbb{R}^n\\,| + \\,(q - q^0)^\\intercal P^{-1}(q - q^0) \\leq s + \\right\\} + + in which + :math:`q^0 \\in \\mathbb{R}^n` refers to ``center``, + the quantity + :math:`P \\in \\mathbb{R}^{n \\times n}` + refers to ``shape_matrix``, + and :math:`s \\geq 0` refers to ``scale``. + + The quantity :math:`s` is related to the Gaussian confidence level + (``gaussian_conf_lvl``) :math:`p \\in [0, 1)` + by :math:`s = \\chi_{n}^2(p)`, in which + :math:`\\chi_{n}^2(\\cdot)` is the quantile function + of the chi-squared distribution with :math:`n` degrees of freedom. + Examples -------- A 3D origin-centered unit ball: @@ -2727,6 +2890,19 @@ class DiscreteScenarioSet(UncertaintySet): scenarios : (M, N) array_like A sequence of `M` distinct uncertain parameter realizations. + Notes + ----- + The :math:`n`-dimensional discrete set is defined by + + .. math:: + + \\left\\{ + q^1, q^2, \\dots, q^m + \\right\\} + + in which :math:`q^i \\in \\mathbb{R}^n` + refers to ``scenarios[i - 1]`` for :math:`i = 1, 2, \\dots, m`. + Examples -------- 2D set with three scenarios: @@ -2895,6 +3071,19 @@ class IntersectionSet(UncertaintySet): an intersection. At least two uncertainty sets must be provided. All sets must be of the same dimension. + Notes + ----- + The :math:`n`-dimensional intersection set is defined by + + .. math:: + + \\mathcal{Q}_1 \\cap \\mathcal{Q}_2 \\cap \\cdots + \\cap \\mathcal{Q}_m + + in which :math:`\\mathcal{Q}_i \\subset \\mathbb{R}^n` + refers to the uncertainty set ``list(unc_sets.values())[i - 1]`` + for :math:`i = 1, 2, \\dots, m`. + Examples -------- Intersection of origin-centered 2D box (square) and 2D From 79b82bf66ca0c7c2f8e6a540f690a52cf9b15e49 Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 21 Feb 2025 01:34:38 -0500 Subject: [PATCH 02/67] Correct the `PolyhedralSet` class math definition --- pyomo/contrib/pyros/uncertainty_sets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 1cdb2b3a430..e84bd71edc3 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -1410,7 +1410,7 @@ class PolyhedralSet(UncertaintySet): :math:`A \\in \\mathbb{R}^{m \\times n}` refers to ``lhs_coefficients_mat``, and - :math:`b \\in \\mathbb{R}^n` refers to ``rhs_vec``. + :math:`b \\in \\mathbb{R}^m` refers to ``rhs_vec``. Examples -------- From 5dea0be52b3a95a8d9e400c07ed9bc38c683c45f Mon Sep 17 00:00:00 2001 From: jasherma Date: Sat, 22 Feb 2025 03:00:53 -0500 Subject: [PATCH 03/67] Fix `AxisAlignedEllipsoidalSet` doc example --- pyomo/contrib/pyros/uncertainty_sets.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index e84bd71edc3..1734d1057d8 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -2339,13 +2339,13 @@ class AxisAlignedEllipsoidalSet(UncertaintySet): 3D origin-centered unit ball: >>> from pyomo.contrib.pyros import AxisAlignedEllipsoidalSet - >>> sphere = AxisAlignedEllipsoidalSet( + >>> ball = AxisAlignedEllipsoidalSet( ... center=[0, 0, 0], - ... half_lengths=[1, 1, 1] + ... half_lengths=[1, 1, 1], ... ) - >>> sphere.center + >>> ball.center array([0, 0, 0]) - >>> sphere.half_lengths + >>> ball.half_lengths array([1, 1, 1]) """ From 36e846d8fc78bf0ff19f5923892901321140b509 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sat, 22 Feb 2025 04:53:24 -0500 Subject: [PATCH 04/67] Update `UncertaintySet` class docstring --- pyomo/contrib/pyros/uncertainty_sets.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 1734d1057d8..52871ea5c11 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -472,8 +472,12 @@ class UncertaintySet(object, metaclass=abc.ABCMeta): An object representing an uncertainty set to be passed to the PyROS solver. - An `UncertaintySet` object should be viewed as merely a container - for data needed to parameterize the set it represents, + `UncertaintySet` is an abstract base class for constructing + specific subclasses and instances of uncertainty sets. + Therefore, `UncertaintySet` cannot be instantiated directly. + + A concrete `UncertaintySet` instance should be viewed as merely + a container for data needed to parameterize the set it represents, such that the object's attributes do not reference the components of a Pyomo modeling object. """ From 51837b98f1772d70e2a20f0e0860a1216255b302 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sat, 22 Feb 2025 05:19:38 -0500 Subject: [PATCH 05/67] Add draft of restructured PyROS docs --- .../solvers/pyrosdir/getting_started.rst | 748 ++++++++++++++++++ .../explanation/solvers/pyrosdir/index.rst | 38 + .../explanation/solvers/pyrosdir/overview.rst | 106 +++ .../solvers/pyrosdir/solver_interface.rst | 30 + .../solvers/pyrosdir/solver_log.rst | 319 ++++++++ .../solvers/pyrosdir/uncertainty_sets.rst | 72 ++ 6 files changed, 1313 insertions(+) create mode 100644 doc/OnlineDocs/explanation/solvers/pyrosdir/getting_started.rst create mode 100644 doc/OnlineDocs/explanation/solvers/pyrosdir/index.rst create mode 100644 doc/OnlineDocs/explanation/solvers/pyrosdir/overview.rst create mode 100644 doc/OnlineDocs/explanation/solvers/pyrosdir/solver_interface.rst create mode 100644 doc/OnlineDocs/explanation/solvers/pyrosdir/solver_log.rst create mode 100644 doc/OnlineDocs/explanation/solvers/pyrosdir/uncertainty_sets.rst diff --git a/doc/OnlineDocs/explanation/solvers/pyrosdir/getting_started.rst b/doc/OnlineDocs/explanation/solvers/pyrosdir/getting_started.rst new file mode 100644 index 00000000000..fcb3d3406bd --- /dev/null +++ b/doc/OnlineDocs/explanation/solvers/pyrosdir/getting_started.rst @@ -0,0 +1,748 @@ +.. _pyros_installation: + +Getting Started with PyROS +========================== + +.. contents:: Table of Contents + :depth: 2 + :local: + + +Installation +------------ +PyROS can be installed as follows: + +1. :ref:`Install Pyomo `. + PyROS is included in the Pyomo software package, at pyomo/contrib/pyros. +2. Install NumPy and SciPy with your preferred package manager; + both NumPy and SciPy are required dependencies of PyROS. + You may install NumPy and SciPy with, for example, ``conda``: + + :: + + conda install numpy scipy + + or ``pip``: + + :: + + pip install numpy scipy +3. (*Optional*) Test your installation: + install ``pytest`` and ``parameterized`` + with your preferred package manager (as in the previous step): + + :: + + pip install pytest parameterized + + You may then run the PyROS tests as follows: + + :: + + python -c 'import os, pytest, pyomo.contrib.pyros as p; pytest.main([os.path.dirname(p.__file__)])' + + Some tests involving solvers may fail or be skipped, + depending on the solver distributions (e.g., Ipopt, BARON, SCIP) + that you have pre-installed and licensed on your system. + +Usage Tutorial +-------------- +This tutorial shows how to solve robust optimization problems with PyROS. +The problems are derived from the deterministic model *hydro*, +a QCQP taken from the +`GAMS Model Library `_. +We have converted the +`GAMS implementation of hydro `_ +to Pyomo format using the +`GAMS CONVERT utility `_. + +The model *hydro* features 31 variables, +13 of which are considered independent variables, +and the remaining 18 of which are considered state variables. +Moreover, there are +6 linear inequality constraints, +12 linear equality constraints, +6 non-linear (quadratic) equality constraints, +and a quadratic objective. +We have extended this model by converting one objective coefficient, +two constraint coefficients, and one constraint right-hand side +into :class:`~pyomo.core.base.param.Param` objects +so that they can be considered uncertain later on. + +.. note:: + Per our analysis, the model *hydro* satisfies the requirement that + each value of :math:`\left(x, z, q \right)` maps to a unique + value of :math:`y`, which, in accordance with + :ref:`our assumption on the equality constraints `, + indicates a proper partitioning of the model variables + into (first-stage and second-stage) degrees of freedom and + state variables. + +Step 0: Import Pyomo and the PyROS Module +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In anticipation of using the PyROS solver and building the deterministic Pyomo +model: + +.. _pyros_module_imports: + +.. doctest:: + + >>> import pyomo.environ as pyo + >>> import pyomo.contrib.pyros as pyros + +Step 1: Define the Deterministic Problem +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The deterministic Pyomo model for *hydro* is constructed as follows. +We first construct the main model object: + +.. _pyros_model_construct: + +.. doctest:: + + >>> m = pyo.ConcreteModel() + >>> m.name = "hydro" + +Some constants of the model are later considered uncertain. +These are represented by mutable :class:`~pyomo.core.base.param.Param` objects: + +.. doctest:: + + >>> nominal_values = {0: 82.8*0.0016, 1: 4.97, 2: 4.97, 3: 1800} + >>> m.q = pyo.Param( + ... list(nominal_values), + ... initialize=nominal_values, + ... mutable=True, + ... ) + +.. note:: + Primitive data (Python literals) that have been hard-coded within a + deterministic model (:class:`~pyomo.core.base.PyomoModel.ConcreteModel`) + cannot be later considered uncertain, + unless they are first converted to Pyomo + :class:`~pyomo.core.base.param.Param` instances declared on the + :class:`~pyomo.core.base.PyomoModel.ConcreteModel` object. + Furthermore, any :class:`~pyomo.core.base.param.Param` + object that is to be later considered uncertain must be instantiated + with the argument ``mutable=True``. + +.. note:: + If specifying/modifying the ``mutable`` argument in the + :class:`~pyomo.core.base.param.Param` declarations + of your deterministic model source code + is not straightforward in your context, then + you may consider adding **after** the + :ref:`Pyomo/PyROS module imports ` + but **before** + :ref:`instantiating the model object ` + the statement: + + .. code:: + + pyo.Param.DefaultMutable = True + + For all :class:`~pyomo.core.base.param.Param` + objects declared after this statement, + the attribute ``mutable`` is set to True by default. + Hence, non-mutable :class:`~pyomo.core.base.param.Param` + objects are now declared by explicitly passing the argument + ``mutable=False`` to the :class:`~pyomo.core.base.param.Param` + constructor. + + +Finally, we declare the decision variables, objective, and constraints: + +.. doctest:: + + >>> # declare variables + >>> m.x1 = pyo.Var(within=pyo.Reals, bounds=(150, 1500), initialize=150) + >>> m.x2 = pyo.Var(within=pyo.Reals, bounds=(150, 1500), initialize=150) + >>> m.x3 = pyo.Var(within=pyo.Reals, bounds=(150, 1500), initialize=150) + >>> m.x4 = pyo.Var(within=pyo.Reals, bounds=(150, 1500), initialize=150) + >>> m.x5 = pyo.Var(within=pyo.Reals, bounds=(150, 1500), initialize=150) + >>> m.x6 = pyo.Var(within=pyo.Reals, bounds=(150, 1500), initialize=150) + >>> m.x7 = pyo.Var(within=pyo.Reals, bounds=(0, 1000), initialize=0) + >>> m.x8 = pyo.Var(within=pyo.Reals, bounds=(0, 1000), initialize=0) + >>> m.x9 = pyo.Var(within=pyo.Reals, bounds=(0, 1000), initialize=0) + >>> m.x10 = pyo.Var(within=pyo.Reals, bounds=(0, 1000), initialize=0) + >>> m.x11 = pyo.Var(within=pyo.Reals, bounds=(0, 1000), initialize=0) + >>> m.x12 = pyo.Var(within=pyo.Reals, bounds=(0, 1000), initialize=0) + >>> m.x13 = pyo.Var(within=pyo.Reals, bounds=(0, None), initialize=0) + >>> m.x14 = pyo.Var(within=pyo.Reals, bounds=(0, None), initialize=0) + >>> m.x15 = pyo.Var(within=pyo.Reals, bounds=(0, None), initialize=0) + >>> m.x16 = pyo.Var(within=pyo.Reals, bounds=(0, None), initialize=0) + >>> m.x17 = pyo.Var(within=pyo.Reals, bounds=(0, None), initialize=0) + >>> m.x18 = pyo.Var(within=pyo.Reals, bounds=(0, None), initialize=0) + >>> m.x19 = pyo.Var(within=pyo.Reals, bounds=(0, None), initialize=0) + >>> m.x20 = pyo.Var(within=pyo.Reals, bounds=(0, None), initialize=0) + >>> m.x21 = pyo.Var(within=pyo.Reals, bounds=(0, None), initialize=0) + >>> m.x22 = pyo.Var(within=pyo.Reals, bounds=(0, None), initialize=0) + >>> m.x23 = pyo.Var(within=pyo.Reals, bounds=(0, None), initialize=0) + >>> m.x24 = pyo.Var(within=pyo.Reals, bounds=(0, None), initialize=0) + >>> m.x25 = pyo.Var(within=pyo.Reals, bounds=(100000, 100000), initialize=100000) + >>> m.x26 = pyo.Var(within=pyo.Reals, bounds=(60000, 120000), initialize=60000) + >>> m.x27 = pyo.Var(within=pyo.Reals, bounds=(60000, 120000), initialize=60000) + >>> m.x28 = pyo.Var(within=pyo.Reals, bounds=(60000, 120000), initialize=60000) + >>> m.x29 = pyo.Var(within=pyo.Reals, bounds=(60000, 120000), initialize=60000) + >>> m.x30 = pyo.Var(within=pyo.Reals, bounds=(60000, 120000), initialize=60000) + >>> m.x31 = pyo.Var(within=pyo.Reals, bounds=(60000, 120000), initialize=60000) + >>> + >>> # declare objective + >>> m.obj = pyo.Objective( + ... expr=( + ... m.q[0]*m.x1**2 + 82.8*8*m.x1 + 82.8*0.0016*m.x2**2 + ... + 82.8*82.8*8*m.x2 + 82.8*0.0016*m.x3**2 + 82.8*8*m.x3 + ... + 82.8*0.0016*m.x4**2 + 82.8*8*m.x4 + 82.8*0.0016*m.x5**2 + ... + 82.8*8*m.x5 + 82.8*0.0016*m.x6**2 + 82.8*8*m.x6 + 248400 + ... ), + ... sense=pyo.minimize, + ... ) + >>> + >>> # declare constraints + >>> m.c2 = pyo.Constraint(expr=-m.x1 - m.x7 + m.x13 + 1200<= 0) + >>> m.c3 = pyo.Constraint(expr=-m.x2 - m.x8 + m.x14 + 1500 <= 0) + >>> m.c4 = pyo.Constraint(expr=-m.x3 - m.x9 + m.x15 + 1100 <= 0) + >>> m.c5 = pyo.Constraint(expr=-m.x4 - m.x10 + m.x16 + m.q[3] <= 0) + >>> m.c6 = pyo.Constraint(expr=-m.x5 - m.x11 + m.x17 + 950 <= 0) + >>> m.c7 = pyo.Constraint(expr=-m.x6 - m.x12 + m.x18 + 1300 <= 0) + >>> m.c8 = pyo.Constraint(expr=12*m.x19 - m.x25 + m.x26 == 24000) + >>> m.c9 = pyo.Constraint(expr=12*m.x20 - m.x26 + m.x27 == 24000) + >>> m.c10 = pyo.Constraint(expr=12*m.x21 - m.x27 + m.x28 == 24000) + >>> m.c11 = pyo.Constraint(expr=12*m.x22 - m.x28 + m.x29 == 24000) + >>> m.c12 = pyo.Constraint(expr=12*m.x23 - m.x29 + m.x30 == 24000) + >>> m.c13 = pyo.Constraint(expr=12*m.x24 - m.x30 + m.x31 == 24000) + >>> m.c14 = pyo.Constraint(expr=-8e-5*m.x7**2 + m.x13 == 0) + >>> m.c15 = pyo.Constraint(expr=-8e-5*m.x8**2 + m.x14 == 0) + >>> m.c16 = pyo.Constraint(expr=-8e-5*m.x9**2 + m.x15 == 0) + >>> m.c17 = pyo.Constraint(expr=-8e-5*m.x10**2 + m.x16 == 0) + >>> m.c18 = pyo.Constraint(expr=-8e-5*m.x11**2 + m.x17 == 0) + >>> m.c19 = pyo.Constraint(expr=-8e-5*m.x12**2 + m.x18 == 0) + >>> m.c20 = pyo.Constraint(expr=-4.97*m.x7 + m.x19 == 330) + >>> m.c21 = pyo.Constraint(expr=-m.q[1]*m.x8 + m.x20 == 330) + >>> m.c22 = pyo.Constraint(expr=-4.97*m.x9 + m.x21 == 330) + >>> m.c23 = pyo.Constraint(expr=-4.97*m.x10 + m.x22 == 330) + >>> m.c24 = pyo.Constraint(expr=-m.q[2]*m.x11 + m.x23 == 330) + >>> m.c25 = pyo.Constraint(expr=-4.97*m.x12 + m.x24 == 330) + + +Before moving on, we check that the model can be solved to optimality +with a deterministic nonlinear programming (NLP) solver. +For convenience, we use BARON as the solver: + +.. _pyros_solve_deterministic: + +.. doctest:: + :skipif: not (baron.available() and baron.license_is_valid()) + + >>> baron = pyo.SolverFactory("baron") + >>> pyo.assert_optimal_termination(baron.solve(m)) + >>> deterministic_obj = pyo.value(m.obj) + >>> print("Optimal deterministic objective value: {deterministic_obj:.4e}") + Optimal deterministic objective value: 3.5838e+07 + + +Step 2: Define the Uncertainty Quantification +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +We first collect the components of our model that represent the +uncertain parameters. +In this example, we assume that the quantities +represented by ``m.q[0]``, ``m.q[1]``, ``m.q[2]``, and ``m.q[3]`` +are the uncertain parameters. +Since these objects comprise the mutable :class:`~pyomo.core.base.param.Param` +object ``m.q``, we can conveniently specify: + +.. doctest:: + + >>> uncertain_params = m.q + +Equivalently, we may instead set ``uncertain_params`` to +one of the following: + +* ``[m.q]`` +* ``[m.q[0], m.q[1], m.q[2], m.q[3]]`` +* ``list(m.q.values())`` + +.. note:: + Any :class:`~pyomo.core.base.param.Param` object that is + to be considered uncertain by PyROS must have the property + ``mutable=True``. + +.. note:: + PyROS also allows uncertain parameters to be implemented as + :class:`~pyomo.core.base.var.Var` objects declared on the + deterministic model. + This may be convenient for users transitioning to PyROS from + parameter estimation and/or uncertainty quantification workflows, + in which the uncertain parameters are + often represented by :class:`~pyomo.core.base.var.Var` objects. + Prior to invoking PyROS, + all such :class:`~pyomo.core.base.var.Var` objects should be fixed. + + +PyROS requires an uncertainty set against which to robustly +optimize the model. +The goal is to identify a solution to the model that remains feasible +subject to any uncertain parameter realization located within +the uncertainty set. +In PyROS, an uncertainty set is represented by +an instance of a subclass of the +:class:`~pyomo.contrib.pyros.uncertainty_sets.UncertaintySet` class. + +Let us assume in the present example that each uncertain parameter can +independently of the other uncertain parameters +deviate from the parameter's nominal value by up to :math:`\pm 15\%`. +The resulting uncertainty set is a box, which we can implement +as an instance of the +:class:`~pyomo.contrib.pyros.uncertainty_sets.BoxSet` subclass: + +.. doctest:: + + >>> relative_deviation = 0.15 + >>> box_uncertainty_set = pyros.BoxSet(bounds=[ + ... (val * (1 - relative_deviation), val * (1 + relative_deviation)) + ... for val in nominal_values.values() + ... ]) + +Further information on PyROS uncertainty sets is presented in the +:ref:`Uncertainty Sets section `. + + +Step 3: Solve With PyROS +^^^^^^^^^^^^^^^^^^^^^^^^^^ +PyROS can be instantiated through the Pyomo +:class:`~pyomo.opt.base.solvers.SolverFactory`: + +.. doctest:: + + >>> pyros_solver = pyo.SolverFactory("pyros") + +PyROS requires the user to supply one subordinate local NLP optimizer +and one subordinate global NLP optimizer for solving subproblems. +For convenience, we shall have PyROS use +:ref:`the previously instantiated BARON solver ` +as both the subordinate local and global NLP solvers: + +.. doctest:: + :skipif: not (baron.available() and baron.license_is_valid()) + + >>> local_solver = baron + >>> global_solver = baron + +.. note:: + Additional NLP optimizers can be automatically used in the event the primary + subordinate local or global optimizer passed + to the PyROS :meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` method + does not successfully solve a subproblem to an appropriate termination + condition. These alternative solvers are provided through the optional + keyword arguments ``backup_local_solvers`` and ``backup_global_solvers``. + +The final step in solving a model with PyROS is to construct the +remaining required inputs, namely +``first_stage_variables`` and ``second_stage_variables``. +Below, we present two separate cases. + + +A Single-Stage Problem +""""""""""""""""""""""""" +We can use PyROS to solve a single-stage robust optimization problem, +in which all independent variables are designated to be first-stage. +In the present example, the independent variables are +taken to be ``m.x1`` through ``m.x6``, ``m.x19`` through ``m.x24``, and ``m.x31``. +So our variable designation is as follows: + +.. doctest:: + :skipif: not (baron.available() and baron.license_is_valid()) + + >>> first_stage_variables = [ + ... m.x1, m.x2, m.x3, m.x4, m.x5, m.x6, + ... m.x19, m.x20, m.x21, m.x22, m.x23, m.x24, m.x31, + ... ] + >>> second_stage_variables = [] + +The single-stage problem can now be solved +:ref:`to robust optimality ` +by invoking the :meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` +method of the PyROS solver object, as follows: + +.. _single-stage-problem: + +.. doctest:: + :skipif: not (baron.available() and baron.license_is_valid()) + + >>> results_1 = pyros_solver.solve( + ... model=m, + ... first_stage_variables=first_stage_variables, + ... second_stage_variables=second_stage_variables, + ... uncertain_params=uncertain_params, + ... uncertainty_set=box_uncertainty_set, + ... local_solver=local_solver, + ... global_solver=global_solver, + ... objective_focus=pyros.ObjectiveType.worst_case, + ... solve_master_globally=True, + ... ) + ============================================================================== + PyROS: The Pyomo Robust Optimization Solver... + ... + ------------------------------------------------------------------------------ + Robust optimal solution identified. + ... + Termination stats: + Iterations : 6 + Solve time (wall s) : 2.841 + Final objective value : 4.8367e+07 + Termination condition : pyrosTerminationCondition.robust_optimal + ------------------------------------------------------------------------------ + All done. Exiting PyROS. + ============================================================================== + +PyROS (by default) logs to the output console the progress of the optimization +and, upon termination, a summary of the final result. +The summary includes the iteration and solve time requirements, +the final objective function value, and the termination condition. +For further information on the output log, +see the :ref:`Solver Output Log section `. + +A Two-Stage Problem +"""""""""""""""""""""" +Let us now assume that some of the independent variables are second-stage: + +.. doctest:: + :skipif: not (baron.available() and baron.license_is_valid()) + + >>> first_stage_variables = [m.x5, m.x6, m.x19, m.x22, m.x23, m.x24, m.x31] + >>> second_stage_variables = [m.x1, m.x2, m.x3, m.x4, m.x20, m.x21] + + +PyROS uses polynomial decision rules to approximate the adjustability +of the second-stage variables to the uncertain parameters. +The degree of the decision rule polynomials is +specified through the optional keyword argument +``decision_rule_order`` to the PyROS +:meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` method. +In this example, we elect to use affine decision rules by +specifying ``decision_rule_order=1``. +Thus, we can solve the resulting two-stage problem +:ref:`to robust optimality ` +as follows: + +.. _example-two-stg: + +.. doctest:: + :skipif: not (baron.available() and baron.license_is_valid()) + + >>> results_2 = pyros_solver.solve( + ... model=m, + ... first_stage_variables=first_stage_variables, + ... second_stage_variables=second_stage_variables, + ... uncertain_params=uncertain_params, + ... uncertainty_set=box_uncertainty_set, + ... local_solver=local_solver, + ... global_solver=global_solver, + ... objective_focus=pyros.ObjectiveType.worst_case, + ... solve_master_globally=True, + ... decision_rule_order=1, + ... ) + ============================================================================== + PyROS: The Pyomo Robust Optimization Solver... + ... + ------------------------------------------------------------------------------ + Robust optimal solution identified. + ... + Termination stats: + Iterations : 5 + Solve time (wall s) : 6.336 + Final objective value : 3.6285e+07 + Termination condition : pyrosTerminationCondition.robust_optimal + ------------------------------------------------------------------------------ + All done. Exiting PyROS. + ============================================================================== + + +Specifying Arguments Indirectly Through ``options`` +""""""""""""""""""""""""""""""""""""""""""""""""""" +Like other Pyomo solver interface methods, +:meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` +provides support for specifying options indirectly by passing +a keyword argument ``options``, whose value must be a :class:`dict` +mapping names of arguments to :meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` +to their desired values. +For example, the ``solve()`` statement in the +:ref:`two-stage problem snippet ` +could have been equivalently written as: + +.. doctest:: + :skipif: not (baron.available() and baron.license_is_valid()) + + >>> results_2 = pyros_solver.solve( + ... model=m, + ... first_stage_variables=first_stage_variables, + ... second_stage_variables=second_stage_variables, + ... uncertain_params=uncertain_params, + ... uncertainty_set=box_uncertainty_set, + ... local_solver=local_solver, + ... global_solver=global_solver, + ... options={ + ... "objective_focus": pyros.ObjectiveType.worst_case, + ... "solve_master_globally": True, + ... "decision_rule_order": 1, + ... }, + ... ) + ============================================================================== + PyROS: The Pyomo Robust Optimization Solver... + ... + ------------------------------------------------------------------------------ + Robust optimal solution identified. + ------------------------------------------------------------------------------ + ... + Termination stats: + Iterations : 5 + Solve time (wall s) : 6.336 + Final objective value : 3.6285e+07 + Termination condition : pyrosTerminationCondition.robust_optimal + ------------------------------------------------------------------------------ + All done. Exiting PyROS. + ============================================================================== + + +In the event an argument is passed directly +by position or keyword, *and* indirectly through ``options``, +the value passed directly takes precedence over the value +passed through ``options``. + + +Step 4: Check the Results +^^^^^^^^^^^^^^^^^^^^^^^^^^^ +The PyROS :meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` method +returns a results object, +of type :class:`~pyomo.contrib.pyros.solve_data.ROSolveResults`, +that summarizes the outcome of invoking PyROS on a robust optimization problem. +By default, a printout of the results object is included at the end of the solver +output log. +Alternatively, we can display the results object ourselves using: + +.. code:: + + >>> print(results_2) + Termination stats: + Iterations : 5 + Solve time (wall s) : 6.336 + Final objective value : 3.6285e+07 + Termination condition : pyrosTerminationCondition.robust_optimal + +We can also query the results object's individual attributes: + +.. code:: + + >>> results_2.iterations # total number of iterations + 5 + >>> results_2.time # total wall-clock time (in seconds); may vary + 6.336 + >>> results_2.final_objective_value # final objective value; may vary + 36285242.22224089 + >>> results_2.pyros_termination_condition # termination condition + pyrosTerminationCondition.robust_optimal + + +The ``pyros_termination_condition`` attribute of the resuls object +is a member of the +:class:`~pyomo.contrib.pyros.util.pyrosTerminationCondition` enumeration. + +.. _pyros_robust_optimality_args: + +.. note:: + + When the PyROS :meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` method + has successfully solved a given robust optimization problem, + the :attr:`~pyomo.contrib.pyros.solve_data.ROSolveResults.pyros_termination_condition` + attribute of the returned + :class:`~pyomo.contrib.pyros.solve_data.ROSolveResults` + object is set to + :attr:`~pyomo.contrib.pyros.util.pyrosTerminationCondition.robust_optimal` + only if: + + 1. Master problems are solved to global optimality + (by specifying ``solve_master_globally=True``) + 2. A worst-case objective focus is chosen + (by specifying ``objective_focus=pyros.ObjectiveType.worst_case``) + + Otherwise, the termination condition is set to + :attr:`~pyomo.contrib.pyros.util.pyrosTerminationCondition.robust_feasible`. + +.. note:: + + The reported objective and variable values + depend on the value of the option ``objective_focus``: + + * If ``objective_focus=pyros.ObjectiveType.nominal``, + then the objective, second-stage variables, and + state variables are evaluated at + the nominal uncertain parameter realization. + * If ``objective_focus=pyros.ObjectiveType.worst_case``, + then the objective, second-stage variables, and + state variables are evaluated at + the worst-case uncertain parameter realization. + + +We expect that adding second-stage recourse to the +single-stage *hydro* problem results in +a reduction in the robust optimal objective value. +To confirm our expectation, the final objectives can be compared as follows: + +.. doctest:: + :skipif: not (baron.available() and baron.license_is_valid()) + + >>> single_stage_final_objective = pyo.value(results_1.final_objective_value) + >>> two_stage_final_objective = pyo.value(results_2.final_objective_value) + >>> relative_obj_decrease = ( + ... (single_stage_final_objective - two_stage_final_objective) + ... / single_stage_final_objective + ... ) + >>> print( + ... "Percentage decrease (relative to single-stage problem objective): " + ... f"{100 * relative_obj_decrease:.2f}" + ... ) + Percentage decrease (relative to single-stage problem objective): 24.98 + + +Our check confirms that there is a ~25% decrease in the final objective +value when switching from a static decision rule +(no second-stage recourse) to an affine decision rule. + +We can also inspect the state of the model after the solution +has been loaded by invoking ``m.display()`` or ``m.pprint()``. + +.. note:: + + PyROS loads the final solution to the deterministic model only if: + + 1. The argument ``load_solution=True`` has been passed to PyROS + (occurs by default) + 2. The termination condition is either + :attr:`~pyomo.contrib.pyros.util.pyrosTerminationCondition.robust_optimal` + or + :attr:`~pyomo.contrib.pyros.util.pyrosTerminationCondition.robust_feasible` + + Otherwise, the final solution is lost. + + +Analyzing the Price of Robustness +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +PyROS facilitates an analysis of the "price of robustness", +which we define to be the increase in the robust optimal objective value +relative to the deterministically optimal objective value. +The deterministically optimal objective can be obtained as follows: + +.. code:: + + >>> pyo.assert_optimal_termination(global_solver.solve(m)) + >>> deterministic_obj = pyo.value(m.obj) + + +Let us, for example, consider optimizing robustly against a +box uncertainty set centered on the nominal realization +of the uncertain parameters +and parameterized by a value :math:`p \geq 0` +specifying the half-lengths of the box relative to the nominal realization. +Then the box set is defined by: + +.. math:: + + \{q \in \mathbb{R}^4 \,|\, (1 - p)q^\text{nom} \leq q \leq (1 + p)q^\text{nom} \} + +in which :math:`q^\text{nom}` denotes the nominal realization. +We can optimize against box sets of increasing +normalized half-length :math:`p` +by constructing a corresponding +:class:`~pyomo.contrib.pyros.uncertainty_sets.BoxSet` +instance and invoking the +:meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` method +in a for-loop: + +.. code:: + + >>> results_dict = dict() + >>> for half_length in [0.0, 0.1, 0.2, 0.3, 0.4]: + ... print(f"Solving problem for {relative_deviation=}:") + ... box_uncertainty_set = pyros.BoxSet(bounds=[ + ... (val * (1 - half_length), val * (1 + half_length)) + ... for val in nominal_values.values() + ... ]) + ... results_dict[half_length] = pyros_solver.solve( + ... model=m, + ... first_stage_variables=first_stage_variables, + ... second_stage_variables=second_stage_variables, + ... uncertain_params=uncertain_params, + ... uncertainty_set=box_uncertainty_set, + ... local_solver=local_solver, + ... global_solver=pyo.SolverFactory("scip"), + ... objective_focus=pyros.ObjectiveType.worst_case, + ... solve_master_globally=True, + ... decision_rule_order=1, + ... ) + >>> print("All done.") + Solving problem for relative_deviation=0.0: + ... + Solving problem for relative_deviation=0.1: + ... + Solving problem for relative_deviation=0.2: + ... + Solving problem for relative_deviation=0.3: + ... + Solving problem for relative_deviation=0.4 + ... + All done. + +Using the :py:obj:`dict` populated in the loop, +we can print a summary of the results: + +.. code:: + + >>> print("=" * 80) + >>> print( + ... f"{'Relative Half-Len.':20s}", + ... f"{'Termination Cond.':20s}", + ... f"{'Objective Value':20s}", + ... f"{'Price of Rob. (%)':20s}", + ... ) + >>> print("-" * 80) + >>> for half_length, res in results_dict.items(): + ... obj_value, percent_obj_increase = float("nan"), float("nan") + ... is_robust_optimal = ( + ... res.pyros_termination_condition + ... == pyros.pyrosTerminationCondition.robust_optimal + ... ) + ... if is_robust_optimal: + ... obj_value = res.final_objective_value + ... price_of_robustness = ( + ... (res.final_objective_value - deterministic_obj) + ... / deterministic_obj + ... ) + ... print( + ... f"{deviation:<20.1f}", + ... f"{res.pyros_termination_condition.name:20s}", + ... f"{obj_value:<20.4e}", + ... f"{100 * price_of_robustness:<20.2f}", + ... ) + >>> print("=" * 80) + ==================================================================================== + Relative Half-Len. Termination Cond. Objective Value Price of Rob. (%) + ------------------------------------------------------------------------------------ + 0.0 robust_optimal 3.5838e+07 0.00 + 0.1 robust_optimal 3.6134e+07 0.83 + 0.2 robust_optimal 3.6437e+07 1.67 + 0.3 robust_optimal 4.3478e+07 21.32 + 0.4 robust_infeasible nan nan + ==================================================================================== + + +The summary table gives the PyROS termination condition, +final objective value, and relative increase in objective value +(compared to the deterministically optimal value), +for each relative deviation. + +This example clearly illustrates the potential impact of the uncertainty +set size on the robust optimal objective function value +and demonstrates the ease of implementing a price of robustness study +for a given optimization problem under uncertainty. diff --git a/doc/OnlineDocs/explanation/solvers/pyrosdir/index.rst b/doc/OnlineDocs/explanation/solvers/pyrosdir/index.rst new file mode 100644 index 00000000000..c1bd53b75eb --- /dev/null +++ b/doc/OnlineDocs/explanation/solvers/pyrosdir/index.rst @@ -0,0 +1,38 @@ +PyROS Solver +============ + +Introduction +------------ + +PyROS (Pyomo Robust Optimization Solver) is a Pyomo-based meta-solver +for non-convex, two-stage adjustable robust optimization problems. + +It was developed by **Natalie M. Isenberg**, **Jason A. F. Sherman**, +and **Chrysanthos E. Gounaris** of Carnegie Mellon University, +in collaboration with **John D. Siirola** of Sandia National Labs. +The developers gratefully acknowledge support from the U.S. Department of Energy's +`Institute for the Design of Advanced Energy Systems (IDAES) `_. + + +Contents +-------- + +.. toctree:: + :maxdepth: 1 + + Overview + Getting Started + Solver Interface + Uncertainty Sets + Solver Output Log + + +Citing PyROS +------------ +If you use PyROS, please consider citing the publication [IAE+21]_. + + +Feedback and Reporting Issues +----------------------------- +Please provide feedback and/or report any problems by +`opening an issue on the Pyomo GitHub page `_. diff --git a/doc/OnlineDocs/explanation/solvers/pyrosdir/overview.rst b/doc/OnlineDocs/explanation/solvers/pyrosdir/overview.rst new file mode 100644 index 00000000000..8090cdde86c --- /dev/null +++ b/doc/OnlineDocs/explanation/solvers/pyrosdir/overview.rst @@ -0,0 +1,106 @@ +.. _pyros_overview: + +PyROS Overview +============== + +PyROS (Pyomo Robust Optimization Solver) is a Pyomo-based meta-solver +for non-convex, two-stage adjustable robust optimization problems. + +PyROS can accommodate optimization models with: + +* **Continuous variables** only +* **Nonlinearities** (including **nonconvexities**) in both the + variables and uncertain parameters +* **First-stage degrees of freedom** and **second-stage degrees of freedom** +* **Equality constraints** defining state variables, + including implicitly defined state variables that cannot be + eliminated from the model via reformulation +* **Inequality constraints** in the degree-of-freedom and/or state variables + +Supported deterministic models can be written in the general form + +.. _deterministic-model: + +.. math:: + \begin{array}{clll} + \displaystyle \min_{\substack{x \in \mathcal{X}, \\ z \in \mathbb{R}^{n_z}, y\in\mathbb{R}^{n_y}}} & ~~ f_1\left(x\right) + f_2(x,z,y; q^{\text{nom}}) & \\ + \displaystyle \text{s.t.} & ~~ g_i(x, z, y; q^{\text{nom}}) \leq 0 & \forall\,i \in \mathcal{I} \\ + & ~~ h_j(x,z,y; q^{\text{nom}}) = 0 & \forall\,j \in \mathcal{J} \\ + \end{array} + +where: + +* :math:`x \in \mathcal{X}` are the first-stage degrees of freedom, + (or "design" variables,) + of which the feasible space :math:`\mathcal{X} \subseteq \mathbb{R}^{n_x}` + is defined by the model constraints + (including variable bounds specifications) referencing :math:`x` only +* :math:`z \in \mathbb{R}^{n_z}` are the second-stage degrees of freedom + (or "control" variables) +* :math:`y \in \mathbb{R}^{n_y}` are the "state" variables +* :math:`q \in \mathbb{R}^{n_q}` is the vector of model parameters considered + uncertain, and :math:`q^{\text{nom}}` is the vector of nominal values + associated with those +* :math:`f_1\left(x\right)` is the summand of the objective function that depends + only on design variables +* :math:`f_2\left(x, z, y; q\right)` is the summand of the objective function + that depends on all variables and the uncertain parameters +* :math:`g_i\left(x, z, y; q\right)` is the :math:`i^\text{th}` + inequality constraint function in set :math:`\mathcal{I}` + (see :ref:`first Note `) +* :math:`h_j\left(x, z, y; q\right)` is the :math:`j^\text{th}` + equality constraint function in set :math:`\mathcal{J}` + (see :ref:`second Note `) + +.. _var-bounds-to-ineqs: + +.. note:: + PyROS accepts models in which there are: + + 1. Bounds declared on the ``Var`` objects representing + components of the variable vectors + 2. Ranged inequality constraints + +In order to cast the robust optimization counterpart of the +:ref:`deterministic model `, +we now assume that the uncertain parameters may attain +any realization in a compact uncertainty set +:math:`\mathcal{Q} \subseteq \mathbb{R}^{n_q}` containing +the nominal value :math:`q^{\text{nom}}`. +The set :math:`\mathcal{Q}` may be **either continuous or discrete**. + +Based on the above notation, +the form of the robust counterpart addressed by PyROS is + +.. math:: + \begin{array}{ccclll} + \displaystyle \min_{x \in \mathcal{X}} + & \displaystyle \max_{q \in \mathcal{Q}} + & \displaystyle \min_{\substack{z \in \mathbb{R}^{n_z},\\y \in \mathbb{R}^{n_y}}} \ \ & \displaystyle ~~ f_1\left(x\right) + f_2\left(x, z, y, q\right) \\ + & & \text{s.t.}~ & \displaystyle ~~ g_i\left(x, z, y, q\right) \leq 0 & & \forall\, i \in \mathcal{I}\\ + & & & \displaystyle ~~ h_j\left(x, z, y, q\right) = 0 & & \forall\,j \in \mathcal{J} + \end{array} + +PyROS accepts a deterministic model and accompanying uncertainty set +and then, using the Generalized Robust Cutting-Set algorithm developed +in [IAE+21]_, seeks a solution to the robust counterpart. +When using PyROS, please consider citing [IAE+21]_. + +.. _unique-mapping: + +.. note:: + A key assumption of PyROS is that + for every + :math:`x \in \mathcal{X}`, + :math:`z \in \mathbb{R}^{n_z}`, + :math:`q \in \mathcal{Q}`, + there exists a unique :math:`y \in \mathbb{R}^{n_y}` + for which :math:`(x, z, y, q)` + satisfies the equality constraints + :math:`h_j(x, z, y, q) = 0\,\,\forall\, j \in \mathcal{J}`. + If this assumption is not met, + then the selection of 'state' + (i.e., not degree of freedom) variables :math:`y` is incorrect, + and one or more of the :math:`y` variables should be appropriately + redesignated to be part of either :math:`x` or :math:`z`. + diff --git a/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_interface.rst b/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_interface.rst new file mode 100644 index 00000000000..078c8fb0d63 --- /dev/null +++ b/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_interface.rst @@ -0,0 +1,30 @@ +.. _pyros_solver_interface: + +PyROS Solver Interface +====================== + +The PyROS solver is invoked through the +:py:meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` +method of the PyROS solver class. +In summary, the required inputs to the PyROS solver are: + +* The deterministic optimization model +* List of first-stage ("design") variables +* List of second-stage ("control") variables +* List of parameters considered uncertain +* The uncertainty set +* Subordinate local and global nonlinear programming (NLP) solvers + +.. note:: + Any variables in the model not specified to be first-stage or second-stage + variables are automatically considered to be state variables. + + +The PyROS solver can be instantiated through the Pyomo +:class:`~pyomo.opt.base.solvers.SolverFactory`: + +.. code:: + + >>> import pyomo.environ as pyo + >>> import pyomo.contrib.pyros as pyros # register the PyROS solver + >>> pyros_solver = pyo.SolverFactory("pyros") diff --git a/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_log.rst b/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_log.rst new file mode 100644 index 00000000000..c73e3946bbe --- /dev/null +++ b/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_log.rst @@ -0,0 +1,319 @@ +.. _pyros_solver_log: + +PyROS Solver Output Log +======================= + +.. contents:: Table of Contents + :depth: 1 + :local: + + +.. _pyros_solver_log_appearance: + +Default Format +-------------- + +When the PyROS +:meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` method +is invoked on a robust optimization problem, +your console output will, by default, look like this: + + +.. _solver-log-snippet: + +.. code-block:: text + :caption: PyROS solver output log for the :ref:`two-stage problem example `. + :linenos: + + ============================================================================== + PyROS: The Pyomo Robust Optimization Solver, v1.3.4. + Pyomo version: 6.9.0 + Commit hash: unknown + Invoked at UTC 2025-02-13T00:00:00.000000 + + Developed by: Natalie M. Isenberg (1), Jason A. F. Sherman (1), + John D. Siirola (2), Chrysanthos E. Gounaris (1) + (1) Carnegie Mellon University, Department of Chemical Engineering + (2) Sandia National Laboratories, Center for Computing Research + + The developers gratefully acknowledge support from the U.S. Department + of Energy's Institute for the Design of Advanced Energy Systems (IDAES). + ============================================================================== + ================================= DISCLAIMER ================================= + PyROS is still under development. + Please provide feedback and/or report any issues by creating a ticket at + https://github.com/Pyomo/pyomo/issues/new/choose + ============================================================================== + Solver options: + time_limit=None + keepfiles=False + tee=False + load_solution=True + symbolic_solver_labels=False + objective_focus= + nominal_uncertain_param_vals=[0.13248000000000001, 4.97, 4.97, 1800] + decision_rule_order=1 + solve_master_globally=True + max_iter=-1 + robust_feasibility_tolerance=0.0001 + separation_priority_order={} + progress_logger= + backup_local_solvers=[] + backup_global_solvers=[] + subproblem_file_directory=None + bypass_local_separation=False + bypass_global_separation=False + p_robustness={} + ------------------------------------------------------------------------------ + Preprocessing... + Done preprocessing; required wall time of 0.009s. + ------------------------------------------------------------------------------ + Model Statistics: + Number of variables : 62 + Epigraph variable : 1 + First-stage variables : 7 + Second-stage variables : 6 (6 adj.) + State variables : 18 (7 adj.) + Decision rule variables : 30 + Number of uncertain parameters : 4 + Number of constraints : 52 + Equality constraints : 24 + Coefficient matching constraints : 0 + Other first-stage equations : 10 + Second-stage equations : 8 + Decision rule equations : 6 + Inequality constraints : 28 + First-stage inequalities : 1 + Second-stage inequalities : 27 + ------------------------------------------------------------------------------ + Itn Objective 1-Stg Shift 2-Stg Shift #CViol Max Viol Wall Time (s) + ------------------------------------------------------------------------------ + 0 3.5838e+07 - - 5 1.8832e+04 0.412 + 1 3.5838e+07 1.2289e-09 1.5886e-12 5 2.8919e+02 0.992 + 2 3.6269e+07 3.1647e-01 1.0432e-01 4 2.9020e+02 1.865 + 3 3.6285e+07 7.6526e-01 2.2258e-01 0 2.3874e-12g 3.508 + ------------------------------------------------------------------------------ + Robust optimal solution identified. + ------------------------------------------------------------------------------ + Timing breakdown: + + Identifier ncalls cumtime percall % + ----------------------------------------------------------- + main 1 3.509 3.509 100.0 + ------------------------------------------------------ + dr_polishing 3 0.209 0.070 6.0 + global_separation 27 0.590 0.022 16.8 + local_separation 108 1.569 0.015 44.7 + master 4 0.654 0.163 18.6 + master_feasibility 3 0.083 0.028 2.4 + preprocessing 1 0.009 0.009 0.3 + other n/a 0.394 n/a 11.2 + ====================================================== + =========================================================== + + ------------------------------------------------------------------------------ + Termination stats: + Iterations : 4 + Solve time (wall s) : 3.509 + Final objective value : 3.6285e+07 + Termination condition : pyrosTerminationCondition.robust_optimal + ------------------------------------------------------------------------------ + All done. Exiting PyROS. + ============================================================================== + +Observe that the log contains the following information: + + +* **Introductory information** (lines 1--18). + Includes the version number, author + information, (UTC) time at which the solver was invoked, + and, if available, information on the local Git branch and + commit hash. +* **Summary of solver options** (lines 19--40). +* **Preprocessing information** (lines 41--43). + Wall time required for preprocessing + the deterministic model and associated components, + i.e., standardizing model components and adding the decision rule + variables and equations. +* **Model component statistics** (lines 44--61). + Breakdown of model component statistics. + Includes components added by PyROS, such as the decision rule variables + and equations. + The preprocessor may find that some second-stage variables + and state variables are mathematically + not adjustable to the uncertain parameters. + To this end, in the logs, the numbers of + adjustable second-stage variables and state variables + are included in parentheses, next to the total numbers + of second-stage variables and state variables, respectively; + note that "adjustable" has been abbreviated as "adj." +* **Iteration log table** (lines 62--68). + Summary information on the problem iterates and subproblem outcomes. + The constituent columns are defined in detail in + :ref:`the table that follows `. +* **Termination message** (lines 69--70). Very brief summary of the termination outcome. +* **Timing statistics** (lines 71--87). + Tabulated breakdown of the solver timing statistics, based on a + :class:`pyomo.common.timing.HierarchicalTimer` printout. + The identifiers are as follows: + + * ``main``: Time elapsed by the solver. + * ``main.dr_polishing``: Time spent by the subordinate solvers + on polishing of the decision rules. + * ``main.global_separation``: Time spent by the subordinate solvers + on global separation subproblems. + * ``main.local_separation``: Time spent by the subordinate solvers + on local separation subproblems. + * ``main.master``: Time spent by the subordinate solvers on + the master problems. + * ``main.master_feasibility``: Time spent by the subordinate solvers + on the master feasibility problems. + * ``main.preprocessing``: Preprocessing time. + * ``main.other``: Overhead time. + +* **Termination statistics** (lines 88--93). Summary of statistics related to the + iterate at which PyROS terminates. +* **Exit message** (lines 94--95). + +The iteration log table (lines 62--68) is designed to provide, in a concise manner, +important information about the progress of the iterative algorithm for +the problem of interest. +The constituent columns are defined in the +table below: + +.. _table-iteration-log-columns: + +.. list-table:: PyROS iteration log table columns. + :widths: 10 50 + :header-rows: 1 + + * - Column Name + - Definition + * - Itn + - Iteration number. + * - Objective + - Master solution objective function value. + If the objective of the deterministic model provided + has a maximization sense, + then the negative of the objective function value is displayed. + Expect this value to trend upward as the iteration number + increases. + If the master problems are solved globally + (by passing ``solve_master_globally=True``), + then after the iteration number exceeds the number of uncertain parameters, + this value should be monotonically nondecreasing + as the iteration number is increased. + A dash ("-") is produced in lieu of a value if the master + problem of the current iteration is not solved successfully. + * - 1-Stg Shift + - Infinity norm of the relative difference between the first-stage + variable vectors of the master solutions of the current + and previous iterations. Expect this value to trend + downward as the iteration number increases. + A dash ("-") is produced in lieu of a value + if the current iteration number is 0, + there are no first-stage variables, + or the master problem of the current iteration is not solved successfully. + * - 2-Stg Shift + - Infinity norm of the relative difference between the second-stage + variable vectors (evaluated subject to the nominal uncertain + parameter realization) of the master solutions of the current + and previous iterations. Expect this value to trend + downward as the iteration number increases. + A dash ("-") is produced in lieu of a value + if the current iteration number is 0, + there are no second-stage variables, + or the master problem of the current iteration is not solved successfully. + * - #CViol + - Number of second-stage inequality constraints found to be violated during + the separation step of the current iteration. + Unless a custom prioritization of the model's second-stage inequality + constraints is specified (through the ``separation_priority_order`` argument), + expect this number to trend downward as the iteration number increases. + A "+" is appended if not all of the separation problems + were solved successfully, either due to custom prioritization, a time out, + or an issue encountered by the subordinate optimizers. + A dash ("-") is produced in lieu of a value if the separation + routine is not invoked during the current iteration. + * - Max Viol + - Maximum scaled second-stage inequality constraint violation. + Expect this value to trend downward as the iteration number increases. + A 'g' is appended to the value if the separation problems were solved + globally during the current iteration. + A dash ("-") is produced in lieu of a value if the separation + routine is not invoked during the current iteration, or if there are + no second-stage inequality constraints. + * - Wall time (s) + - Total time elapsed by the solver, in seconds, up to the end of the + current iteration. + +.. _pyros_solver_log_verbosity: + +Configuring the Output Log +-------------------------- + +For a given call to the PyROS +:meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` method, +the solver log output is produced by the +Python logger (:py:class:`logging.Logger`) object +derived from the optional argument ``progress_logger``. +By default, ``progress_logger`` +is taken to be the logger with name ``"pyomo.contrib.pyros"``. +The level of the default progress logger is originally set to +:py:obj:`logging.INFO` and, for example, can be set to +:py:obj:`logging.DEBUG` with: + +.. doctest:: + + >>> import logging + >>> logging.getLogger("pyomo.contrib.pyros").setLevel(logging.DEBUG) + + +The verbosity of the output log can be adjusted by setting the +:py:mod:`logging` level of the progress logger. +PyROS logs output messages at different :py:mod:`logging` levels, +according to the following table, in which the levels are +arranged in decreasing order of severity. +Messages with a lower level than that of ``progress_logger`` +are excluded from the solver log. + +.. _table-logging-levels: + +.. list-table:: PyROS solver log output at the various standard Python :py:mod:`logging` levels. + :widths: 10 50 + :header-rows: 1 + + * - Logging Level + - Output Messages + * - :py:obj:`logging.ERROR` + - * Information on the subproblem for which an exception was raised + by a subordinate solver + * Details about failure of the PyROS coefficient matching routine + * - :py:obj:`logging.WARNING` + - * Information about a subproblem not solved to an acceptable status + by the user-provided subordinate optimizers + * Invocation of a backup solver for a particular subproblem + * Caution about solution robustness guarantees in event that + user passes ``bypass_global_separation=True`` + * - :py:obj:`logging.INFO` + - * PyROS version, author, and disclaimer information + * Summary of user options + * Breakdown of model component statistics + * Iteration log table + * Termination details: message, timing breakdown, summary of statistics + * - :py:obj:`logging.DEBUG` + - * Progress through the various preprocessing subroutines + * Termination outcomes and summary of statistics for + every master feasility, master, and DR polishing problem + * Progress updates for the separation procedure + * Separation subproblem initial point infeasibilities + * Summary of separation loop outcomes: second-stage inequality constraints + violated, uncertain parameter scenario added to the + master problem + * Uncertain parameter scenarios added to the master problem + thus far + +We refer the reader to the +:doc:`official Python logging library documentation ` +for further guidance on (customization of) Python logger objects; +for a basic tutorial, see the :doc:`logging HOWTO `. diff --git a/doc/OnlineDocs/explanation/solvers/pyrosdir/uncertainty_sets.rst b/doc/OnlineDocs/explanation/solvers/pyrosdir/uncertainty_sets.rst new file mode 100644 index 00000000000..9fc597a7a1d --- /dev/null +++ b/doc/OnlineDocs/explanation/solvers/pyrosdir/uncertainty_sets.rst @@ -0,0 +1,72 @@ +.. _pyros_uncertainty_sets: + +PyROS Uncertainty Sets +====================== +In PyROS, the uncertainty set of a robust optimization problem +is represented by an instance of a subclass of the +:class:`~pyomo.contrib.pyros.uncertainty_sets.UncertaintySet` +abstract base class. +PyROS provides a suite of pre-implemented concrete +:class:`~pyomo.contrib.pyros.uncertainty_sets.UncertaintySet` +subclasses to facilitate instantiation of uncertainty sets +that are commonly used in the optimization literature. +Custom uncertainty set types can be implemented by subclassing +:class:`~pyomo.contrib.pyros.uncertainty_sets.UncertaintySet`. +The pre-implemented subclasses are enumerated below: + +.. autosummary:: + + ~pyomo.contrib.pyros.uncertainty_sets.AxisAlignedEllipsoidalSet + ~pyomo.contrib.pyros.uncertainty_sets.BoxSet + ~pyomo.contrib.pyros.uncertainty_sets.BudgetSet + ~pyomo.contrib.pyros.uncertainty_sets.CardinalitySet + ~pyomo.contrib.pyros.uncertainty_sets.DiscreteScenarioSet + ~pyomo.contrib.pyros.uncertainty_sets.EllipsoidalSet + ~pyomo.contrib.pyros.uncertainty_sets.FactorModelSet + ~pyomo.contrib.pyros.uncertainty_sets.IntersectionSet + ~pyomo.contrib.pyros.uncertainty_sets.PolyhedralSet + +.. + Everything inside this block is commented out. + + The table that follows provides mathematical definitions + of the various and pre-implemented + :class:`~pyomo.contrib.pyros.uncertainty_sets.UncertaintySet` + subclasses. + + .. _pyros_uncertainty_sets_math_defs: + + .. list-table:: Mathematical definitions for PyROS uncertainty sets of dimension :math:`n`. + :header-rows: 1 + :class: tight-table + + * - Uncertainty Set Type + - Input Data + - Mathematical Definition + * - :class:`~pyomo.contrib.pyros.uncertainty_sets.BoxSet` + - :math:`\begin{array}{l} q ^{\text{L}} \in \mathbb{R}^{n}, \\ q^{\text{U}} \in \mathbb{R}^{n} \end{array}` + - :math:`\{q \in \mathbb{R}^n \mid q^\mathrm{L} \leq q \leq q^\mathrm{U}\}` + * - :class:`~pyomo.contrib.pyros.uncertainty_sets.CardinalitySet` + - :math:`\begin{array}{l} q^{0} \in \mathbb{R}^{n}, \\ \hat{q} \in \mathbb{R}_{+}^{n}, \\ \Gamma \in [0, n] \end{array}` + - :math:`\left\{ q \in \mathbb{R}^{n} \middle| \begin{array}{l} \exists\,\xi \in [0, 1]^n\,:\\ \quad \,q = q^{0} + \hat{q} \circ \xi \\ \quad \displaystyle \sum_{i=1}^{n} \xi_{i} \leq \Gamma \end{array} \right\}` + * - :class:`~pyomo.contrib.pyros.uncertainty_sets.BudgetSet` + - :math:`\begin{array}{l} q^{0} \in \mathbb{R}^{n}, \\ b \in \mathbb{R}_{+}^{L}, \\ B \in \{0, 1\}^{L \times n} \end{array}` + - :math:`\left\{ q \in \mathbb{R}^{n} \middle| \begin{array}{l} \begin{pmatrix} B \\ -I \end{pmatrix} q \leq \begin{pmatrix} b + Bq^{0} \\ -q^{0} \end{pmatrix} \end{array} \right\}` + * - :class:`~pyomo.contrib.pyros.uncertainty_sets.FactorModelSet` + - :math:`\begin{array}{l} q^{0} \in \mathbb{R}^{n}, \\ \Psi \in \mathbb{R}^{n \times F}, \\ \beta \in [0, 1] \end{array}` + - :math:`\left\{ q \in \mathbb{R}^{n} \middle| \begin{array}{l} \exists\,\xi \in [-1, 1]^F \,:\\ \quad q = q^{0} + \Psi \xi \\ \quad \displaystyle\bigg| \sum_{j=1}^{F} \xi_{j} \bigg| \leq \beta F \end{array} \right\}` + * - :class:`~pyomo.contrib.pyros.uncertainty_sets.PolyhedralSet` + - :math:`\begin{array}{l} A \in \mathbb{R}^{m \times n}, \\ b \in \mathbb{R}^{m}\end{array}` + - :math:`\{q \in \mathbb{R}^{n} \mid A q \leq b\}` + * - :class:`~pyomo.contrib.pyros.uncertainty_sets.AxisAlignedEllipsoidalSet` + - :math:`\begin{array}{l} q^0 \in \mathbb{R}^{n}, \\ \alpha \in \mathbb{R}_{+}^{n} \end{array}` + - :math:`\left\{ q \in \mathbb{R}^{n} \middle| \begin{array}{l} \displaystyle\sum_{\substack{i = 1: \\ \alpha_{i} > 0}}^{n} \left(\frac{q_{i} - q_{i}^{0}}{\alpha_{i}}\right)^2 \leq 1 \\ q_{i} = q_{i}^{0} \,\forall\,i : \alpha_{i} = 0 \end{array} \right\}` + * - :class:`~pyomo.contrib.pyros.uncertainty_sets.EllipsoidalSet` + - :math:`\begin{array}{l} q^0 \in \mathbb{R}^n, \\ P \in \mathbb{S}_{++}^{n}, \\ s \in \mathbb{R}_{+} \end{array}` + - :math:`\{q \in \mathbb{R}^{n} \mid (q - q^{0})^{\intercal} P^{-1} (q - q^{0}) \leq s\}` + * - :class:`~pyomo.contrib.pyros.uncertainty_sets.DiscreteScenarioSet` + - :math:`q^{1}, q^{2},\dots , q^{S} \in \mathbb{R}^{n}` + - :math:`\{q^{1}, q^{2}, \dots , q^{S}\}` + * - :class:`~pyomo.contrib.pyros.uncertainty_sets.IntersectionSet` + - :math:`\mathcal{Q}_{1}, \mathcal{Q}_{2}, \dots , \mathcal{Q}_{m} \subset \mathbb{R}^{n}` + - :math:`\displaystyle \bigcap_{i=1}^{m} \mathcal{Q}_{i}` From a76a8b34542eb1c2e13ffe1bb9fa0cdce0a6ede2 Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 24 Feb 2025 00:58:48 -0500 Subject: [PATCH 06/67] Make further changes to split documentation --- .../solvers/pyrosdir/getting_started.rst | 113 ++++++++++-------- .../solvers/pyrosdir/solver_interface.rst | 39 +++--- .../solvers/pyrosdir/solver_log.rst | 6 +- 3 files changed, 89 insertions(+), 69 deletions(-) diff --git a/doc/OnlineDocs/explanation/solvers/pyrosdir/getting_started.rst b/doc/OnlineDocs/explanation/solvers/pyrosdir/getting_started.rst index fcb3d3406bd..1e1ca7850c4 100644 --- a/doc/OnlineDocs/explanation/solvers/pyrosdir/getting_started.rst +++ b/doc/OnlineDocs/explanation/solvers/pyrosdir/getting_started.rst @@ -47,7 +47,7 @@ PyROS can be installed as follows: Usage Tutorial -------------- -This tutorial shows how to solve robust optimization problems with PyROS. +In this tutorial, we will solve robust optimization problems with PyROS. The problems are derived from the deterministic model *hydro*, a QCQP taken from the `GAMS Model Library `_. @@ -64,10 +64,10 @@ Moreover, there are 12 linear equality constraints, 6 non-linear (quadratic) equality constraints, and a quadratic objective. -We have extended this model by converting one objective coefficient, -two constraint coefficients, and one constraint right-hand side -into :class:`~pyomo.core.base.param.Param` objects -so that they can be considered uncertain later on. +**All variables of the model are continuous.** + +.. note:: + PyROS does not support models with discrete decision variables. .. note:: Per our analysis, the model *hydro* satisfies the requirement that @@ -78,6 +78,12 @@ so that they can be considered uncertain later on. into (first-stage and second-stage) degrees of freedom and state variables. +We have extended this model by converting one objective coefficient, +two constraint coefficients, and one constraint right-hand side +into :class:`~pyomo.core.base.param.Param` objects +so that they can be considered uncertain later on. + + Step 0: Import Pyomo and the PyROS Module ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -95,7 +101,7 @@ Step 1: Define the Deterministic Problem ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The deterministic Pyomo model for *hydro* is constructed as follows. -We first construct the main model object: +We first instantiate a Pyomo model object: .. _pyros_model_construct: @@ -228,7 +234,7 @@ Finally, we declare the decision variables, objective, and constraints: Before moving on, we check that the model can be solved to optimality with a deterministic nonlinear programming (NLP) solver. -For convenience, we use BARON as the solver: +We have elected to use BARON as the solver: .. _pyros_solve_deterministic: @@ -265,20 +271,14 @@ one of the following: * ``list(m.q.values())`` .. note:: - Any :class:`~pyomo.core.base.param.Param` object that is - to be considered uncertain by PyROS must have the property - ``mutable=True``. - -.. note:: - PyROS also allows uncertain parameters to be implemented as - :class:`~pyomo.core.base.var.Var` objects declared on the - deterministic model. - This may be convenient for users transitioning to PyROS from - parameter estimation and/or uncertainty quantification workflows, - in which the uncertain parameters are - often represented by :class:`~pyomo.core.base.var.Var` objects. - Prior to invoking PyROS, - all such :class:`~pyomo.core.base.var.Var` objects should be fixed. + 1. Any :class:`~pyomo.core.base.param.Param` object that + represents an uncertain parameter must be instantiated + with the constructor argument ``mutable=True``. + 2. Uncertain parameters can also be represented by + :class:`~pyomo.core.base.var.Var` + objects declared on the deterministic model. + Prior to invoking PyROS, + all such :class:`~pyomo.core.base.var.Var` objects should be fixed. PyROS requires an uncertainty set against which to robustly @@ -290,11 +290,12 @@ In PyROS, an uncertainty set is represented by an instance of a subclass of the :class:`~pyomo.contrib.pyros.uncertainty_sets.UncertaintySet` class. -Let us assume in the present example that each uncertain parameter can +In the present example, +let us assume that each uncertain parameter can independently of the other uncertain parameters deviate from the parameter's nominal value by up to :math:`\pm 15\%`. -The resulting uncertainty set is a box, which we can implement -as an instance of the +Then the parameter values are constrained to a box region, +which we can implement as an instance of the :class:`~pyomo.contrib.pyros.uncertainty_sets.BoxSet` subclass: .. doctest:: @@ -362,7 +363,7 @@ So our variable designation is as follows: >>> second_stage_variables = [] The single-stage problem can now be solved -:ref:`to robust optimality ` +to robust optimality by invoking the :meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` method of the PyROS solver object, as follows: @@ -372,6 +373,7 @@ method of the PyROS solver object, as follows: :skipif: not (baron.available() and baron.license_is_valid()) >>> results_1 = pyros_solver.solve( + ... # required arguments ... model=m, ... first_stage_variables=first_stage_variables, ... second_stage_variables=second_stage_variables, @@ -379,6 +381,7 @@ method of the PyROS solver object, as follows: ... uncertainty_set=box_uncertainty_set, ... local_solver=local_solver, ... global_solver=global_solver, + ... # optional arguments: solve to robust optimality ... objective_focus=pyros.ObjectiveType.worst_case, ... solve_master_globally=True, ... ) @@ -442,7 +445,7 @@ as follows: ... global_solver=global_solver, ... objective_focus=pyros.ObjectiveType.worst_case, ... solve_master_globally=True, - ... decision_rule_order=1, + ... decision_rule_order=1, # use affine decision rules ... ) ============================================================================== PyROS: The Pyomo Robust Optimization Solver... @@ -464,9 +467,10 @@ Specifying Arguments Indirectly Through ``options`` """"""""""""""""""""""""""""""""""""""""""""""""""" Like other Pyomo solver interface methods, :meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` -provides support for specifying options indirectly by passing -a keyword argument ``options``, whose value must be a :class:`dict` -mapping names of arguments to :meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` +provides support for specifying optional arguments indirectly by passing +a keyword argument ``options``, for which the value must be a :class:`dict` +mapping names of optional arguments to +:meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` to their desired values. For example, the ``solve()`` statement in the :ref:`two-stage problem snippet ` @@ -476,6 +480,7 @@ could have been equivalently written as: :skipif: not (baron.available() and baron.license_is_valid()) >>> results_2 = pyros_solver.solve( + ... # required arguments ... model=m, ... first_stage_variables=first_stage_variables, ... second_stage_variables=second_stage_variables, @@ -483,6 +488,7 @@ could have been equivalently written as: ... uncertainty_set=box_uncertainty_set, ... local_solver=local_solver, ... global_solver=global_solver, + ... # optional arguments: passed indirectly ... options={ ... "objective_focus": pyros.ObjectiveType.worst_case, ... "solve_master_globally": True, @@ -511,6 +517,15 @@ by position or keyword, *and* indirectly through ``options``, the value passed directly takes precedence over the value passed through ``options``. +.. warning:: + + All required arguments to the PyROS + :meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` method + must be passed directly by position or keyword, + or else an exception is raised. + Required arguments passed indirectly through the ``options`` + setting are ignored. + Step 4: Check the Results ^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -537,7 +552,7 @@ We can also query the results object's individual attributes: >>> results_2.iterations # total number of iterations 5 - >>> results_2.time # total wall-clock time (in seconds); may vary + >>> results_2.time # total wall-clock seconds; may vary 6.336 >>> results_2.final_objective_value # final objective value; may vary 36285242.22224089 @@ -631,15 +646,7 @@ Analyzing the Price of Robustness ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PyROS facilitates an analysis of the "price of robustness", which we define to be the increase in the robust optimal objective value -relative to the deterministically optimal objective value. -The deterministically optimal objective can be obtained as follows: - -.. code:: - - >>> pyo.assert_optimal_termination(global_solver.solve(m)) - >>> deterministic_obj = pyo.value(m.obj) - - +relative to the deterministically optimal objective value Let us, for example, consider optimizing robustly against a box uncertainty set centered on the nominal realization of the uncertain parameters @@ -676,7 +683,7 @@ in a for-loop: ... uncertain_params=uncertain_params, ... uncertainty_set=box_uncertainty_set, ... local_solver=local_solver, - ... global_solver=pyo.SolverFactory("scip"), + ... global_solver=global_solver, ... objective_focus=pyros.ObjectiveType.worst_case, ... solve_master_globally=True, ... decision_rule_order=1, @@ -695,7 +702,9 @@ in a for-loop: All done. Using the :py:obj:`dict` populated in the loop, -we can print a summary of the results: +and the +:ref:`previously evaluated deterministically optimal objective value `, +we can print a tabular summary of the results: .. code:: @@ -737,12 +746,20 @@ we can print a summary of the results: ==================================================================================== -The summary table gives the PyROS termination condition, -final objective value, and relative increase in objective value -(compared to the deterministically optimal value), -for each relative deviation. +The table shows the response of the PyROS termination condition, +final objective value, and price of robustness +to the relative half-length :math:`p`. +Observe that: + +* The optimal objective value for the box set of relative half-length + :math:`p=0` is equal to the optimal deterministic objective value +* The objective value (and thus, the price of robustness) + increases with the half-length +* For large enough half-length (:math:`p=0.4`) the problem + is robust infeasible -This example clearly illustrates the potential impact of the uncertainty -set size on the robust optimal objective function value -and demonstrates the ease of implementing a price of robustness study +Therefore, this example clearly illustrates the potential +impact of the uncertainty set size on the robust optimal +objective function value +and the ease of analyzing the price of robustness for a given optimization problem under uncertainty. diff --git a/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_interface.rst b/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_interface.rst index 078c8fb0d63..3ef69a45921 100644 --- a/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_interface.rst +++ b/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_interface.rst @@ -3,28 +3,29 @@ PyROS Solver Interface ====================== -The PyROS solver is invoked through the -:py:meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` -method of the PyROS solver class. -In summary, the required inputs to the PyROS solver are: - -* The deterministic optimization model -* List of first-stage ("design") variables -* List of second-stage ("control") variables -* List of parameters considered uncertain -* The uncertainty set -* Subordinate local and global nonlinear programming (NLP) solvers - -.. note:: - Any variables in the model not specified to be first-stage or second-stage - variables are automatically considered to be state variables. - - -The PyROS solver can be instantiated through the Pyomo -:class:`~pyomo.opt.base.solvers.SolverFactory`: +Like other Pyomo solvers, the PyROS solver can be instantiated directly +or through the Pyomo :class:`~pyomo.opt.base.solvers.SolverFactory`: .. code:: >>> import pyomo.environ as pyo >>> import pyomo.contrib.pyros as pyros # register the PyROS solver >>> pyros_solver = pyo.SolverFactory("pyros") + +Subsequently, the solver in invoked by calling the +:meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` method, +the required inputs of which are the: + +* Deterministic optimization model +* First-stage ("design") variables +* Second-stage ("control") variables +* Parameters considered uncertain +* Uncertainty set +* Subordinate local and global nonlinear programming (NLP) solvers + +See the :py:meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` +method for further information. + +.. note:: + Any variables in the model not specified to be first-stage or second-stage + variables are automatically considered to be state variables. diff --git a/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_log.rst b/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_log.rst index c73e3946bbe..0ec43d853c9 100644 --- a/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_log.rst +++ b/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_log.rst @@ -171,8 +171,10 @@ Observe that the log contains the following information: * ``main.preprocessing``: Preprocessing time. * ``main.other``: Overhead time. -* **Termination statistics** (lines 88--93). Summary of statistics related to the - iterate at which PyROS terminates. +* **Final result** (lines 88--93). + A printout of the + :class:`~pyomo.contrib.pyros.solve_data.ROSolveResults` + object that is finally returned * **Exit message** (lines 94--95). The iteration log table (lines 62--68) is designed to provide, in a concise manner, From 92c30533882909b72c8b4bc7a7c8b7975f04ed33 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 26 Feb 2025 12:50:58 -0500 Subject: [PATCH 07/67] Make corrections to uncertainty set documentation --- pyomo/contrib/pyros/uncertainty_sets.py | 31 ++++++++++++++----------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 52871ea5c11..a32ad08c9a4 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -995,7 +995,7 @@ class BoxSet(UncertaintySet): 5D hypercube with bounds 0 and 1 in each dimension: - >>> hypercube_5d = BoxSet(bounds=[[0, 1] for idx in range(5)]) + >>> hypercube_5d = BoxSet(bounds=[[0, 1]] * 5) >>> hypercube_5d.bounds array([[0, 1], [0, 1], @@ -1141,7 +1141,7 @@ class CardinalitySet(UncertaintySet): the quantity :math:`\\hat{q} \\in \\mathbb{R}_{+}^n` refers to ``positive_deviation``, and :math:`\\Gamma \\in [0, n]` refers to ``gamma``. - The operator :math:`\\circ` denotes the element-wise product. + The operator ":math:`\\circ`" denotes the element-wise product. Examples -------- @@ -1208,7 +1208,7 @@ def origin(self, val): def positive_deviation(self): """ (N,) numpy.ndarray : Maximal coordinate deviations from the - origin in each dimension. All entries are nonnegative. + origin in each dimension. All entries should be nonnegative. """ return self._positive_deviation @@ -1246,15 +1246,16 @@ def positive_deviation(self, val): def gamma(self): """ numeric type : Upper bound for the number of uncertain - parameters which may maximally deviate from their respective + parameters that may maximally deviate from their respective origin values simultaneously. Must be a numerical value ranging from 0 to the set dimension `N`. Note that, mathematically, setting `gamma` to 0 reduces the set - to a singleton containing the center, while setting `gamma` to + to a singleton containing the point represented by + ``self.origin``, while setting `gamma` to the set dimension `N` makes the set mathematically equivalent to a `BoxSet` with bounds - ``numpy.array([origin, origin + positive_deviation]).T``. + ``numpy.array([self.origin, self.origin + self.positive_deviation]).T``. """ return self._gamma @@ -1743,6 +1744,7 @@ def budget_membership_mat(self): constraints. Each row corresponds to a single budget constraint and defines which uncertain parameters participate in that row's constraint. + All entries should be of value 0 or 1. """ return self._budget_membership_mat @@ -1962,7 +1964,7 @@ class FactorModelSet(UncertaintySet): \\left\\{ q \\in \\mathbb{R}^n\\, \\middle| \\, - \\exists\\, \\xi \\in [-1, 1]^n \\,:\\, + \\exists\\, \\xi \\in [-1, 1]^F \\,:\\, \\left[ \\begin{array}{l} q = q^0 + \\Psi \\xi \\\\ @@ -2135,8 +2137,8 @@ def beta(self): Note that, mathematically, setting ``beta = 0`` will enforce that as many factors will be above 0 as there will be below 0 (i.e., "zero-net-alpha" model). If ``beta = 1``, - then the set is numerically equivalent to a `BoxSet` with bounds - ``[self.origin - psi @ np.ones(F), self.origin + psi @ np.ones(F)].T``. + then any number of factors can be above 0 or below 0 + simultaneously. """ return self._beta @@ -2538,13 +2540,13 @@ class EllipsoidalSet(UncertaintySet): \\left\\{ q \\in \\mathbb{R}^n\\,| - \\,(q - q^0)^\\intercal P^{-1}(q - q^0) \\leq s + \\,(q - q^0)^\\intercal \\Sigma^{-1}(q - q^0) \\leq s \\right\\} in which :math:`q^0 \\in \\mathbb{R}^n` refers to ``center``, the quantity - :math:`P \\in \\mathbb{R}^{n \\times n}` + :math:`\\Sigma \\in \\mathbb{R}^{n \\times n}` refers to ``shape_matrix``, and :math:`s \\geq 0` refers to ``scale``. @@ -2574,7 +2576,7 @@ class EllipsoidalSet(UncertaintySet): >>> ball.scale 1 - A 2D ellipsoid with custom rotation and scaling: + A 2D ellipsoidal region with custom rotation and scaling: >>> rotated_ellipsoid = EllipsoidalSet( ... center=[1, 1], @@ -2589,7 +2591,7 @@ class EllipsoidalSet(UncertaintySet): >>> rotated_ellipsoid.scale 0.5 - A 4D 95% confidence ellipsoid: + A 4D 95% confidence ellipsoidal region: >>> conf_ellipsoid = EllipsoidalSet( ... center=np.zeros(4), @@ -3101,7 +3103,8 @@ class IntersectionSet(UncertaintySet): ... center=[0, 0], ... half_lengths=[2, 2], ... ) - >>> # to construct intersection, pass sets as keyword arguments + >>> # to construct intersection, pass sets as keyword arguments. + >>> # keywords are arbitrary >>> intersection = IntersectionSet(set1=square, set2=circle) >>> intersection.all_sets UncertaintySetList([...]) From fab9da8787ea714c4cde66983852e8b355917057 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 26 Feb 2025 12:58:25 -0500 Subject: [PATCH 08/67] Update `BudgetSet` documentation --- pyomo/contrib/pyros/uncertainty_sets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index a32ad08c9a4..de2df62a816 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -1633,6 +1633,7 @@ class BudgetSet(UncertaintySet): Each row corresponds to a single budget constraint, and defines which uncertain parameters (which dimensions) participate in that row's constraint. + All entries should be of value 0 or 1. rhs_vec : (L,) array_like Budget limits (upper bounds) with respect to the origin of the set. From 01363610a49cbff3b3942fef2df1a6f36a0e0cfd Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 26 Feb 2025 13:16:45 -0500 Subject: [PATCH 09/67] Make correction to `BoxSet` class documentation --- pyomo/contrib/pyros/uncertainty_sets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index de2df62a816..54f59f23994 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -972,10 +972,10 @@ class BoxSet(UncertaintySet): in which :math:`q^\\text{L} \\in \\mathbb{R}^n` refers to - ``np.array(bounds[:, 0])``, + ``np.array(bounds)[:, 0]``, and :math:`q^\\text{U} \\in \\mathbb{R}^n` refers to - ``np.array(bounds[:, 1])``. + ``np.array(bounds)[:, 1]``. Examples -------- From bdbd0cb85eb9d7034bc986f60029409299dc54f3 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 26 Feb 2025 13:29:54 -0500 Subject: [PATCH 10/67] Tweak documentation of `BoxSet` --- pyomo/contrib/pyros/uncertainty_sets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 54f59f23994..6d2a1772b93 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -972,10 +972,10 @@ class BoxSet(UncertaintySet): in which :math:`q^\\text{L} \\in \\mathbb{R}^n` refers to - ``np.array(bounds)[:, 0]``, + ``numpy.array(bounds)[:, 0]``, and :math:`q^\\text{U} \\in \\mathbb{R}^n` refers to - ``np.array(bounds)[:, 1]``. + ``numpy.array(bounds)[:, 1]``. Examples -------- From 97b0dead7d0a255a6eafb1ac17bdcdd42f838b3d Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 26 Feb 2025 13:32:13 -0500 Subject: [PATCH 11/67] Tweak `IntersectionSet` documentation --- pyomo/contrib/pyros/uncertainty_sets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 6d2a1772b93..d2e8dcaa2b3 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -3068,7 +3068,7 @@ def point_in_set(self, point): class IntersectionSet(UncertaintySet): """ - An intersection of a sequence of uncertainty sets, each of which + An intersection of uncertainty sets, each of which is represented by an `UncertaintySet` object. Parameters From 6f82871c4cee9ba4cf299ef4f6083a925a719568 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 26 Feb 2025 13:32:53 -0500 Subject: [PATCH 12/67] Tweak `IntersectionSet` documentation further --- pyomo/contrib/pyros/uncertainty_sets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index d2e8dcaa2b3..a69604957a6 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -3068,7 +3068,7 @@ def point_in_set(self, point): class IntersectionSet(UncertaintySet): """ - An intersection of uncertainty sets, each of which + An intersection of two or more uncertainty sets, each of which is represented by an `UncertaintySet` object. Parameters From 7fadafa389dd31356ede1aa562cf7682d736a1cc Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 26 Feb 2025 19:59:09 -0500 Subject: [PATCH 13/67] Restore PyROS uncertainty sets table and make it scrollable --- doc/OnlineDocs/_static/theme_overrides.css | 4 + .../solvers/pyrosdir/uncertainty_sets.rst | 77 +++++++++---------- 2 files changed, 42 insertions(+), 39 deletions(-) diff --git a/doc/OnlineDocs/_static/theme_overrides.css b/doc/OnlineDocs/_static/theme_overrides.css index 936b3f95b13..d0eddc5faf9 100644 --- a/doc/OnlineDocs/_static/theme_overrides.css +++ b/doc/OnlineDocs/_static/theme_overrides.css @@ -72,6 +72,10 @@ dl.py.method dt em span.n { padding-right: 8px !important; } +.rst-content table.scrollwide { + overflow: scroll !important; + display: block; +} /* OLD theme overrides diff --git a/doc/OnlineDocs/explanation/solvers/pyrosdir/uncertainty_sets.rst b/doc/OnlineDocs/explanation/solvers/pyrosdir/uncertainty_sets.rst index 9fc597a7a1d..23acb5984e9 100644 --- a/doc/OnlineDocs/explanation/solvers/pyrosdir/uncertainty_sets.rst +++ b/doc/OnlineDocs/explanation/solvers/pyrosdir/uncertainty_sets.rst @@ -29,44 +29,43 @@ The pre-implemented subclasses are enumerated below: .. Everything inside this block is commented out. - The table that follows provides mathematical definitions - of the various and pre-implemented - :class:`~pyomo.contrib.pyros.uncertainty_sets.UncertaintySet` - subclasses. +Mathematical definitions of the pre-implemented +:class:`~pyomo.contrib.pyros.uncertainty_sets.UncertaintySet` +subclasses are provided below. - .. _pyros_uncertainty_sets_math_defs: - - .. list-table:: Mathematical definitions for PyROS uncertainty sets of dimension :math:`n`. - :header-rows: 1 - :class: tight-table +.. _pyros_uncertainty_sets_math_defs: - * - Uncertainty Set Type - - Input Data - - Mathematical Definition - * - :class:`~pyomo.contrib.pyros.uncertainty_sets.BoxSet` - - :math:`\begin{array}{l} q ^{\text{L}} \in \mathbb{R}^{n}, \\ q^{\text{U}} \in \mathbb{R}^{n} \end{array}` - - :math:`\{q \in \mathbb{R}^n \mid q^\mathrm{L} \leq q \leq q^\mathrm{U}\}` - * - :class:`~pyomo.contrib.pyros.uncertainty_sets.CardinalitySet` - - :math:`\begin{array}{l} q^{0} \in \mathbb{R}^{n}, \\ \hat{q} \in \mathbb{R}_{+}^{n}, \\ \Gamma \in [0, n] \end{array}` - - :math:`\left\{ q \in \mathbb{R}^{n} \middle| \begin{array}{l} \exists\,\xi \in [0, 1]^n\,:\\ \quad \,q = q^{0} + \hat{q} \circ \xi \\ \quad \displaystyle \sum_{i=1}^{n} \xi_{i} \leq \Gamma \end{array} \right\}` - * - :class:`~pyomo.contrib.pyros.uncertainty_sets.BudgetSet` - - :math:`\begin{array}{l} q^{0} \in \mathbb{R}^{n}, \\ b \in \mathbb{R}_{+}^{L}, \\ B \in \{0, 1\}^{L \times n} \end{array}` - - :math:`\left\{ q \in \mathbb{R}^{n} \middle| \begin{array}{l} \begin{pmatrix} B \\ -I \end{pmatrix} q \leq \begin{pmatrix} b + Bq^{0} \\ -q^{0} \end{pmatrix} \end{array} \right\}` - * - :class:`~pyomo.contrib.pyros.uncertainty_sets.FactorModelSet` - - :math:`\begin{array}{l} q^{0} \in \mathbb{R}^{n}, \\ \Psi \in \mathbb{R}^{n \times F}, \\ \beta \in [0, 1] \end{array}` - - :math:`\left\{ q \in \mathbb{R}^{n} \middle| \begin{array}{l} \exists\,\xi \in [-1, 1]^F \,:\\ \quad q = q^{0} + \Psi \xi \\ \quad \displaystyle\bigg| \sum_{j=1}^{F} \xi_{j} \bigg| \leq \beta F \end{array} \right\}` - * - :class:`~pyomo.contrib.pyros.uncertainty_sets.PolyhedralSet` - - :math:`\begin{array}{l} A \in \mathbb{R}^{m \times n}, \\ b \in \mathbb{R}^{m}\end{array}` - - :math:`\{q \in \mathbb{R}^{n} \mid A q \leq b\}` - * - :class:`~pyomo.contrib.pyros.uncertainty_sets.AxisAlignedEllipsoidalSet` - - :math:`\begin{array}{l} q^0 \in \mathbb{R}^{n}, \\ \alpha \in \mathbb{R}_{+}^{n} \end{array}` - - :math:`\left\{ q \in \mathbb{R}^{n} \middle| \begin{array}{l} \displaystyle\sum_{\substack{i = 1: \\ \alpha_{i} > 0}}^{n} \left(\frac{q_{i} - q_{i}^{0}}{\alpha_{i}}\right)^2 \leq 1 \\ q_{i} = q_{i}^{0} \,\forall\,i : \alpha_{i} = 0 \end{array} \right\}` - * - :class:`~pyomo.contrib.pyros.uncertainty_sets.EllipsoidalSet` - - :math:`\begin{array}{l} q^0 \in \mathbb{R}^n, \\ P \in \mathbb{S}_{++}^{n}, \\ s \in \mathbb{R}_{+} \end{array}` - - :math:`\{q \in \mathbb{R}^{n} \mid (q - q^{0})^{\intercal} P^{-1} (q - q^{0}) \leq s\}` - * - :class:`~pyomo.contrib.pyros.uncertainty_sets.DiscreteScenarioSet` - - :math:`q^{1}, q^{2},\dots , q^{S} \in \mathbb{R}^{n}` - - :math:`\{q^{1}, q^{2}, \dots , q^{S}\}` - * - :class:`~pyomo.contrib.pyros.uncertainty_sets.IntersectionSet` - - :math:`\mathcal{Q}_{1}, \mathcal{Q}_{2}, \dots , \mathcal{Q}_{m} \subset \mathbb{R}^{n}` - - :math:`\displaystyle \bigcap_{i=1}^{m} \mathcal{Q}_{i}` +.. list-table:: Mathematical definitions of PyROS uncertainty sets of dimension :math:`n`. + :header-rows: 1 + :class: scrollwide + + * - Uncertainty Set Type + - Input Data + - Mathematical Definition + * - :class:`~pyomo.contrib.pyros.uncertainty_sets.BoxSet` + - :math:`\begin{array}{l} q ^{\text{L}} \in \mathbb{R}^{n}, \\ q^{\text{U}} \in \mathbb{R}^{n} \end{array}` + - :math:`\{q \in \mathbb{R}^n \mid q^\mathrm{L} \leq q \leq q^\mathrm{U}\}` + * - :class:`~pyomo.contrib.pyros.uncertainty_sets.CardinalitySet` + - :math:`\begin{array}{l} q^{0} \in \mathbb{R}^{n}, \\ \hat{q} \in \mathbb{R}_{+}^{n}, \\ \Gamma \in [0, n] \end{array}` + - :math:`\left\{ q \in \mathbb{R}^{n} \middle| \begin{array}{l} \exists\,\xi \in [0, 1]^n\,:\\ \quad \,q = q^{0} + \hat{q} \circ \xi \\ \quad \displaystyle \sum_{i=1}^{n} \xi_{i} \leq \Gamma \end{array} \right\}` + * - :class:`~pyomo.contrib.pyros.uncertainty_sets.BudgetSet` + - :math:`\begin{array}{l} q^{0} \in \mathbb{R}^{n}, \\ b \in \mathbb{R}_{+}^{L}, \\ B \in \{0, 1\}^{L \times n} \end{array}` + - :math:`\left\{ q \in \mathbb{R}^{n} \middle| \begin{array}{l} \begin{pmatrix} B \\ -I \end{pmatrix} q \leq \begin{pmatrix} b + Bq^{0} \\ -q^{0} \end{pmatrix} \end{array} \right\}` + * - :class:`~pyomo.contrib.pyros.uncertainty_sets.FactorModelSet` + - :math:`\begin{array}{l} q^{0} \in \mathbb{R}^{n}, \\ \Psi \in \mathbb{R}^{n \times F}, \\ \beta \in [0, 1] \end{array}` + - :math:`\left\{ q \in \mathbb{R}^{n} \middle| \begin{array}{l} \exists\,\xi \in [-1, 1]^F \,:\\ \quad q = q^{0} + \Psi \xi \\ \quad \displaystyle\bigg| \sum_{j=1}^{F} \xi_{j} \bigg| \leq \beta F \end{array} \right\}` + * - :class:`~pyomo.contrib.pyros.uncertainty_sets.PolyhedralSet` + - :math:`\begin{array}{l} A \in \mathbb{R}^{m \times n}, \\ b \in \mathbb{R}^{m}\end{array}` + - :math:`\{q \in \mathbb{R}^{n} \mid A q \leq b\}` + * - :class:`~pyomo.contrib.pyros.uncertainty_sets.AxisAlignedEllipsoidalSet` + - :math:`\begin{array}{l} q^0 \in \mathbb{R}^{n}, \\ \alpha \in \mathbb{R}_{+}^{n} \end{array}` + - :math:`\left\{ q \in \mathbb{R}^{n} \middle| \begin{array}{l} \displaystyle\sum_{\substack{i = 1: \\ \alpha_{i} > 0}}^{n} \left(\frac{q_{i} - q_{i}^{0}}{\alpha_{i}}\right)^2 \leq 1 \\ q_{i} = q_{i}^{0} \,\forall\,i : \alpha_{i} = 0 \end{array} \right\}` + * - :class:`~pyomo.contrib.pyros.uncertainty_sets.EllipsoidalSet` + - :math:`\begin{array}{l} q^0 \in \mathbb{R}^n, \\ P \in \mathbb{S}_{++}^{n}, \\ s \in \mathbb{R}_{+} \end{array}` + - :math:`\{q \in \mathbb{R}^{n} \mid (q - q^{0})^{\intercal} P^{-1} (q - q^{0}) \leq s\}` + * - :class:`~pyomo.contrib.pyros.uncertainty_sets.DiscreteScenarioSet` + - :math:`q^{1}, q^{2},\dots , q^{S} \in \mathbb{R}^{n}` + - :math:`\{q^{1}, q^{2}, \dots , q^{S}\}` + * - :class:`~pyomo.contrib.pyros.uncertainty_sets.IntersectionSet` + - :math:`\mathcal{Q}_{1}, \mathcal{Q}_{2}, \dots , \mathcal{Q}_{m} \subset \mathbb{R}^{n}` + - :math:`\displaystyle \bigcap_{i=1}^{m} \mathcal{Q}_{i}` From ce3c546d804ecb167350f606a04d4452ab0609bf Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 26 Feb 2025 19:59:48 -0500 Subject: [PATCH 14/67] Update summaries of `BoxSet` and `DiscreteScenarioSet` docs --- pyomo/contrib/pyros/uncertainty_sets.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index a69604957a6..36085d8b203 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -952,7 +952,7 @@ def dim(self): class BoxSet(UncertaintySet): """ - A hyper-rectangle (i.e., "box"). + A hyperrectangle (or box). Parameters ---------- @@ -2889,8 +2889,7 @@ def set_as_constraint(self, uncertain_params=None, block=None): class DiscreteScenarioSet(UncertaintySet): """ - A discrete set of finitely many uncertain parameter realizations - (or scenarios). + A set of finitely many distinct points (or scenarios). Parameters ---------- From c0ff86f024ebd8572324ff7c001756d1f3036f0c Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 26 Feb 2025 23:44:03 -0500 Subject: [PATCH 15/67] Reorder rows in uncertainty sets math table --- .../solvers/pyrosdir/uncertainty_sets.rst | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/doc/OnlineDocs/explanation/solvers/pyrosdir/uncertainty_sets.rst b/doc/OnlineDocs/explanation/solvers/pyrosdir/uncertainty_sets.rst index 23acb5984e9..04a4d5e99de 100644 --- a/doc/OnlineDocs/explanation/solvers/pyrosdir/uncertainty_sets.rst +++ b/doc/OnlineDocs/explanation/solvers/pyrosdir/uncertainty_sets.rst @@ -42,30 +42,30 @@ subclasses are provided below. * - Uncertainty Set Type - Input Data - Mathematical Definition + * - :class:`~pyomo.contrib.pyros.uncertainty_sets.AxisAlignedEllipsoidalSet` + - :math:`\begin{array}{l} q^0 \in \mathbb{R}^{n}, \\ \alpha \in \mathbb{R}_{+}^{n} \end{array}` + - :math:`\left\{ q \in \mathbb{R}^{n} \middle| \begin{array}{l} \displaystyle\sum_{\substack{i = 1: \\ \alpha_{i} > 0}}^{n} \left(\frac{q_{i} - q_{i}^{0}}{\alpha_{i}}\right)^2 \leq 1 \\ q_{i} = q_{i}^{0} \,\forall\,i : \alpha_{i} = 0 \end{array} \right\}` * - :class:`~pyomo.contrib.pyros.uncertainty_sets.BoxSet` - :math:`\begin{array}{l} q ^{\text{L}} \in \mathbb{R}^{n}, \\ q^{\text{U}} \in \mathbb{R}^{n} \end{array}` - :math:`\{q \in \mathbb{R}^n \mid q^\mathrm{L} \leq q \leq q^\mathrm{U}\}` - * - :class:`~pyomo.contrib.pyros.uncertainty_sets.CardinalitySet` - - :math:`\begin{array}{l} q^{0} \in \mathbb{R}^{n}, \\ \hat{q} \in \mathbb{R}_{+}^{n}, \\ \Gamma \in [0, n] \end{array}` - - :math:`\left\{ q \in \mathbb{R}^{n} \middle| \begin{array}{l} \exists\,\xi \in [0, 1]^n\,:\\ \quad \,q = q^{0} + \hat{q} \circ \xi \\ \quad \displaystyle \sum_{i=1}^{n} \xi_{i} \leq \Gamma \end{array} \right\}` * - :class:`~pyomo.contrib.pyros.uncertainty_sets.BudgetSet` - :math:`\begin{array}{l} q^{0} \in \mathbb{R}^{n}, \\ b \in \mathbb{R}_{+}^{L}, \\ B \in \{0, 1\}^{L \times n} \end{array}` - :math:`\left\{ q \in \mathbb{R}^{n} \middle| \begin{array}{l} \begin{pmatrix} B \\ -I \end{pmatrix} q \leq \begin{pmatrix} b + Bq^{0} \\ -q^{0} \end{pmatrix} \end{array} \right\}` - * - :class:`~pyomo.contrib.pyros.uncertainty_sets.FactorModelSet` - - :math:`\begin{array}{l} q^{0} \in \mathbb{R}^{n}, \\ \Psi \in \mathbb{R}^{n \times F}, \\ \beta \in [0, 1] \end{array}` - - :math:`\left\{ q \in \mathbb{R}^{n} \middle| \begin{array}{l} \exists\,\xi \in [-1, 1]^F \,:\\ \quad q = q^{0} + \Psi \xi \\ \quad \displaystyle\bigg| \sum_{j=1}^{F} \xi_{j} \bigg| \leq \beta F \end{array} \right\}` - * - :class:`~pyomo.contrib.pyros.uncertainty_sets.PolyhedralSet` - - :math:`\begin{array}{l} A \in \mathbb{R}^{m \times n}, \\ b \in \mathbb{R}^{m}\end{array}` - - :math:`\{q \in \mathbb{R}^{n} \mid A q \leq b\}` - * - :class:`~pyomo.contrib.pyros.uncertainty_sets.AxisAlignedEllipsoidalSet` - - :math:`\begin{array}{l} q^0 \in \mathbb{R}^{n}, \\ \alpha \in \mathbb{R}_{+}^{n} \end{array}` - - :math:`\left\{ q \in \mathbb{R}^{n} \middle| \begin{array}{l} \displaystyle\sum_{\substack{i = 1: \\ \alpha_{i} > 0}}^{n} \left(\frac{q_{i} - q_{i}^{0}}{\alpha_{i}}\right)^2 \leq 1 \\ q_{i} = q_{i}^{0} \,\forall\,i : \alpha_{i} = 0 \end{array} \right\}` - * - :class:`~pyomo.contrib.pyros.uncertainty_sets.EllipsoidalSet` - - :math:`\begin{array}{l} q^0 \in \mathbb{R}^n, \\ P \in \mathbb{S}_{++}^{n}, \\ s \in \mathbb{R}_{+} \end{array}` - - :math:`\{q \in \mathbb{R}^{n} \mid (q - q^{0})^{\intercal} P^{-1} (q - q^{0}) \leq s\}` + * - :class:`~pyomo.contrib.pyros.uncertainty_sets.CardinalitySet` + - :math:`\begin{array}{l} q^{0} \in \mathbb{R}^{n}, \\ \hat{q} \in \mathbb{R}_{+}^{n}, \\ \Gamma \in [0, n] \end{array}` + - :math:`\left\{ q \in \mathbb{R}^{n} \middle| \begin{array}{l} \exists\,\xi \in [0, 1]^n\,:\\ \quad \,q = q^{0} + \hat{q} \circ \xi \\ \quad \displaystyle \sum_{i=1}^{n} \xi_{i} \leq \Gamma \end{array} \right\}` * - :class:`~pyomo.contrib.pyros.uncertainty_sets.DiscreteScenarioSet` - :math:`q^{1}, q^{2},\dots , q^{S} \in \mathbb{R}^{n}` - :math:`\{q^{1}, q^{2}, \dots , q^{S}\}` + * - :class:`~pyomo.contrib.pyros.uncertainty_sets.EllipsoidalSet` + - :math:`\begin{array}{l} q^0 \in \mathbb{R}^n, \\ P \in \mathbb{S}_{++}^{n}, \\ s \in \mathbb{R}_{+} \end{array}` + - :math:`\{q \in \mathbb{R}^{n} \mid (q - q^{0})^{\intercal} P^{-1} (q - q^{0}) \leq s\}` + * - :class:`~pyomo.contrib.pyros.uncertainty_sets.FactorModelSet` + - :math:`\begin{array}{l} q^{0} \in \mathbb{R}^{n}, \\ \Psi \in \mathbb{R}^{n \times F}, \\ \beta \in [0, 1] \end{array}` + - :math:`\left\{ q \in \mathbb{R}^{n} \middle| \begin{array}{l} \exists\,\xi \in [-1, 1]^F \,:\\ \quad q = q^{0} + \Psi \xi \\ \quad \displaystyle\bigg| \sum_{j=1}^{F} \xi_{j} \bigg| \leq \beta F \end{array} \right\}` * - :class:`~pyomo.contrib.pyros.uncertainty_sets.IntersectionSet` - :math:`\mathcal{Q}_{1}, \mathcal{Q}_{2}, \dots , \mathcal{Q}_{m} \subset \mathbb{R}^{n}` - :math:`\displaystyle \bigcap_{i=1}^{m} \mathcal{Q}_{i}` + * - :class:`~pyomo.contrib.pyros.uncertainty_sets.PolyhedralSet` + - :math:`\begin{array}{l} A \in \mathbb{R}^{m \times n}, \\ b \in \mathbb{R}^{m}\end{array}` + - :math:`\{q \in \mathbb{R}^{n} \mid A q \leq b\}` From fdcf9c95f714892223e86bad35857f0a84ee745f Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 26 Feb 2025 23:44:23 -0500 Subject: [PATCH 16/67] Update new PyROS docs homepage --- doc/OnlineDocs/explanation/solvers/pyrosdir/index.rst | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/doc/OnlineDocs/explanation/solvers/pyrosdir/index.rst b/doc/OnlineDocs/explanation/solvers/pyrosdir/index.rst index c1bd53b75eb..266a1ecf25f 100644 --- a/doc/OnlineDocs/explanation/solvers/pyrosdir/index.rst +++ b/doc/OnlineDocs/explanation/solvers/pyrosdir/index.rst @@ -1,9 +1,6 @@ PyROS Solver ============ -Introduction ------------- - PyROS (Pyomo Robust Optimization Solver) is a Pyomo-based meta-solver for non-convex, two-stage adjustable robust optimization problems. @@ -13,12 +10,9 @@ in collaboration with **John D. Siirola** of Sandia National Labs. The developers gratefully acknowledge support from the U.S. Department of Energy's `Institute for the Design of Advanced Energy Systems (IDAES) `_. - -Contents --------- - .. toctree:: :maxdepth: 1 + :caption: Index of PyROS Documentation Overview Getting Started @@ -29,7 +23,8 @@ Contents Citing PyROS ------------ -If you use PyROS, please consider citing the publication [IAE+21]_. +If you use PyROS in your research, +please acknowledge PyROS by citing [IAE+21]_. Feedback and Reporting Issues From 7858bff64977e2ec879b1675cf7e65cfe65094a3 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 27 Feb 2025 15:37:04 -0500 Subject: [PATCH 17/67] Restructure the uncertainty sets guide --- .../solvers/pyrosdir/uncertainty_sets.rst | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/doc/OnlineDocs/explanation/solvers/pyrosdir/uncertainty_sets.rst b/doc/OnlineDocs/explanation/solvers/pyrosdir/uncertainty_sets.rst index 04a4d5e99de..9652b4bdf40 100644 --- a/doc/OnlineDocs/explanation/solvers/pyrosdir/uncertainty_sets.rst +++ b/doc/OnlineDocs/explanation/solvers/pyrosdir/uncertainty_sets.rst @@ -1,7 +1,16 @@ .. _pyros_uncertainty_sets: +====================== PyROS Uncertainty Sets ====================== + +.. contents:: Table of Contents + :depth: 1 + :local: + + +Overview +======== In PyROS, the uncertainty set of a robust optimization problem is represented by an instance of a subclass of the :class:`~pyomo.contrib.pyros.uncertainty_sets.UncertaintySet` @@ -11,7 +20,12 @@ PyROS provides a suite of pre-implemented concrete subclasses to facilitate instantiation of uncertainty sets that are commonly used in the optimization literature. Custom uncertainty set types can be implemented by subclassing -:class:`~pyomo.contrib.pyros.uncertainty_sets.UncertaintySet`. +:class:`~pyomo.contrib.pyros.uncertainty_sets.UncertaintySet`, +though there are limitations. + + +Pre-Implemented Uncertainty Set Types +===================================== The pre-implemented subclasses are enumerated below: .. autosummary:: @@ -26,8 +40,6 @@ The pre-implemented subclasses are enumerated below: ~pyomo.contrib.pyros.uncertainty_sets.IntersectionSet ~pyomo.contrib.pyros.uncertainty_sets.PolyhedralSet -.. - Everything inside this block is commented out. Mathematical definitions of the pre-implemented :class:`~pyomo.contrib.pyros.uncertainty_sets.UncertaintySet` @@ -69,3 +81,16 @@ subclasses are provided below. * - :class:`~pyomo.contrib.pyros.uncertainty_sets.PolyhedralSet` - :math:`\begin{array}{l} A \in \mathbb{R}^{m \times n}, \\ b \in \mathbb{R}^{m}\end{array}` - :math:`\{q \in \mathbb{R}^{n} \mid A q \leq b\}` + + +Custom Uncertainty Set Types +============================ +A custom uncertainty set type +in which all uncertain parameters +are modeled as continuous quantities +can be implemented by subclassing +:class:`~pyomo.contrib.pyros.uncertainty_sets.UncertaintySet`. +For discrete sets, we recommend using the pre-implemented +:class:`~pyomo.contrib.pyros.uncertainty_sets.DiscreteScenarioSet` +subclass instead of implementing a new set type. +PyROS does not support mixed-integer uncertainty set types. From 95dbbc6cd7045ea6337f43b3bcbcf731f3100ee1 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 27 Feb 2025 15:37:24 -0500 Subject: [PATCH 18/67] Restructure the solver log guide --- doc/OnlineDocs/explanation/solvers/pyrosdir/solver_log.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_log.rst b/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_log.rst index 0ec43d853c9..e4bc10ee60b 100644 --- a/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_log.rst +++ b/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_log.rst @@ -1,5 +1,6 @@ .. _pyros_solver_log: +======================= PyROS Solver Output Log ======================= @@ -11,7 +12,7 @@ PyROS Solver Output Log .. _pyros_solver_log_appearance: Default Format --------------- +============== When the PyROS :meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` method @@ -252,7 +253,7 @@ table below: .. _pyros_solver_log_verbosity: Configuring the Output Log --------------------------- +========================== For a given call to the PyROS :meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` method, From 778435d9755482666c174243674103ec00bde8a0 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 27 Feb 2025 15:37:38 -0500 Subject: [PATCH 19/67] Update index headings --- doc/OnlineDocs/explanation/solvers/pyrosdir/index.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/OnlineDocs/explanation/solvers/pyrosdir/index.rst b/doc/OnlineDocs/explanation/solvers/pyrosdir/index.rst index 266a1ecf25f..e6dd30ae49b 100644 --- a/doc/OnlineDocs/explanation/solvers/pyrosdir/index.rst +++ b/doc/OnlineDocs/explanation/solvers/pyrosdir/index.rst @@ -1,3 +1,4 @@ +============ PyROS Solver ============ @@ -22,12 +23,12 @@ The developers gratefully acknowledge support from the U.S. Department of Energy Citing PyROS ------------- +============ If you use PyROS in your research, please acknowledge PyROS by citing [IAE+21]_. Feedback and Reporting Issues ------------------------------ +============================= Please provide feedback and/or report any problems by `opening an issue on the Pyomo GitHub page `_. From b7b07ff5b976693ae8c7b3b97cb5c3d452378e97 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 27 Feb 2025 19:49:22 -0500 Subject: [PATCH 20/67] Tweak solver output log intro --- doc/OnlineDocs/explanation/solvers/pyrosdir/solver_log.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_log.rst b/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_log.rst index e4bc10ee60b..622fda2b1d3 100644 --- a/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_log.rst +++ b/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_log.rst @@ -17,7 +17,8 @@ Default Format When the PyROS :meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` method is invoked on a robust optimization problem, -your console output will, by default, look like this: +your console output will, by default, look like this +(line numbers added for reference): .. _solver-log-snippet: From 5711f30fa96066c82a5eb179feba2e0d18273374 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 27 Feb 2025 19:50:23 -0500 Subject: [PATCH 21/67] Restructure tutorial and solver interface docs --- .../solvers/pyrosdir/getting_started.rst | 219 ++++++------------ .../explanation/solvers/pyrosdir/index.rst | 2 +- .../explanation/solvers/pyrosdir/overview.rst | 7 +- .../solvers/pyrosdir/solver_interface.rst | 181 +++++++++++++-- 4 files changed, 244 insertions(+), 165 deletions(-) diff --git a/doc/OnlineDocs/explanation/solvers/pyrosdir/getting_started.rst b/doc/OnlineDocs/explanation/solvers/pyrosdir/getting_started.rst index 1e1ca7850c4..359cce55584 100644 --- a/doc/OnlineDocs/explanation/solvers/pyrosdir/getting_started.rst +++ b/doc/OnlineDocs/explanation/solvers/pyrosdir/getting_started.rst @@ -1,15 +1,16 @@ .. _pyros_installation: +========================== Getting Started with PyROS ========================== .. contents:: Table of Contents - :depth: 2 + :depth: 3 :local: Installation ------------- +============ PyROS can be installed as follows: 1. :ref:`Install Pyomo `. @@ -46,8 +47,9 @@ PyROS can be installed as follows: that you have pre-installed and licensed on your system. Usage Tutorial --------------- -In this tutorial, we will solve robust optimization problems with PyROS. +============== +In this tutorial, we will use PyROS to solve a few robust +optimization problems. The problems are derived from the deterministic model *hydro*, a QCQP taken from the `GAMS Model Library `_. @@ -66,18 +68,6 @@ Moreover, there are and a quadratic objective. **All variables of the model are continuous.** -.. note:: - PyROS does not support models with discrete decision variables. - -.. note:: - Per our analysis, the model *hydro* satisfies the requirement that - each value of :math:`\left(x, z, q \right)` maps to a unique - value of :math:`y`, which, in accordance with - :ref:`our assumption on the equality constraints `, - indicates a proper partitioning of the model variables - into (first-stage and second-stage) degrees of freedom and - state variables. - We have extended this model by converting one objective coefficient, two constraint coefficients, and one constraint right-hand side into :class:`~pyomo.core.base.param.Param` objects @@ -85,7 +75,7 @@ so that they can be considered uncertain later on. Step 0: Import Pyomo and the PyROS Module -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +----------------------------------------- In anticipation of using the PyROS solver and building the deterministic Pyomo model: @@ -97,8 +87,11 @@ model: >>> import pyomo.environ as pyo >>> import pyomo.contrib.pyros as pyros -Step 1: Define the Deterministic Problem -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Step 1: Define the Solver Inputs +-------------------------------- + +Deterministic Model +^^^^^^^^^^^^^^^^^^^ The deterministic Pyomo model for *hydro* is constructed as follows. We first instantiate a Pyomo model object: @@ -123,39 +116,11 @@ These are represented by mutable :class:`~pyomo.core.base.param.Param` objects: ... ) .. note:: - Primitive data (Python literals) that have been hard-coded within a - deterministic model (:class:`~pyomo.core.base.PyomoModel.ConcreteModel`) - cannot be later considered uncertain, - unless they are first converted to Pyomo - :class:`~pyomo.core.base.param.Param` instances declared on the - :class:`~pyomo.core.base.PyomoModel.ConcreteModel` object. - Furthermore, any :class:`~pyomo.core.base.param.Param` - object that is to be later considered uncertain must be instantiated - with the argument ``mutable=True``. - -.. note:: - If specifying/modifying the ``mutable`` argument in the - :class:`~pyomo.core.base.param.Param` declarations - of your deterministic model source code - is not straightforward in your context, then - you may consider adding **after** the - :ref:`Pyomo/PyROS module imports ` - but **before** - :ref:`instantiating the model object ` - the statement: - - .. code:: - - pyo.Param.DefaultMutable = True - - For all :class:`~pyomo.core.base.param.Param` - objects declared after this statement, - the attribute ``mutable`` is set to True by default. - Hence, non-mutable :class:`~pyomo.core.base.param.Param` - objects are now declared by explicitly passing the argument - ``mutable=False`` to the :class:`~pyomo.core.base.param.Param` - constructor. - + Uncertain parameters cannot be represented directly by + primitive data (Python literals) that have been hard-coded within a + deterministic model (:class:`~pyomo.core.base.PyomoModel.ConcreteModel`). + See the + :ref:`Uncertain parameters section of the solver interface overview `. Finally, we declare the decision variables, objective, and constraints: @@ -248,9 +213,14 @@ We have elected to use BARON as the solver: Optimal deterministic objective value: 3.5838e+07 -Step 2: Define the Uncertainty Quantification -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +First-Stage and Second-Stage Variables +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +We will define the first-stage and second-stage variables +later for each of two separate cases. + +Uncertain Parameters +^^^^^^^^^^^^^^^^^^^^ We first collect the components of our model that represent the uncertain parameters. In this example, we assume that the quantities @@ -270,16 +240,8 @@ one of the following: * ``[m.q[0], m.q[1], m.q[2], m.q[3]]`` * ``list(m.q.values())`` -.. note:: - 1. Any :class:`~pyomo.core.base.param.Param` object that - represents an uncertain parameter must be instantiated - with the constructor argument ``mutable=True``. - 2. Uncertain parameters can also be represented by - :class:`~pyomo.core.base.var.Var` - objects declared on the deterministic model. - Prior to invoking PyROS, - all such :class:`~pyomo.core.base.var.Var` objects should be fixed. - +Uncertainty Set +^^^^^^^^^^^^^^^ PyROS requires an uncertainty set against which to robustly optimize the model. @@ -292,8 +254,7 @@ an instance of a subclass of the In the present example, let us assume that each uncertain parameter can -independently of the other uncertain parameters -deviate from the parameter's nominal value by up to :math:`\pm 15\%`. +independently deviate from its nominal value by up to :math:`\pm 15\%`. Then the parameter values are constrained to a box region, which we can implement as an instance of the :class:`~pyomo.contrib.pyros.uncertainty_sets.BoxSet` subclass: @@ -309,22 +270,15 @@ which we can implement as an instance of the Further information on PyROS uncertainty sets is presented in the :ref:`Uncertainty Sets section `. - -Step 3: Solve With PyROS -^^^^^^^^^^^^^^^^^^^^^^^^^^ -PyROS can be instantiated through the Pyomo -:class:`~pyomo.opt.base.solvers.SolverFactory`: - -.. doctest:: - - >>> pyros_solver = pyo.SolverFactory("pyros") - -PyROS requires the user to supply one subordinate local NLP optimizer +Subordinate NLP Solvers +^^^^^^^^^^^^^^^^^^^^^^^ +PyROS requires at least one subordinate local NLP optimizer and one subordinate global NLP optimizer for solving subproblems. For convenience, we shall have PyROS use :ref:`the previously instantiated BARON solver ` as both the subordinate local and global NLP solvers: + .. doctest:: :skipif: not (baron.available() and baron.license_is_valid()) @@ -339,14 +293,23 @@ as both the subordinate local and global NLP solvers: condition. These alternative solvers are provided through the optional keyword arguments ``backup_local_solvers`` and ``backup_global_solvers``. + +Step 2: Solve With PyROS +------------------------ +PyROS can be instantiated through the Pyomo +:class:`~pyomo.opt.base.solvers.SolverFactory`: + +.. doctest:: + + >>> pyros_solver = pyo.SolverFactory("pyros") + The final step in solving a model with PyROS is to construct the remaining required inputs, namely ``first_stage_variables`` and ``second_stage_variables``. Below, we present two separate cases. - A Single-Stage Problem -""""""""""""""""""""""""" +^^^^^^^^^^^^^^^^^^^^^^ We can use PyROS to solve a single-stage robust optimization problem, in which all independent variables are designated to be first-stage. In the present example, the independent variables are @@ -408,7 +371,7 @@ For further information on the output log, see the :ref:`Solver Output Log section `. A Two-Stage Problem -"""""""""""""""""""""" +^^^^^^^^^^^^^^^^^^^ Let us now assume that some of the independent variables are second-stage: .. doctest:: @@ -418,6 +381,14 @@ Let us now assume that some of the independent variables are second-stage: >>> second_stage_variables = [m.x1, m.x2, m.x3, m.x4, m.x20, m.x21] +.. note:: + Per our analysis, our selections of first-stage variables + and second-stage variables for the model *hydro* + in both the single-stage problem and the two-stage problem + satisfy our + :ref:`assumption that the state variable values are uniquely defined `. + + PyROS uses polynomial decision rules to approximate the adjustability of the second-stage variables to the uncertain parameters. The degree of the decision rule polynomials is @@ -527,8 +498,8 @@ passed through ``options``. setting are ignored. -Step 4: Check the Results -^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Step 3: Check the Outputs +-------------------------- The PyROS :meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` method returns a results object, of type :class:`~pyomo.contrib.pyros.solve_data.ROSolveResults`, @@ -560,50 +531,10 @@ We can also query the results object's individual attributes: pyrosTerminationCondition.robust_optimal -The ``pyros_termination_condition`` attribute of the resuls object -is a member of the -:class:`~pyomo.contrib.pyros.util.pyrosTerminationCondition` enumeration. - -.. _pyros_robust_optimality_args: - -.. note:: - - When the PyROS :meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` method - has successfully solved a given robust optimization problem, - the :attr:`~pyomo.contrib.pyros.solve_data.ROSolveResults.pyros_termination_condition` - attribute of the returned - :class:`~pyomo.contrib.pyros.solve_data.ROSolveResults` - object is set to - :attr:`~pyomo.contrib.pyros.util.pyrosTerminationCondition.robust_optimal` - only if: - - 1. Master problems are solved to global optimality - (by specifying ``solve_master_globally=True``) - 2. A worst-case objective focus is chosen - (by specifying ``objective_focus=pyros.ObjectiveType.worst_case``) - - Otherwise, the termination condition is set to - :attr:`~pyomo.contrib.pyros.util.pyrosTerminationCondition.robust_feasible`. - -.. note:: - - The reported objective and variable values - depend on the value of the option ``objective_focus``: - - * If ``objective_focus=pyros.ObjectiveType.nominal``, - then the objective, second-stage variables, and - state variables are evaluated at - the nominal uncertain parameter realization. - * If ``objective_focus=pyros.ObjectiveType.worst_case``, - then the objective, second-stage variables, and - state variables are evaluated at - the worst-case uncertain parameter realization. - - We expect that adding second-stage recourse to the single-stage *hydro* problem results in a reduction in the robust optimal objective value. -To confirm our expectation, the final objectives can be compared as follows: +To confirm our expectation, the final objectives can be compared: .. doctest:: :skipif: not (baron.available() and baron.license_is_valid()) @@ -625,33 +556,28 @@ Our check confirms that there is a ~25% decrease in the final objective value when switching from a static decision rule (no second-stage recourse) to an affine decision rule. -We can also inspect the state of the model after the solution -has been loaded by invoking ``m.display()`` or ``m.pprint()``. - -.. note:: - - PyROS loads the final solution to the deterministic model only if: - - 1. The argument ``load_solution=True`` has been passed to PyROS - (occurs by default) - 2. The termination condition is either - :attr:`~pyomo.contrib.pyros.util.pyrosTerminationCondition.robust_optimal` - or - :attr:`~pyomo.contrib.pyros.util.pyrosTerminationCondition.robust_feasible` - - Otherwise, the final solution is lost. +Since PyROS has successfully solved our problem, +the final solution has been automatically loaded to the model. +We can inspect the resulting state of the model +by invoking, for example, ``m.display()`` or ``m.pprint()``. +For a general discussion of the PyROS solver outputs, +see the +:ref:`Overview of Outputs section of the Solver Interface documentation `. Analyzing the Price of Robustness -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +--------------------------------- +In conjunction with standard Pyomo control flow tools, PyROS facilitates an analysis of the "price of robustness", which we define to be the increase in the robust optimal objective value -relative to the deterministically optimal objective value +relative to the deterministically optimal objective value. + Let us, for example, consider optimizing robustly against a box uncertainty set centered on the nominal realization of the uncertain parameters and parameterized by a value :math:`p \geq 0` -specifying the half-lengths of the box relative to the nominal realization. +specifying the half-length of the box relative to the nominal realization +in each dimension. Then the box set is defined by: .. math:: @@ -661,11 +587,10 @@ Then the box set is defined by: in which :math:`q^\text{nom}` denotes the nominal realization. We can optimize against box sets of increasing normalized half-length :math:`p` -by constructing a corresponding -:class:`~pyomo.contrib.pyros.uncertainty_sets.BoxSet` -instance and invoking the -:meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` method -in a for-loop: +by iterating over select values of :math:`p` in a ``for`` loop, +and in each iteration, solving a robust optimization problem +subject to a corresponding +:class:`~pyomo.contrib.pyros.uncertainty_sets.BoxSet` instance: .. code:: @@ -708,6 +633,7 @@ we can print a tabular summary of the results: .. code:: + >>> # table header >>> print("=" * 80) >>> print( ... f"{'Relative Half-Len.':20s}", @@ -723,6 +649,7 @@ we can print a tabular summary of the results: ... == pyros.pyrosTerminationCondition.robust_optimal ... ) ... if is_robust_optimal: + ... # compute the price of robustness ... obj_value = res.final_objective_value ... price_of_robustness = ( ... (res.final_objective_value - deterministic_obj) diff --git a/doc/OnlineDocs/explanation/solvers/pyrosdir/index.rst b/doc/OnlineDocs/explanation/solvers/pyrosdir/index.rst index e6dd30ae49b..a209b42b7d7 100644 --- a/doc/OnlineDocs/explanation/solvers/pyrosdir/index.rst +++ b/doc/OnlineDocs/explanation/solvers/pyrosdir/index.rst @@ -15,7 +15,7 @@ The developers gratefully acknowledge support from the U.S. Department of Energy :maxdepth: 1 :caption: Index of PyROS Documentation - Overview + Methodology Overview Getting Started Solver Interface Uncertainty Sets diff --git a/doc/OnlineDocs/explanation/solvers/pyrosdir/overview.rst b/doc/OnlineDocs/explanation/solvers/pyrosdir/overview.rst index 8090cdde86c..3c97aaeaa60 100644 --- a/doc/OnlineDocs/explanation/solvers/pyrosdir/overview.rst +++ b/doc/OnlineDocs/explanation/solvers/pyrosdir/overview.rst @@ -1,7 +1,8 @@ .. _pyros_overview: -PyROS Overview -============== +========================== +PyROS Methodology Overview +========================== PyROS (Pyomo Robust Optimization Solver) is a Pyomo-based meta-solver for non-convex, two-stage adjustable robust optimization problems. @@ -86,7 +87,7 @@ and then, using the Generalized Robust Cutting-Set algorithm developed in [IAE+21]_, seeks a solution to the robust counterpart. When using PyROS, please consider citing [IAE+21]_. -.. _unique-mapping: +.. _pyros_unique_state_vars: .. note:: A key assumption of PyROS is that diff --git a/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_interface.rst b/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_interface.rst index 3ef69a45921..43c904dd230 100644 --- a/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_interface.rst +++ b/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_interface.rst @@ -1,10 +1,20 @@ .. _pyros_solver_interface: +====================== PyROS Solver Interface ====================== -Like other Pyomo solvers, the PyROS solver can be instantiated directly -or through the Pyomo :class:`~pyomo.opt.base.solvers.SolverFactory`: +.. contents:: Table of Contents + :depth: 2 + :local: + +Instantiation +============= + +The PyROS solver is invoked through the +:meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` method +of an instance of the PyROS solver class, which can be +instantiated as follows: .. code:: @@ -12,20 +22,161 @@ or through the Pyomo :class:`~pyomo.opt.base.solvers.SolverFactory`: >>> import pyomo.contrib.pyros as pyros # register the PyROS solver >>> pyros_solver = pyo.SolverFactory("pyros") -Subsequently, the solver in invoked by calling the -:meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` method, -the required inputs of which are the: -* Deterministic optimization model -* First-stage ("design") variables -* Second-stage ("control") variables -* Parameters considered uncertain -* Uncertainty set -* Subordinate local and global nonlinear programming (NLP) solvers +Overview of Inputs +================== +Deterministic Model +------------------- +PyROS is designed to operate on a single-objective deterministic model, +from which the robust optimization counterpart is automatically inferred. +All variables of the model should be continuous, as +mixed-integer problems are not supported. + +First-Stage and Second-Stage Variables +-------------------------------------- +A model may have either first-stage variables, second-stage variables, +or both. +Any variable of the model that is excluded from the lists +of first-stage and second-stage variables +is automatically considered to be a state variable. + -See the :py:meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` -method for further information. +.. _pyros_uncertain_params: + +Uncertain Parameters +-------------------- +Uncertain parameters can be represented by either +mutable :class:`~pyomo.core.base.param.Param` +or fixed :class:`~pyomo.core.base.var.Var` objects. +Uncertain parameters *cannot* be directly +represented by Python literals that have been hard-coded into the +deterministic model. + +A :class:`~pyomo.core.base.param.Param` object can be made mutable +at construction by passing the argument ``mutable=True`` to the +:class:`~pyomo.core.base.param.Param` constructor. +If specifying/modifying the ``mutable`` argument +is not straightforward in your context, +then add the following lines of code to your script +before setting up your deterministic model: + + +.. code:: + + import pyomo.environ as pyo + pyo.Param.DefaultMutable = True + +All :class:`~pyomo.core.base.param.Param` objects declared +after the preceding code statements will be made mutable by default. + + +Uncertainty Set +--------------- +See the :ref:`pyros_uncertainty_sets` section. + +Subordinate NLP Solvers +----------------------- +PyROS requires at least one subordinate +local nonlinear programming (NLP) solver (e.g., Ipopt or CONOPT) +and subordinate global NLP solver (e.g., BARON or SCIP) +to solve subproblems. .. note:: - Any variables in the model not specified to be first-stage or second-stage - variables are automatically considered to be state variables. + + In advance of invoking the PyROS solver, + check that your deterministic model can be solved + to optimality by either your subordinate local or global + NLP solver. + +Optional Arguments +------------------ +The optional arguments are enumerated in the documentation of the +:meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` method. + +.. _pyros_solver_outputs: + +Overview of Outputs +=================== + +.. _pyros_output_results_object: + +Results Object +-------------- +The :meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` method returns +an :class:`~pyomo.contrib.pyros.solve_data.ROSolveResults` object. + +When the PyROS :meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` method +has successfully solved a given robust optimization problem, +the +:attr:`~pyomo.contrib.pyros.solve_data.ROSolveResults.pyros_termination_condition` +attribute of the returned +:attr:`~pyomo.contrib.pyros.solve_data.ROSolveResults` +object is set to +:attr:`~pyomo.contrib.pyros.util.pyrosTerminationCondition.robust_optimal` +if and only if: + +1. Master problems are solved to global optimality + (by passing ``solve_master_globally=True``) +2. A worst-case objective focus is chosen + (by setting ``objective_focus`` + to :attr:`~pyomo.contrib.pyros.util.ObjectiveType.worst_case``) + +Otherwise, the termination condition is set to +:attr:`~pyomo.contrib.pyros.util.pyrosTerminationCondition.robust_feasible`. + +The +:attr:`~pyomo.contrib.pyros.solve_data.ROSolveResults.final_objective_value` +attribute of the results object depends on +the value of the optional ``objective_focus`` argument to the +:meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` method: + +* If ``objective_focus`` is set to + :attr:`~pyomo.contrib.pyros.util.ObjectiveType.nominal`, + then those variables are evaluated at + the nominal uncertain parameter realization +* If ``objective_focus`` is set to + :attr:`~pyomo.contrib.pyros.util.ObjectiveType.worst_case`, + then those variables are evaluated at + the uncertain parameter realization that induces the worst-case + objective function value + +The second-stage variable and state variable values in the +:ref:`solution loaded to the model ` +are evaluated similarly. + +.. _pyros_output_final_solution: + +Final Solution +-------------- +PyROS automatically loads the final solution found to the model +(i.e., updates the values of the variables of the determinstic model) +if and only if: + +1. The argument ``load_solution=True`` has been passed to PyROS + (occurs by default) +2. The + :attr:`~pyomo.contrib.pyros.solve_data.ROSolveResults.pyros_termination_condition` + attribute of the returned + :attr:`~pyomo.contrib.pyros.solve_data.ROSolveResults` object + is either + :attr:`~pyomo.contrib.pyros.util.pyrosTerminationCondition.robust_optimal` + or + :attr:`~pyomo.contrib.pyros.util.pyrosTerminationCondition.robust_feasible` + +Otherwise, the solution is lost. + +If a solution is loaded to the model, +then, +as mentioned in our discussion of the +:ref:`results object `, +the second-stage variables and state variables +of the model are updated according to +the value of the optional ``objective_focus`` argument to +the :meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` method. +The uncertain parameter objects are left unchanged. + + +Solver Log Output +----------------- +See the :ref:`pyros_solver_log` section for more information on the +PyROS solver log output. From a7810ec9ee0902bba093dc75e957ef4ea6b10c2e Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 27 Feb 2025 19:53:50 -0500 Subject: [PATCH 22/67] Update PyROS solver class docs --- pyomo/contrib/pyros/pyros.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index d43c8ef5690..85699a9965c 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -78,15 +78,25 @@ def _get_pyomo_version_info(): @SolverFactory.register( "pyros", - doc="Robust optimization (RO) solver implementing " - "the generalized robust cutting-set algorithm (GRCS)", + doc="Pyomo Robust Optimization Solver (PyROS): " + "implementation of a generalized robust cutting-set algorithm (GRCS)", ) class PyROS(object): - ''' - PyROS (Pyomo Robust Optimization Solver) implementing a + """ + Pyomo Robust Optimization Solver (PyROS): implementation of a generalized robust cutting-set algorithm (GRCS) - to solve two-stage NLP optimization models under uncertainty. - ''' + for the solution of two-stage nonlinear programs + under uncertainty. + + We recommend instantiating this class as follows: + + .. code:: + + >>> import pyomo.environ as pyo + >>> import pyomo.contrib.pyros as pyros + >>> pyros_solver = pyo.SolverFactory("pyros") + + """ CONFIG = pyros_config() _LOG_LINE_LENGTH = 78 From ec69cf546c2770c1ca4ffe81d52022a1501c3df9 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 27 Feb 2025 19:54:21 -0500 Subject: [PATCH 23/67] Update reference to PyROS solver page --- doc/OnlineDocs/explanation/solvers/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/explanation/solvers/index.rst b/doc/OnlineDocs/explanation/solvers/index.rst index a50a604f93e..d416603404d 100644 --- a/doc/OnlineDocs/explanation/solvers/index.rst +++ b/doc/OnlineDocs/explanation/solvers/index.rst @@ -6,7 +6,7 @@ Solvers persistent gdpopt - pyros + pyrosdir/index mindtpy mcpp multistart From 3574e6a318d6942c3287ec39efcc2c363863c02e Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 27 Feb 2025 21:01:57 -0500 Subject: [PATCH 24/67] Add sentence on state variable uniqueness assumption --- .../explanation/solvers/pyrosdir/solver_interface.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_interface.rst b/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_interface.rst index 43c904dd230..539f040845d 100644 --- a/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_interface.rst +++ b/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_interface.rst @@ -39,6 +39,8 @@ or both. Any variable of the model that is excluded from the lists of first-stage and second-stage variables is automatically considered to be a state variable. +PyROS assumes that the state variables are +:ref:`uniquely defined by the equality constraints `. .. _pyros_uncertain_params: @@ -119,7 +121,7 @@ if and only if: (by passing ``solve_master_globally=True``) 2. A worst-case objective focus is chosen (by setting ``objective_focus`` - to :attr:`~pyomo.contrib.pyros.util.ObjectiveType.worst_case``) + to :attr:`~pyomo.contrib.pyros.util.ObjectiveType.worst_case`) Otherwise, the termination condition is set to :attr:`~pyomo.contrib.pyros.util.pyrosTerminationCondition.robust_feasible`. From 29de16cf48cec175931c8c6fcbb1ef155d958fb6 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 5 Mar 2025 17:37:54 -0500 Subject: [PATCH 25/67] Update uncertainty sets docs page --- .../solvers/pyrosdir/uncertainty_sets.rst | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/doc/OnlineDocs/explanation/solvers/pyrosdir/uncertainty_sets.rst b/doc/OnlineDocs/explanation/solvers/pyrosdir/uncertainty_sets.rst index 9652b4bdf40..ce16e5d16d8 100644 --- a/doc/OnlineDocs/explanation/solvers/pyrosdir/uncertainty_sets.rst +++ b/doc/OnlineDocs/explanation/solvers/pyrosdir/uncertainty_sets.rst @@ -15,18 +15,21 @@ In PyROS, the uncertainty set of a robust optimization problem is represented by an instance of a subclass of the :class:`~pyomo.contrib.pyros.uncertainty_sets.UncertaintySet` abstract base class. -PyROS provides a suite of pre-implemented concrete -:class:`~pyomo.contrib.pyros.uncertainty_sets.UncertaintySet` -subclasses to facilitate instantiation of uncertainty sets +PyROS provides a suite of +:ref:`pre-implemented concrete subclasses ` +to facilitate instantiation of uncertainty sets that are commonly used in the optimization literature. -Custom uncertainty set types can be implemented by subclassing -:class:`~pyomo.contrib.pyros.uncertainty_sets.UncertaintySet`, -though there are limitations. +:ref:`Custom uncertainty set types ` +can be implemented by subclassing +:class:`~pyomo.contrib.pyros.uncertainty_sets.UncertaintySet`. +.. _pyros_pre_implemented_types: Pre-Implemented Uncertainty Set Types ===================================== -The pre-implemented subclasses are enumerated below: +The pre-implemented +:class:`~pyomo.contrib.pyros.uncertainty_sets.UncertaintySet` +subclasses are enumerated below: .. autosummary:: @@ -83,6 +86,8 @@ subclasses are provided below. - :math:`\{q \in \mathbb{R}^{n} \mid A q \leq b\}` +.. _pyros_custom_sets: + Custom Uncertainty Set Types ============================ A custom uncertainty set type From 72a8f9a8a1952e9a6a3bcd924606a2ca77eb8623 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 6 Mar 2025 11:08:32 -0500 Subject: [PATCH 26/67] Rephrase some text on solver outputs --- .../explanation/solvers/pyrosdir/solver_interface.rst | 8 ++++---- .../explanation/solvers/pyrosdir/solver_log.rst | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_interface.rst b/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_interface.rst index 539f040845d..a447b263a6b 100644 --- a/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_interface.rst +++ b/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_interface.rst @@ -134,13 +134,13 @@ the value of the optional ``objective_focus`` argument to the * If ``objective_focus`` is set to :attr:`~pyomo.contrib.pyros.util.ObjectiveType.nominal`, - then those variables are evaluated at + then the objective is evaluated subject to the nominal uncertain parameter realization * If ``objective_focus`` is set to :attr:`~pyomo.contrib.pyros.util.ObjectiveType.worst_case`, - then those variables are evaluated at + then the objective is evaluated subject the uncertain parameter realization that induces the worst-case - objective function value + objective value The second-stage variable and state variable values in the :ref:`solution loaded to the model ` @@ -178,7 +178,7 @@ the :meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` method. The uncertain parameter objects are left unchanged. -Solver Log Output +Solver Output Log ----------------- See the :ref:`pyros_solver_log` section for more information on the PyROS solver log output. diff --git a/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_log.rst b/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_log.rst index 622fda2b1d3..80fec16f932 100644 --- a/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_log.rst +++ b/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_log.rst @@ -258,7 +258,7 @@ Configuring the Output Log For a given call to the PyROS :meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` method, -the solver log output is produced by the +the solver output log is produced by the Python logger (:py:class:`logging.Logger`) object derived from the optional argument ``progress_logger``. By default, ``progress_logger`` @@ -283,7 +283,7 @@ are excluded from the solver log. .. _table-logging-levels: -.. list-table:: PyROS solver log output at the various standard Python :py:mod:`logging` levels. +.. list-table:: Solver output log messages at the various standard Python :py:mod:`logging` levels. :widths: 10 50 :header-rows: 1 From dbff40367ecca0f9f5af4403bab37eecd1133d83 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 6 Mar 2025 11:36:16 -0500 Subject: [PATCH 27/67] Update GAMS hydro link --- doc/OnlineDocs/explanation/solvers/pyrosdir/getting_started.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/explanation/solvers/pyrosdir/getting_started.rst b/doc/OnlineDocs/explanation/solvers/pyrosdir/getting_started.rst index 359cce55584..cb8e9a82fc8 100644 --- a/doc/OnlineDocs/explanation/solvers/pyrosdir/getting_started.rst +++ b/doc/OnlineDocs/explanation/solvers/pyrosdir/getting_started.rst @@ -54,7 +54,7 @@ The problems are derived from the deterministic model *hydro*, a QCQP taken from the `GAMS Model Library `_. We have converted the -`GAMS implementation of hydro `_ +`GAMS implementation of hydro `_ to Pyomo format using the `GAMS CONVERT utility `_. From 6ab9ce0852f4b21c70a28a123c5c6ce81c6b6c12 Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 14 Mar 2025 02:44:27 -0400 Subject: [PATCH 28/67] Incorporate methodology overview formulation updates --- .../explanation/solvers/pyrosdir/overview.rst | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/doc/OnlineDocs/explanation/solvers/pyrosdir/overview.rst b/doc/OnlineDocs/explanation/solvers/pyrosdir/overview.rst index 3c97aaeaa60..d128dcbcb6d 100644 --- a/doc/OnlineDocs/explanation/solvers/pyrosdir/overview.rst +++ b/doc/OnlineDocs/explanation/solvers/pyrosdir/overview.rst @@ -23,11 +23,13 @@ Supported deterministic models can be written in the general form .. _deterministic-model: .. math:: - \begin{array}{clll} + :nowrap: + + \[\begin{array}{clll} \displaystyle \min_{\substack{x \in \mathcal{X}, \\ z \in \mathbb{R}^{n_z}, y\in\mathbb{R}^{n_y}}} & ~~ f_1\left(x\right) + f_2(x,z,y; q^{\text{nom}}) & \\ \displaystyle \text{s.t.} & ~~ g_i(x, z, y; q^{\text{nom}}) \leq 0 & \forall\,i \in \mathcal{I} \\ & ~~ h_j(x,z,y; q^{\text{nom}}) = 0 & \forall\,j \in \mathcal{J} \\ - \end{array} + \end{array}\] where: @@ -74,13 +76,15 @@ Based on the above notation, the form of the robust counterpart addressed by PyROS is .. math:: - \begin{array}{ccclll} + :nowrap: + + \[\begin{array}{ccclll} \displaystyle \min_{x \in \mathcal{X}} & \displaystyle \max_{q \in \mathcal{Q}} & \displaystyle \min_{\substack{z \in \mathbb{R}^{n_z},\\y \in \mathbb{R}^{n_y}}} \ \ & \displaystyle ~~ f_1\left(x\right) + f_2\left(x, z, y, q\right) \\ & & \text{s.t.}~ & \displaystyle ~~ g_i\left(x, z, y, q\right) \leq 0 & & \forall\, i \in \mathcal{I}\\ & & & \displaystyle ~~ h_j\left(x, z, y, q\right) = 0 & & \forall\,j \in \mathcal{J} - \end{array} + \end{array}\] PyROS accepts a deterministic model and accompanying uncertainty set and then, using the Generalized Robust Cutting-Set algorithm developed From 73fb76cdf2e336aeb3f3eb39556bebb3096c2ba2 Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 21 Mar 2025 15:39:41 -0400 Subject: [PATCH 29/67] Remove line numbers from solver log example --- .../solvers/pyrosdir/solver_log.rst | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_log.rst b/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_log.rst index 80fec16f932..9f3df2d2679 100644 --- a/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_log.rst +++ b/doc/OnlineDocs/explanation/solvers/pyrosdir/solver_log.rst @@ -17,15 +17,12 @@ Default Format When the PyROS :meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` method is invoked on a robust optimization problem, -your console output will, by default, look like this -(line numbers added for reference): +your console output will, by default, look like this: .. _solver-log-snippet: .. code-block:: text - :caption: PyROS solver output log for the :ref:`two-stage problem example `. - :linenos: ============================================================================== PyROS: The Pyomo Robust Optimization Solver, v1.3.4. @@ -123,21 +120,23 @@ your console output will, by default, look like this All done. Exiting PyROS. ============================================================================== -Observe that the log contains the following information: +Observe that the log contains the following information +(listed in order of appearance): -* **Introductory information** (lines 1--18). +* **Introductory information and disclaimer**: Includes the version number, author information, (UTC) time at which the solver was invoked, and, if available, information on the local Git branch and commit hash. -* **Summary of solver options** (lines 19--40). -* **Preprocessing information** (lines 41--43). +* **Summary of solver options**: Enumeration of + specifications for optional arguments to the solver. +* **Preprocessing information**: Wall time required for preprocessing the deterministic model and associated components, i.e., standardizing model components and adding the decision rule variables and equations. -* **Model component statistics** (lines 44--61). +* **Model component statistics**: Breakdown of model component statistics. Includes components added by PyROS, such as the decision rule variables and equations. @@ -149,12 +148,13 @@ Observe that the log contains the following information: are included in parentheses, next to the total numbers of second-stage variables and state variables, respectively; note that "adjustable" has been abbreviated as "adj." -* **Iteration log table** (lines 62--68). +* **Iteration log table**: Summary information on the problem iterates and subproblem outcomes. The constituent columns are defined in detail in :ref:`the table that follows `. -* **Termination message** (lines 69--70). Very brief summary of the termination outcome. -* **Timing statistics** (lines 71--87). +* **Termination message**: One-line message briefly summarizing + the reason the solver has terminated. +* **Timing statistics**: Tabulated breakdown of the solver timing statistics, based on a :class:`pyomo.common.timing.HierarchicalTimer` printout. The identifiers are as follows: @@ -173,13 +173,13 @@ Observe that the log contains the following information: * ``main.preprocessing``: Preprocessing time. * ``main.other``: Overhead time. -* **Final result** (lines 88--93). +* **Final result**: A printout of the :class:`~pyomo.contrib.pyros.solve_data.ROSolveResults` - object that is finally returned -* **Exit message** (lines 94--95). + object that is finally returned. +* **Exit message**: Confirmation that the solver has been exited properly. -The iteration log table (lines 62--68) is designed to provide, in a concise manner, +The iteration log table is designed to provide, in a concise manner, important information about the progress of the iterative algorithm for the problem of interest. The constituent columns are defined in the From 0dcd22b6b67679d716e36dd236490b5ae20d35e5 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 17 Sep 2025 09:20:52 -0400 Subject: [PATCH 30/67] Add new PyROS reactor-cooler tutorial --- .../solvers/pyrosdir/getting_started.rst | 17 +- .../explanation/solvers/pyrosdir/index.rst | 1 + .../tutorial/deterministic_heatmap.png | Bin 0 -> 86140 bytes .../solvers/pyrosdir/tutorial/dr0_heatmap.png | Bin 0 -> 92287 bytes .../solvers/pyrosdir/tutorial/dr1_heatmap.png | Bin 0 -> 94133 bytes .../pyrosdir/tutorial/por_heatmaps.png | Bin 0 -> 287000 bytes .../pyrosdir/tutorial/por_sensitivity.png | Bin 0 -> 48303 bytes .../pyrosdir/tutorial/reactor_cooler.png | Bin 0 -> 49912 bytes .../solvers/pyrosdir/tutorial/tutorial.rst | 885 ++++++++++++++++++ 9 files changed, 898 insertions(+), 5 deletions(-) create mode 100644 doc/OnlineDocs/explanation/solvers/pyrosdir/tutorial/deterministic_heatmap.png create mode 100644 doc/OnlineDocs/explanation/solvers/pyrosdir/tutorial/dr0_heatmap.png create mode 100644 doc/OnlineDocs/explanation/solvers/pyrosdir/tutorial/dr1_heatmap.png create mode 100644 doc/OnlineDocs/explanation/solvers/pyrosdir/tutorial/por_heatmaps.png create mode 100644 doc/OnlineDocs/explanation/solvers/pyrosdir/tutorial/por_sensitivity.png create mode 100644 doc/OnlineDocs/explanation/solvers/pyrosdir/tutorial/reactor_cooler.png create mode 100644 doc/OnlineDocs/explanation/solvers/pyrosdir/tutorial/tutorial.rst diff --git a/doc/OnlineDocs/explanation/solvers/pyrosdir/getting_started.rst b/doc/OnlineDocs/explanation/solvers/pyrosdir/getting_started.rst index cb8e9a82fc8..0442b1be5b4 100644 --- a/doc/OnlineDocs/explanation/solvers/pyrosdir/getting_started.rst +++ b/doc/OnlineDocs/explanation/solvers/pyrosdir/getting_started.rst @@ -46,11 +46,12 @@ PyROS can be installed as follows: depending on the solver distributions (e.g., Ipopt, BARON, SCIP) that you have pre-installed and licensed on your system. -Usage Tutorial -============== -In this tutorial, we will use PyROS to solve a few robust -optimization problems. -The problems are derived from the deterministic model *hydro*, + +Quickstart +========== +We now provide a quick overview of how to use PyROS to solve +robust optimization problems. +The problems here are derived from the deterministic model *hydro*, a QCQP taken from the `GAMS Model Library `_. We have converted the @@ -690,3 +691,9 @@ impact of the uncertainty set size on the robust optimal objective function value and the ease of analyzing the price of robustness for a given optimization problem under uncertainty. + + +Beyond the Basics +================= +A more in-depth introduction to PyROS is given +in the :ref:`Usage Tutorial `. diff --git a/doc/OnlineDocs/explanation/solvers/pyrosdir/index.rst b/doc/OnlineDocs/explanation/solvers/pyrosdir/index.rst index a209b42b7d7..ce954b69c5f 100644 --- a/doc/OnlineDocs/explanation/solvers/pyrosdir/index.rst +++ b/doc/OnlineDocs/explanation/solvers/pyrosdir/index.rst @@ -20,6 +20,7 @@ The developers gratefully acknowledge support from the U.S. Department of Energy Solver Interface Uncertainty Sets Solver Output Log + Usage Tutorial Citing PyROS diff --git a/doc/OnlineDocs/explanation/solvers/pyrosdir/tutorial/deterministic_heatmap.png b/doc/OnlineDocs/explanation/solvers/pyrosdir/tutorial/deterministic_heatmap.png new file mode 100644 index 0000000000000000000000000000000000000000..b5d844bcc7a5b4b1f98d923aa27150e36ff95bfe GIT binary patch literal 86140 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