diff --git a/.coin-or/projDesc.xml b/.coin-or/projDesc.xml
index d13ac8804cf..8a5a9e0a7df 100644
--- a/.coin-or/projDesc.xml
+++ b/.coin-or/projDesc.xml
@@ -227,8 +227,8 @@ Carl D. Laird, Chair, Pyomo Management Committee, claird at andrew dot cmu dot e
Use explicit overrides to disable use of automated
version reporting.
-->
- 6.7.3
- 6.7.3
+ 6.8.0
+ 6.8.0
diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml
index 932b0d8eea6..d439dafaf0a 100644
--- a/.github/workflows/release_wheel_creation.yml
+++ b/.github/workflows/release_wheel_creation.yml
@@ -14,10 +14,8 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
-env:
- PYOMO_SETUP_ARGS: "--with-cython --with-distributable-extensions"
-
jobs:
+
native_wheels:
name: Build wheels (${{ matrix.wheel-version }}) on ${{ matrix.os }} for native and cross-compiled architecture
runs-on: ${{ matrix.os }}
@@ -31,14 +29,26 @@ jobs:
include:
- wheel-version: 'cp38*'
TARGET: 'py38'
+ GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions"
- wheel-version: 'cp39*'
TARGET: 'py39'
+ GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions"
- wheel-version: 'cp310*'
TARGET: 'py310'
+ GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions"
- wheel-version: 'cp311*'
TARGET: 'py311'
+ GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions"
- wheel-version: 'cp312*'
TARGET: 'py312'
+ GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions"
+
+ exclude:
+ - wheel-version: 'cp311*'
+ os: windows-latest
+ - wheel-version: 'cp312*'
+ os: windows-latest
+
steps:
- uses: actions/checkout@v4
- name: Build wheels
@@ -47,13 +57,13 @@ jobs:
output-dir: dist
env:
CIBW_ARCHS_LINUX: "native"
- CIBW_ARCHS_MACOS: "native arm64"
- CIBW_ARCHS_WINDOWS: "native ARM64"
- CIBW_SKIP: "*-musllinux*"
+ CIBW_ARCHS_MACOS: "x86_64 arm64"
+ CIBW_ARCHS_WINDOWS: "AMD64 ARM64"
CIBW_BUILD: ${{ matrix.wheel-version }}
+ CIBW_SKIP: "*-musllinux*"
CIBW_BUILD_VERBOSITY: 1
CIBW_BEFORE_BUILD: pip install cython pybind11
- CIBW_CONFIG_SETTINGS: '--global-option="--with-cython --with-distributable-extensions"'
+ CIBW_ENVIRONMENT: PYOMO_SETUP_ARGS="${{ matrix.GLOBAL_OPTIONS }}"
- uses: actions/upload-artifact@v4
with:
name: native_wheels-${{ matrix.os }}-${{ matrix.TARGET }}
@@ -72,14 +82,19 @@ jobs:
include:
- wheel-version: 'cp38*'
TARGET: 'py38'
+ GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions"
- wheel-version: 'cp39*'
TARGET: 'py39'
+ GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions"
- wheel-version: 'cp310*'
TARGET: 'py310'
+ GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions"
- wheel-version: 'cp311*'
TARGET: 'py311'
+ GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions"
- wheel-version: 'cp312*'
TARGET: 'py312'
+ GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions"
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
@@ -93,17 +108,43 @@ jobs:
output-dir: dist
env:
CIBW_ARCHS_LINUX: "aarch64"
- CIBW_SKIP: "*-musllinux*"
CIBW_BUILD: ${{ matrix.wheel-version }}
+ CIBW_SKIP: "*-musllinux*"
CIBW_BUILD_VERBOSITY: 1
CIBW_BEFORE_BUILD: pip install cython pybind11
- CIBW_CONFIG_SETTINGS: '--global-option="--with-cython --with-distributable-extensions"'
+ CIBW_ENVIRONMENT: PYOMO_SETUP_ARGS="${{ matrix.GLOBAL_OPTIONS }}"
- uses: actions/upload-artifact@v4
with:
name: alt_wheels-${{ matrix.os }}-${{ matrix.TARGET }}
path: dist/*.whl
overwrite: true
+ pure_python:
+ name: pure_python_wheel
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ['3.11']
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install twine wheel setuptools pybind11
+ - name: Build pure python wheel
+ run: |
+ python setup.py --without-cython sdist --format=gztar bdist_wheel
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: purepythonwheel
+ path: dist/*.whl
+ overwrite: true
+
generictarball:
name: ${{ matrix.TARGET }}
runs-on: ${{ matrix.os }}
diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml
index 03894a1cb20..a4f2f8128e9 100644
--- a/.github/workflows/test_branches.yml
+++ b/.github/workflows/test_branches.yml
@@ -21,8 +21,8 @@ defaults:
env:
PYTHONWARNINGS: ignore::UserWarning
PYTHON_CORE_PKGS: wheel
- PYPI_ONLY: z3-solver
- PYPY_EXCLUDE: scipy numdifftools seaborn statsmodels
+ PYPI_ONLY: z3-solver linear-tree
+ PYPY_EXCLUDE: scipy numdifftools seaborn statsmodels linear-tree
CACHE_VER: v221013.1
NEOS_EMAIL: tests@pyomo.org
SRC_REF: ${{ github.head_ref || github.ref }}
diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml
index cc9760cbe5d..2ca7e166fd8 100644
--- a/.github/workflows/test_pr_and_main.yml
+++ b/.github/workflows/test_pr_and_main.yml
@@ -29,8 +29,8 @@ defaults:
env:
PYTHONWARNINGS: ignore::UserWarning
PYTHON_CORE_PKGS: wheel
- PYPI_ONLY: z3-solver
- PYPY_EXCLUDE: scipy numdifftools seaborn statsmodels
+ PYPI_ONLY: z3-solver linear-tree
+ PYPY_EXCLUDE: scipy numdifftools seaborn statsmodels linear-tree
CACHE_VER: v221013.1
NEOS_EMAIL: tests@pyomo.org
SRC_REF: ${{ github.head_ref || github.ref }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8d1d1e45e3d..5d16105d24b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,69 @@ Pyomo CHANGELOG
===============
+-------------------------------------------------------------------------------
+Pyomo 6.8.0 (20 Aug 2024)
+-------------------------------------------------------------------------------
+
+SIGNIFICANT CHANGE NOTICE
+
+- Internal data storage for Constraint objects (see #3293)
+- No longer release cythonized wheel for Python 3.11+ (see #3355)
+
+CHANGELOG
+
+- General
+ - Add ParameterizedQuadraticRepn and corresponding walker (#3324)
+ - Update Pyomo for NumPy 2.0 compatibility (#3292, #3353)
+ - Add ParameterizedLinearRepn and corresponding walker (#3268)
+ - Update Release Process Workflow for changes in `pip` (#3355)
+- Core
+ - Handle uninitialized variable in `propagate_solution` of scaling
+ transformation (#3275)
+ - Add `context` option to `SuffixFinder` (#3348)
+ - Remove the `_suppress_ctypes` attribute from Block (#3347)
+ - Improve `Set` initialization performance (#3302)
+ - Update Constraint to only store the original expression (not
+ lower/body/upper) (#3293)
+ - Kernel: fix bug in conic geomean (#3310)
+ - Fix bug with IndexedSet objects and the within argument (#3288)
+ - Support validate/filter for IndexedSet components using index (#3338)
+- Solver Interfaces
+ - Resolve NLv2 incompatibility with multithreading (#3332)
+ - Resolve writer performance degradation (#3343)
+ - Fix bug with inconsistent use of `result` and `results` (#3337)
+ - LegacySolverWrapper: restore 'options' attribute (#3334)
+ - Fix bug in XpressDirect._load_slacks (#3318)
+ - NLv2: support expressions with nested external functions (#3319)
+ - Ignore errors on ASL solver version check (#3298)
+ - Add SAS solver interface (#2886, #3309)
+- Testing
+ - Omnibus testing / platform portability fixes (#3335)
+ - Change BARON download URL (#3328)
+ - Disable interface/testing for NEOS/octeract (#3322)
+ - Fix typo in Jenkins driver (#3312)
+ - Jenkins: update logic for recording variables (#3311)
+ - Unpin Codecov / Update coverage (#3303)
+- GDP
+ - Don't transform known-to-be infeasible Disjuncts in multiple BigM (#3314)
+- Contributed Packages
+ - alternative_solutions: Add a new contrib package for generating
+ alternative solutions (#3270)
+ - APPSI: Allow maingo_solvermodel to be imported without maingopy (#3330)
+ - APPSI: Sort indices while removing constraints to fix bug in HiGHs
+ interface (#3281)
+ - CP: Add beforeChild handling for bools in logical expressions (#3315)
+ - DoE: Refactor to improve API and maintainability (#3317)
+ - incidence_analysis: Raise error in `generate_strongly_connected_components`
+ instead of asserting (#3305)
+ - parmest: Add missing main call for example file (#3349)
+ - piecewise: Add incremental PW linear to MIP transformation (#3287)
+ - piecewise: Add nonlinear-to-piecewise-linear transformation (#3333)
+ - PyNumero: Support user-provided CyIpopt callbacks with 13 arguments (#3289)
+ - PyNumero: Support PyomoNLP scaling factors on sub-blocks (#3295)
+ - PyROS: Temporarily Adjust NL Writer Feasibility Tolerance (#3280)
+ - viewer: Add option to specify the model by variable name (#3271)
+
-------------------------------------------------------------------------------
Pyomo 6.7.3 (29 May 2024)
-------------------------------------------------------------------------------
diff --git a/RELEASE.md b/RELEASE.md
index e42469cbad5..ec1e532cbcc 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -1,24 +1,19 @@
-We are pleased to announce the release of Pyomo 6.7.3.
+We are pleased to announce the release of Pyomo 6.8.0.
Pyomo is a collection of Python software packages that supports a
diverse set of optimization capabilities for formulating and analyzing
optimization models.
-The following are highlights of the 6.7 release series:
-
- - Added support for Python 3.12
- - Removed support for Python 3.7
- - New writer for converting linear models to matrix form
- - Improved handling of nested GDPs
- - Redesigned user API for parameter estimation
- - New packages:
- - iis: new capability for identifying minimal intractable systems
- - latex_printer: print Pyomo models to a LaTeX compatible format
- - contrib.solver: preview of redesigned solver interfaces
- - simplification: simplify Pyomo expressions
- - New solver interfaces
- - MAiNGO: Mixed-integer nonlinear global optimization
- - ...and of course numerous minor bug fixes and performance enhancements
+The following are highlights of the 6.8 release series:
+
+- Support for Numpy2
+- Refactor of Design of Experiments (`contrib.doe`)
+- New packages:
+ - alternative_solutions: alternative (near) optimal solutions
+- New solver interfaces:
+ - SAS: Statistical Analysis System
+ - v2: Ongoing solver interface refactor
+- ...and of course numerous minor bug fixes and performance enhancements
A full list of updates and changes is available in the
[`CHANGELOG.md`](https://github.com/Pyomo/pyomo/blob/main/CHANGELOG.md).
diff --git a/examples/pyomo/tutorials/set.dat b/examples/pyomo/tutorials/set.dat
index e2ad04122d8..16ad7ff9698 100644
--- a/examples/pyomo/tutorials/set.dat
+++ b/examples/pyomo/tutorials/set.dat
@@ -17,5 +17,9 @@ set S[5] := 2 3;
set T[2] := 1 3;
set T[5] := 2 3;
+set T_indexed_validate[2] := 1;
+set T_indexed_validate[3] := 1 2;
+set T_indexed_validate[4] := 1 2 3;
+
set X[2] := 1;
-set X[5] := 2 3;
\ No newline at end of file
+set X[5] := 2 3;
diff --git a/examples/pyomo/tutorials/set.out b/examples/pyomo/tutorials/set.out
index dd1ef2d4335..3f278a2f9b2 100644
--- a/examples/pyomo/tutorials/set.out
+++ b/examples/pyomo/tutorials/set.out
@@ -1,4 +1,4 @@
-24 Set Declarations
+25 Set Declarations
A : Size=1, Index=None, Ordered=Insertion
Key : Dimen : Domain : Size : Members
None : 1 : Any : 3 : {1, 2, 3}
@@ -80,6 +80,11 @@
Key : Dimen : Domain : Size : Members
2 : 1 : Any : 2 : {1, 3}
5 : 1 : Any : 2 : {2, 3}
+ T_indexed_validate : Size=3, Index=B, Ordered=Insertion
+ Key : Dimen : Domain : Size : Members
+ 2 : 1 : Any : 1 : {1,}
+ 3 : 1 : Any : 2 : {1, 2}
+ 4 : 1 : Any : 3 : {1, 2, 3}
U : Size=1, Index=None, Ordered=Insertion
Key : Dimen : Domain : Size : Members
None : 1 : Any : 5 : {1, 2, 6, 24, 120}
@@ -94,4 +99,4 @@
2 : 1 : S[2] : 1 : {1,}
5 : 1 : S[5] : 2 : {2, 3}
-24 Declarations: A B C D E F G H Hsub I J K K_2 L M N O P R S X T U V
+25 Declarations: A B C D E F G H Hsub I J K K_2 L M N O P R S X T T_indexed_validate U V
diff --git a/examples/pyomo/tutorials/set.py b/examples/pyomo/tutorials/set.py
index c1ea60b48ad..220bfbc82da 100644
--- a/examples/pyomo/tutorials/set.py
+++ b/examples/pyomo/tutorials/set.py
@@ -187,7 +187,17 @@ def T_validate(model, value):
return value in model.A
-model.T = Set(model.B, validate=M_validate)
+model.T = Set(model.B, validate=T_validate)
+
+
+#
+# Validation also provides the index within the IndexedSet being validated:
+#
+def T_indexed_validate(model, value, i):
+ return value in model.A and value < i
+
+
+model.T_indexed_validate = Set(model.B, validate=T_indexed_validate)
##
diff --git a/pyomo/contrib/doe/__init__.py b/pyomo/contrib/doe/__init__.py
index ffb6df1a860..154b52124d3 100644
--- a/pyomo/contrib/doe/__init__.py
+++ b/pyomo/contrib/doe/__init__.py
@@ -24,8 +24,7 @@
@deprecated(
- "Use of MeasurementVariables in Pyomo.DoE is no longer supported.",
- version='6.7.4.dev0',
+ "Use of MeasurementVariables in Pyomo.DoE is no longer supported.", version='6.8.0'
)
class MeasurementVariables:
def __init__(self, *args):
@@ -33,7 +32,7 @@ def __init__(self, *args):
@deprecated(
- "Use of DesignVariables in Pyomo.DoE is no longer supported.", version='6.7.4.dev0'
+ "Use of DesignVariables in Pyomo.DoE is no longer supported.", version='6.8.0'
)
class DesignVariables:
def __init__(self, *args):
@@ -41,7 +40,7 @@ def __init__(self, *args):
@deprecated(
- "Use of ModelOptionLib in Pyomo.DoE is no longer supported.", version='6.7.4.dev0'
+ "Use of ModelOptionLib in Pyomo.DoE is no longer supported.", version='6.8.0'
)
class ModelOptionLib:
def __init__(self, *args):
diff --git a/pyomo/contrib/piecewise/__init__.py b/pyomo/contrib/piecewise/__init__.py
index b23200b3f7d..28aeee74c56 100644
--- a/pyomo/contrib/piecewise/__init__.py
+++ b/pyomo/contrib/piecewise/__init__.py
@@ -33,9 +33,15 @@
from pyomo.contrib.piecewise.transform.convex_combination import (
ConvexCombinationTransformation,
)
+from pyomo.contrib.piecewise.transform.nonlinear_to_pwl import (
+ DomainPartitioningMethod,
+ NonlinearToPWL,
+)
from pyomo.contrib.piecewise.transform.nested_inner_repn import (
NestedInnerRepresentationGDPTransformation,
)
from pyomo.contrib.piecewise.transform.disaggregated_logarithmic import (
DisaggregatedLogarithmicMIPTransformation,
)
+from pyomo.contrib.piecewise.transform.incremental import IncrementalMIPTransformation
+from pyomo.contrib.piecewise.triangulations import Triangulation
diff --git a/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py b/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py
new file mode 100644
index 00000000000..283c64cb27f
--- /dev/null
+++ b/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py
@@ -0,0 +1,3119 @@
+# ___________________________________________________________________________
+#
+# Pyomo: Python Optimization Modeling Objects
+# Copyright (c) 2008-2024
+# National Technology and Engineering Solutions of Sandia, LLC
+# Under the terms of Contract DE-NA0003525 with National Technology and
+# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
+# rights in this software.
+# This software is distributed under the 3-clause BSD License.
+# ___________________________________________________________________________
+
+from pyomo.common.dependencies import networkx as nx
+import itertools
+
+
+def _get_double_cube_graph():
+ # Graph of a double cube
+ sign_vecs = list(itertools.product((-1, 1), repeat=3))
+ permutations = itertools.permutations(range(1, 4))
+ simplices = list(itertools.product(sign_vecs, permutations))
+
+ G = nx.Graph()
+ G.add_nodes_from(simplices)
+ for s in sign_vecs:
+ # interior connectivity of cubes
+ G.add_edges_from(
+ [
+ ((s, (1, 2, 3)), (s, (1, 3, 2))),
+ ((s, (1, 3, 2)), (s, (3, 1, 2))),
+ ((s, (3, 1, 2)), (s, (3, 2, 1))),
+ ((s, (3, 2, 1)), (s, (2, 3, 1))),
+ ((s, (2, 3, 1)), (s, (2, 1, 3))),
+ ((s, (2, 1, 3)), (s, (1, 2, 3))),
+ ]
+ )
+ # connectivity between cubes in double cube
+ for simplex in simplices:
+ neighbor_sign = list(simplex[0])
+ neighbor_sign[simplex[1][2] - 1] *= -1
+ neighbor_simplex = (tuple(neighbor_sign), simplex[1])
+ G.add_edge(simplex, neighbor_simplex)
+
+ return G
+
+
+"""
+This code was used to generate the data structure in this file. It should never
+need to be run again, but is here for the sake of documentation:
+
+# Get a list of 60 hamiltonian paths used in the 3d version of the ordered J1
+# triangulation, and dump it to stdout.
+if __name__ == '__main__':
+ G = _get_double_cube_graph()
+
+ # Each of these simplices has an outward face in the specified direction; also,
+ # the +x simplex of one cube is adjacent to the -x simplex of a cube adjacent in
+ # the x direction, and similarly for the others.
+ border_simplices = {
+ # simplices in low-coordinate cube
+ # -x
+ ((-1, 0, 0), 1): ((-1, -1, -1), (1, 2, 3)),
+ ((-1, 0, 0), 2): ((-1, -1, -1), (1, 3, 2)),
+ # -y
+ ((0, -1, 0), 1): ((-1, -1, -1), (2, 1, 3)),
+ ((0, -1, 0), 2): ((-1, -1, -1), (2, 3, 1)),
+ # -z
+ ((0, 0, -1), 1): ((-1, -1, -1), (3, 1, 2)),
+ ((0, 0, -1), 2): ((-1, -1, -1), (3, 2, 1)),
+ # simplices in one-high-coordinate cubes
+ # +x
+ ((1, 0, 0), 1): ((1, -1, -1), (1, 2, 3)),
+ ((1, 0, 0), 2): ((1, -1, -1), (1, 3, 2)),
+ # +y
+ ((0, 1, 0), 1): ((-1, 1, -1), (2, 1, 3)),
+ ((0, 1, 0), 2): ((-1, 1, -1), (2, 3, 1)),
+ # +z
+ ((0, 0, 1), 1): ((-1, -1, 1), (3, 1, 2)),
+ ((0, 0, 1), 2): ((-1, -1, 1), (3, 2, 1)),
+ }
+
+ # Need: Hamiltonian paths from each input to some output in each direction
+ all_needed_hamiltonians = {}
+ for i, s1 in border_simplices.items():
+ for j, s2 in border_simplices.items():
+ # I could cut the number of these in half or less via symmetry but
+ # I don't care
+ if i[0] != j[0]:
+ if (i, (j[0], 1)) in all_needed_hamiltonians.keys() or (
+ i,
+ (j[0], 2),
+ ) in all_needed_hamiltonians.keys():
+ print(
+ f"skipping search for path from {i} to {j} because we have a "
+ f"path from {i} to {(j[0], 1) if (i, (j[0], 1)) in "
+ f"all_needed_hamiltonians.keys() else (j[0], 2)}"
+ )
+ continue
+ print(f"searching for path from {i} to {j}")
+ for path in nx.all_simple_paths(G, s1, s2):
+ if len(path) == 48:
+ # it's hamiltonian!
+ print(f"found hamiltonian path from {i} to {j}")
+ all_needed_hamiltonians[(i, j)] = path
+ break
+ print(f"done looking for paths from {i} to {j}")
+ print()
+ print(all_needed_hamiltonians)
+
+"""
+
+
+# This file was generated using generate_ordered_3d_j1_triangulation_data.py
+# Data format: Keys are a pair of simplices specified as the direction they are facing,
+# as a standard unit vector or negative of one, and a tag, 1 or 2, disambiguating which
+# of the two simplices considered is used. Values are a list of simplices given as
+# (sign_vector, permutation) pairs.
+def get_hamiltonian_paths():
+ return {
+ (((-1, 0, 0), 1), ((0, -1, 0), 1)): [
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ],
+ (((-1, 0, 0), 1), ((0, 0, -1), 2)): [
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ],
+ (((-1, 0, 0), 1), ((1, 0, 0), 1)): [
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 2, 3)),
+ ],
+ (((-1, 0, 0), 1), ((0, 1, 0), 2)): [
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ],
+ (((-1, 0, 0), 1), ((0, 0, 1), 1)): [
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ],
+ (((-1, 0, 0), 2), ((0, -1, 0), 2)): [
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ],
+ (((-1, 0, 0), 2), ((0, 0, -1), 1)): [
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ],
+ (((-1, 0, 0), 2), ((1, 0, 0), 2)): [
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (1, 3, 2)),
+ ],
+ (((-1, 0, 0), 2), ((0, 1, 0), 1)): [
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ],
+ (((-1, 0, 0), 2), ((0, 0, 1), 2)): [
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ],
+ (((0, -1, 0), 1), ((-1, 0, 0), 1)): [
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ],
+ (((0, -1, 0), 1), ((0, 0, -1), 1)): [
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ],
+ (((0, -1, 0), 1), ((1, 0, 0), 2)): [
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 3, 2)),
+ ],
+ (((0, -1, 0), 1), ((0, 1, 0), 1)): [
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ],
+ (((0, -1, 0), 1), ((0, 0, 1), 2)): [
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ],
+ (((0, -1, 0), 2), ((-1, 0, 0), 2)): [
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ],
+ (((0, -1, 0), 2), ((0, 0, -1), 2)): [
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ],
+ (((0, -1, 0), 2), ((1, 0, 0), 1)): [
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 2, 3)),
+ ],
+ (((0, -1, 0), 2), ((0, 1, 0), 2)): [
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ],
+ (((0, -1, 0), 2), ((0, 0, 1), 1)): [
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ],
+ (((0, 0, -1), 1), ((-1, 0, 0), 2)): [
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ],
+ (((0, 0, -1), 1), ((0, -1, 0), 1)): [
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ],
+ (((0, 0, -1), 1), ((1, 0, 0), 1)): [
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, -1), (1, 2, 3)),
+ ],
+ (((0, 0, -1), 1), ((0, 1, 0), 2)): [
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ],
+ (((0, 0, -1), 1), ((0, 0, 1), 1)): [
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ],
+ (((0, 0, -1), 2), ((-1, 0, 0), 1)): [
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ],
+ (((0, 0, -1), 2), ((0, -1, 0), 2)): [
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ],
+ (((0, 0, -1), 2), ((1, 0, 0), 2)): [
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 3, 2)),
+ ],
+ (((0, 0, -1), 2), ((0, 1, 0), 1)): [
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ],
+ (((0, 0, -1), 2), ((0, 0, 1), 2)): [
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ],
+ (((1, 0, 0), 1), ((-1, 0, 0), 1)): [
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ],
+ (((1, 0, 0), 1), ((0, -1, 0), 2)): [
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ],
+ (((1, 0, 0), 1), ((0, 0, -1), 1)): [
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ],
+ (((1, 0, 0), 1), ((0, 1, 0), 1)): [
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ],
+ (((1, 0, 0), 1), ((0, 0, 1), 2)): [
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ],
+ (((1, 0, 0), 2), ((-1, 0, 0), 2)): [
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ],
+ (((1, 0, 0), 2), ((0, -1, 0), 1)): [
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ],
+ (((1, 0, 0), 2), ((0, 0, -1), 2)): [
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ],
+ (((1, 0, 0), 2), ((0, 1, 0), 2)): [
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ],
+ (((1, 0, 0), 2), ((0, 0, 1), 1)): [
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ],
+ (((0, 1, 0), 1), ((-1, 0, 0), 2)): [
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ],
+ (((0, 1, 0), 1), ((0, -1, 0), 1)): [
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ],
+ (((0, 1, 0), 1), ((0, 0, -1), 2)): [
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ],
+ (((0, 1, 0), 1), ((1, 0, 0), 1)): [
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 2, 3)),
+ ],
+ (((0, 1, 0), 1), ((0, 0, 1), 1)): [
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ],
+ (((0, 1, 0), 2), ((-1, 0, 0), 1)): [
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ],
+ (((0, 1, 0), 2), ((0, -1, 0), 2)): [
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ],
+ (((0, 1, 0), 2), ((0, 0, -1), 1)): [
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ],
+ (((0, 1, 0), 2), ((1, 0, 0), 2)): [
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 3, 2)),
+ ],
+ (((0, 1, 0), 2), ((0, 0, 1), 2)): [
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ],
+ (((0, 0, 1), 1), ((-1, 0, 0), 1)): [
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ],
+ (((0, 0, 1), 1), ((0, -1, 0), 2)): [
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ],
+ (((0, 0, 1), 1), ((0, 0, -1), 1)): [
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ],
+ (((0, 0, 1), 1), ((1, 0, 0), 2)): [
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (1, 3, 2)),
+ ],
+ (((0, 0, 1), 1), ((0, 1, 0), 1)): [
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((-1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ],
+ (((0, 0, 1), 2), ((-1, 0, 0), 2)): [
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ],
+ (((0, 0, 1), 2), ((0, -1, 0), 1)): [
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ],
+ (((0, 0, 1), 2), ((0, 0, -1), 2)): [
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ],
+ (((0, 0, 1), 2), ((1, 0, 0), 1)): [
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (1, 2, 3)),
+ ],
+ (((0, 0, 1), 2), ((0, 1, 0), 2)): [
+ ((-1, -1, 1), (3, 2, 1)),
+ ((-1, -1, 1), (3, 1, 2)),
+ ((-1, -1, 1), (1, 3, 2)),
+ ((-1, -1, 1), (1, 2, 3)),
+ ((-1, -1, 1), (2, 1, 3)),
+ ((-1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (2, 3, 1)),
+ ((1, -1, 1), (3, 2, 1)),
+ ((1, -1, 1), (3, 1, 2)),
+ ((1, -1, 1), (1, 3, 2)),
+ ((1, -1, 1), (1, 2, 3)),
+ ((1, -1, 1), (2, 1, 3)),
+ ((1, -1, -1), (2, 1, 3)),
+ ((1, -1, -1), (1, 2, 3)),
+ ((1, -1, -1), (1, 3, 2)),
+ ((1, -1, -1), (3, 1, 2)),
+ ((1, 1, -1), (3, 1, 2)),
+ ((1, 1, -1), (1, 3, 2)),
+ ((1, 1, -1), (1, 2, 3)),
+ ((1, 1, -1), (2, 1, 3)),
+ ((1, 1, -1), (2, 3, 1)),
+ ((1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 2, 1)),
+ ((-1, 1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 1, 2)),
+ ((-1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (3, 2, 1)),
+ ((1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 3, 1)),
+ ((-1, -1, -1), (2, 1, 3)),
+ ((-1, -1, -1), (1, 2, 3)),
+ ((-1, -1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 3, 2)),
+ ((-1, 1, -1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 2, 3)),
+ ((-1, 1, 1), (1, 3, 2)),
+ ((-1, 1, 1), (3, 1, 2)),
+ ((-1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 2, 1)),
+ ((1, 1, 1), (3, 1, 2)),
+ ((1, 1, 1), (1, 3, 2)),
+ ((1, 1, 1), (1, 2, 3)),
+ ((1, 1, 1), (2, 1, 3)),
+ ((1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 3, 1)),
+ ((-1, 1, 1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 1, 3)),
+ ((-1, 1, -1), (2, 3, 1)),
+ ],
+ }
diff --git a/pyomo/contrib/piecewise/piecewise_linear_function.py b/pyomo/contrib/piecewise/piecewise_linear_function.py
index 742eb52d5bb..f4dcdce8db4 100644
--- a/pyomo/contrib/piecewise/piecewise_linear_function.py
+++ b/pyomo/contrib/piecewise/piecewise_linear_function.py
@@ -19,7 +19,12 @@
from pyomo.contrib.piecewise.piecewise_linear_expression import (
PiecewiseLinearExpression,
)
-from pyomo.core import Any, NonNegativeIntegers, value, Var
+from pyomo.contrib.piecewise.triangulations import (
+ get_unordered_j1_triangulation,
+ get_ordered_j1_triangulation,
+ Triangulation,
+)
+from pyomo.core import Any, NonNegativeIntegers, value
from pyomo.core.base.block import BlockData, Block
from pyomo.core.base.component import ModelComponentFactory
from pyomo.core.base.expression import Expression
@@ -43,12 +48,21 @@ def __init__(self, component=None):
BlockData.__init__(self, component)
with self._declare_reserved_components():
+ # map of PiecewiseLinearExpression objects to integer indices in
+ # self._expressions
+ self._expression_ids = ComponentMap()
+ # index is monotonically increasing integer
self._expressions = Expression(NonNegativeIntegers)
self._transformed_exprs = ComponentMap()
self._simplices = None
# These will always be tuples, even when we only have one dimension.
self._points = []
self._linear_functions = []
+ self._triangulation = None
+
+ @property
+ def triangulation(self):
+ return self._triangulation
def __call__(self, *args):
"""
@@ -63,8 +77,9 @@ def __call__(self, *args):
return self._evaluate(*args)
else:
expr = PiecewiseLinearExpression(args, self)
- idx = id(expr)
+ idx = len(self._expressions)
self._expressions[idx] = expr
+ self._expression_ids[expr] = idx
return self._expressions[idx]
def _evaluate(self, *args):
@@ -134,7 +149,12 @@ def map_transformation_var(self, pw_expr, v):
Records on the PiecewiseLinearFunction object that the transformed
form of the PiecewiseLinearExpression object pw_expr is the Var v.
"""
- self._transformed_exprs[self._expressions[id(pw_expr)]] = v
+ if pw_expr not in self._expression_ids:
+ raise DeveloperError(
+ "ID of PiecewiseLinearExpression '%s' not in the _expression_ids "
+ "dictionary of PiecewiseLinearFunction '%s'" % (pw_expr, self)
+ )
+ self._transformed_exprs[self._expressions[self._expression_ids[pw_expr]]] = v
def get_transformation_var(self, pw_expr):
"""
@@ -159,7 +179,7 @@ def __call__(self, x):
class _multivariate_linear_functor(AutoSlots.Mixin):
- __slots__ = 'normal'
+ __slots__ = ('normal',)
def __init__(self, normal):
self.normal = normal
@@ -225,6 +245,19 @@ class PiecewiseLinearFunction(Block):
expression for a linear function of the arguments.
tabular_data: A dictionary mapping values of the nonlinear function
to points in the domain
+ triangulation (optional): An enum value of type Triangulation specifying
+ how Pyomo should triangulate the function domain, or None. Behavior
+ depends on how this piecewise-linear function is constructed:
+ when constructed using methods (1) or (4) above, valid arguments
+ are the members of Triangulation except Unknown or AssumeValid,
+ and Pyomo will use that method to triangulate the domain and to tag
+ the resulting PWLF. If no argument or None is passed, the default
+ is Triangulation.Delaunay. When constructed using methods (2) or (3)
+ above, valid arguments are only Triangulation.Unknown and
+ Triangulation.AssumeValid. Pyomo will tag the constructed PWLF
+ as specified, trusting the user in the case of AssumeValid.
+ When no argument or None is passed, the default is
+ Triangulation.Unknown
"""
_ComponentDataClass = PiecewiseLinearFunctionData
@@ -251,6 +284,7 @@ def __init__(self, *args, **kwargs):
_linear_functions = kwargs.pop('linear_functions', None)
_tabular_data_arg = kwargs.pop('tabular_data', None)
_tabular_data_rule_arg = kwargs.pop('tabular_data_rule', None)
+ _triangulation_rule_arg = kwargs.pop('triangulation', None)
kwargs.setdefault('ctype', PiecewiseLinearFunction)
Block.__init__(self, *args, **kwargs)
@@ -269,6 +303,9 @@ def __init__(self, *args, **kwargs):
self._tabular_data_rule = Initializer(
_tabular_data_rule_arg, treat_sequences_as_mappings=False
)
+ self._triangulation_rule = Initializer(
+ _triangulation_rule_arg, treat_sequences_as_mappings=False
+ )
def _get_dimension_from_points(self, points):
if len(points) < 1:
@@ -284,12 +321,34 @@ def _get_dimension_from_points(self, points):
return dimension
- def _construct_simplices_from_multivariate_points(self, obj, points, dimension):
- try:
- triangulation = spatial.Delaunay(points)
- except (spatial.QhullError, ValueError) as error:
- logger.error("Unable to triangulate the set of input points.")
- raise
+ def _construct_simplices_from_multivariate_points(
+ self, obj, parent, points, dimension
+ ):
+ if self._triangulation_rule is None:
+ tri = Triangulation.Delaunay
+ else:
+ tri = self._triangulation_rule(parent, obj._index)
+ if tri is None:
+ tri = Triangulation.Delaunay
+
+ if tri == Triangulation.Delaunay:
+ try:
+ triangulation = spatial.Delaunay(points)
+ except (spatial.QhullError, ValueError) as error:
+ logger.error("Unable to triangulate the set of input points.")
+ raise
+ obj._triangulation = tri
+ elif tri == Triangulation.J1:
+ triangulation = get_unordered_j1_triangulation(points, dimension)
+ obj._triangulation = tri
+ elif tri == Triangulation.OrderedJ1:
+ triangulation = get_ordered_j1_triangulation(points, dimension)
+ obj._triangulation = tri
+ else:
+ raise ValueError(
+ "Invalid or unrecognized triangulation specified for '%s': %s"
+ % (obj, tri)
+ )
# Get the points for the triangulation because they might not all be
# there if any were coplanar.
@@ -308,7 +367,13 @@ def _construct_simplices_from_multivariate_points(self, obj, points, dimension):
# checking the determinant because matrix_rank will by default calculate a
# tolerance based on the input to account for numerical errors in the
# SVD computation.
- if (
+ if tri in (Triangulation.J1, Triangulation.OrderedJ1):
+ # Note: do not sort vertices from OrderedJ1, or it will break.
+ # Non-ordered J1 is already sorted, though it doesn't matter.
+ # Also, we don't need to check for degeneracy with simplices we
+ # made ourselves.
+ obj._simplices.append(tuple(simplex))
+ elif (
np.linalg.matrix_rank(
points[:, 1:]
- np.append(points[:, : dimension - 1], points[:, [0]], axis=1)
@@ -326,6 +391,24 @@ def _construct_simplices_from_multivariate_points(self, obj, points, dimension):
"%s from the triangulation." % pt[0]
)
+ # Call when constructing from simplices to allow use of AssumeValid and
+ # ensure the user is not making mistakes
+ def _check_and_set_triangulation_from_user(self, parent, obj):
+ if self._triangulation_rule is None:
+ tri = None
+ else:
+ tri = self._triangulation_rule(parent, obj._index)
+ if tri is None or tri == Triangulation.Unknown:
+ obj._triangulation = Triangulation.Unknown
+ elif tri == Triangulation.AssumeValid:
+ obj._triangulation = Triangulation.AssumeValid
+ else:
+ raise ValueError(
+ f"Invalid or unrecognized triangulation tag specified for {obj} when"
+ f" giving simplices: {tri}. Valid arguments when giving simplices are"
+ " Triangulation.Unknown and Triangulation.AssumeValid."
+ )
+
def _construct_one_dimensional_simplices_from_points(self, obj, points):
points.sort()
obj._simplices = []
@@ -347,15 +430,25 @@ def _construct_from_function_and_points(self, obj, parent, nonlinear_function):
# avoid a dependence on scipy.
self._construct_one_dimensional_simplices_from_points(obj, points)
return self._construct_from_univariate_function_and_segments(
- obj, nonlinear_function
+ obj, parent, nonlinear_function, segments_are_user_defined=False
)
- self._construct_simplices_from_multivariate_points(obj, points, dimension)
+ self._construct_simplices_from_multivariate_points(
+ obj, parent, points, dimension
+ )
return self._construct_from_function_and_simplices(
obj, parent, nonlinear_function, simplices_are_user_defined=False
)
- def _construct_from_univariate_function_and_segments(self, obj, func):
+ def _construct_from_univariate_function_and_segments(
+ self, obj, parent, func, segments_are_user_defined=True
+ ):
+ # We can trust they are nicely ordered if we made them, otherwise anything goes.
+ if segments_are_user_defined:
+ self._check_and_set_triangulation_from_user(parent, obj)
+ else:
+ obj._triangulation = Triangulation.AssumeValid
+
for idx1, idx2 in obj._simplices:
x1 = obj._points[idx1][0]
x2 = obj._points[idx2][0]
@@ -386,9 +479,14 @@ def _construct_from_function_and_simplices(
# it separately in order to avoid a kind of silly dependence on
# numpy.
return self._construct_from_univariate_function_and_segments(
- obj, nonlinear_function
+ obj, parent, nonlinear_function, simplices_are_user_defined
)
+ # If we triangulated, then this tag was already set. If they provided it,
+ # then check their arguments and set.
+ if simplices_are_user_defined:
+ self._check_and_set_triangulation_from_user(parent, obj)
+
# evaluate the function at each of the points and form the homogeneous
# system of equations
A = np.ones((dimension + 2, dimension + 2))
@@ -440,6 +538,7 @@ def _construct_from_linear_functions_and_simplices(
# have been called.
obj._get_simplices_from_arg(self._simplices_rule(parent, obj._index))
obj._linear_functions = [f for f in self._linear_funcs_rule(parent, obj._index)]
+ self._check_and_set_triangulation_from_user(parent, obj)
return obj
@_define_handler(_handlers, False, False, False, False, True)
@@ -457,12 +556,20 @@ def _construct_from_tabular_data(self, obj, parent, nonlinear_function):
# avoid a dependence on scipy.
self._construct_one_dimensional_simplices_from_points(obj, points)
return self._construct_from_univariate_function_and_segments(
- obj, _tabular_data_functor(tabular_data, tupleize=True)
+ obj,
+ parent,
+ _tabular_data_functor(tabular_data, tupleize=True),
+ segments_are_user_defined=False,
)
- self._construct_simplices_from_multivariate_points(obj, points, dimension)
+ self._construct_simplices_from_multivariate_points(
+ obj, parent, points, dimension
+ )
return self._construct_from_function_and_simplices(
- obj, parent, _tabular_data_functor(tabular_data)
+ obj,
+ parent,
+ _tabular_data_functor(tabular_data),
+ simplices_are_user_defined=False,
)
def _getitem_when_not_present(self, index):
@@ -499,7 +606,9 @@ def _getitem_when_not_present(self, index):
"a list of corresponding simplices, or a dictionary "
"mapping points to nonlinear function values."
)
- return handler(self, obj, parent, nonlinear_function)
+ obj = handler(self, obj, parent, nonlinear_function)
+
+ return obj
class ScalarPiecewiseLinearFunction(
diff --git a/pyomo/contrib/piecewise/tests/common_tests.py b/pyomo/contrib/piecewise/tests/common_tests.py
index 23e67474934..c891c8d502a 100644
--- a/pyomo/contrib/piecewise/tests/common_tests.py
+++ b/pyomo/contrib/piecewise/tests/common_tests.py
@@ -34,8 +34,9 @@ def check_log_x_model_soln(test, m):
test.assertAlmostEqual(value(m.obj), m.f2(4))
-def check_transformation_do_not_descend(test, transformation):
- m = models.make_log_x_model()
+def check_transformation_do_not_descend(test, transformation, m=None):
+ if m is None:
+ m = models.make_log_x_model()
transform = TransformationFactory(transformation)
transform.apply_to(m)
@@ -43,8 +44,9 @@ def check_transformation_do_not_descend(test, transformation):
test.check_pw_paraboloid(m)
-def check_transformation_PiecewiseLinearFunction_targets(test, transformation):
- m = models.make_log_x_model()
+def check_transformation_PiecewiseLinearFunction_targets(test, transformation, m=None):
+ if m is None:
+ m = models.make_log_x_model()
transform = TransformationFactory(transformation)
transform.apply_to(m, targets=[m.pw_log])
@@ -54,8 +56,9 @@ def check_transformation_PiecewiseLinearFunction_targets(test, transformation):
test.assertIsNone(m.pw_paraboloid.get_transformation_var(m.paraboloid_expr))
-def check_descend_into_expressions(test, transformation):
- m = models.make_log_x_model()
+def check_descend_into_expressions(test, transformation, m=None):
+ if m is None:
+ m = models.make_log_x_model()
transform = TransformationFactory(transformation)
transform.apply_to(m, descend_into_expressions=True)
@@ -64,8 +67,9 @@ def check_descend_into_expressions(test, transformation):
test.check_pw_paraboloid(m)
-def check_descend_into_expressions_constraint_target(test, transformation):
- m = models.make_log_x_model()
+def check_descend_into_expressions_constraint_target(test, transformation, m=None):
+ if m is None:
+ m = models.make_log_x_model()
transform = TransformationFactory(transformation)
transform.apply_to(m, descend_into_expressions=True, targets=[m.indexed_c])
@@ -74,8 +78,9 @@ def check_descend_into_expressions_constraint_target(test, transformation):
test.assertIsNone(m.pw_log.get_transformation_var(m.log_expr))
-def check_descend_into_expressions_objective_target(test, transformation):
- m = models.make_log_x_model()
+def check_descend_into_expressions_objective_target(test, transformation, m=None):
+ if m is None:
+ m = models.make_log_x_model()
transform = TransformationFactory(transformation)
transform.apply_to(m, descend_into_expressions=True, targets=[m.obj])
diff --git a/pyomo/contrib/piecewise/tests/models.py b/pyomo/contrib/piecewise/tests/models.py
index 1a8bef04ad7..e209b1ac879 100644
--- a/pyomo/contrib/piecewise/tests/models.py
+++ b/pyomo/contrib/piecewise/tests/models.py
@@ -9,11 +9,18 @@
# This software is distributed under the 3-clause BSD License.
# ___________________________________________________________________________
-from pyomo.contrib.piecewise import PiecewiseLinearFunction
+from pyomo.contrib.piecewise import PiecewiseLinearFunction, Triangulation
from pyomo.environ import ConcreteModel, Constraint, log, Objective, Var
+default_simplices = [
+ [(0, 1), (0, 4), (3, 4)],
+ [(0, 1), (3, 4), (3, 1)],
+ [(3, 4), (3, 7), (0, 7)],
+ [(0, 7), (0, 4), (3, 4)],
+]
-def make_log_x_model():
+
+def make_log_x_model(simplices=default_simplices):
m = ConcreteModel()
m.x = Var(bounds=(1, 10))
m.pw_log = PiecewiseLinearFunction(points=[1, 3, 6, 10], function=log)
@@ -50,14 +57,11 @@ def g2(x1, x2):
return 3 * x1 + 11 * x2 - 28
m.g2 = g2
- simplices = [
- [(0, 1), (0, 4), (3, 4)],
- [(0, 1), (3, 4), (3, 1)],
- [(3, 4), (3, 7), (0, 7)],
- [(0, 7), (0, 4), (3, 4)],
- ]
+
m.pw_paraboloid = PiecewiseLinearFunction(
- simplices=simplices, linear_functions=[g1, g1, g2, g2]
+ simplices=simplices,
+ linear_functions=[g1, g1, g2, g2],
+ triangulation=Triangulation.AssumeValid,
)
m.paraboloid_expr = m.pw_paraboloid(m.x1, m.x2)
diff --git a/pyomo/contrib/piecewise/tests/test_incremental.py b/pyomo/contrib/piecewise/tests/test_incremental.py
new file mode 100644
index 00000000000..8ca43df20f3
--- /dev/null
+++ b/pyomo/contrib/piecewise/tests/test_incremental.py
@@ -0,0 +1,204 @@
+# ___________________________________________________________________________
+#
+# Pyomo: Python Optimization Modeling Objects
+# Copyright (c) 2008-2024
+# National Technology and Engineering Solutions of Sandia, LLC
+# Under the terms of Contract DE-NA0003525 with National Technology and
+# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
+# rights in this software.
+# This software is distributed under the 3-clause BSD License.
+# ___________________________________________________________________________
+
+import pyomo.common.unittest as unittest
+import pyomo.contrib.piecewise.tests.common_tests as ct
+from pyomo.contrib.piecewise.tests.models import make_log_x_model
+from pyomo.contrib.piecewise.triangulations import Triangulation
+from pyomo.core.base import TransformationFactory
+from pyomo.core.expr.compare import assertExpressionsEqual
+from pyomo.environ import (
+ Constraint,
+ SolverFactory,
+ Var,
+ ConcreteModel,
+ Objective,
+ log,
+ value,
+ minimize,
+)
+from pyomo.contrib.piecewise import PiecewiseLinearFunction
+import itertools
+
+
+class TestTransformPiecewiseModelToIncrementalMIP(unittest.TestCase):
+
+ def check_pw_log(self, m):
+ z = m.pw_log.get_transformation_var(m.log_expr)
+ self.assertIsInstance(z, Var)
+ log_block = z.parent_block()
+
+ # Vars: three deltas, two y binaries, one substitute var
+ self.assertEqual(len(log_block.component_map(Var)), 3)
+ self.assertIsInstance(log_block.delta, Var)
+ self.assertEqual(len(log_block.delta), 3)
+ self.assertIsInstance(log_block.y_binaries, Var)
+ self.assertEqual(len(log_block.y_binaries), 2)
+ self.assertIsInstance(log_block.substitute_var, Var)
+ self.assertEqual(len(log_block.substitute_var), 1)
+
+ # Constraints: 2 delta below y, 2 y below delta, one each of the three others
+ self.assertEqual(len(log_block.component_map(Constraint)), 5)
+ self.assertIsInstance(log_block.deltas_below_y, Constraint)
+ self.assertEqual(len(log_block.deltas_below_y), 2)
+ self.assertIsInstance(log_block.y_below_delta, Constraint)
+ self.assertEqual(len(log_block.y_below_delta), 2)
+ self.assertIsInstance(log_block.delta_one_constraint, Constraint)
+ self.assertEqual(len(log_block.delta_one_constraint), 1)
+ self.assertIsInstance(log_block.x_constraint, Constraint)
+ self.assertEqual(len(log_block.x_constraint), 1)
+ self.assertIsInstance(log_block.set_substitute, Constraint)
+ self.assertEqual(len(log_block.set_substitute), 1)
+
+ assertExpressionsEqual(
+ self,
+ log_block.x_constraint[0].expr,
+ m.x
+ == 1
+ + (
+ log_block.delta[0, 1] * (3 - 1)
+ + log_block.delta[1, 1] * (6 - 3)
+ + log_block.delta[2, 1] * (10 - 6)
+ ),
+ )
+ assertExpressionsEqual(
+ self,
+ log_block.set_substitute.expr,
+ log_block.substitute_var
+ == m.f1(1)
+ + (
+ log_block.delta[0, 1] * (m.f2(3) - m.f1(1))
+ + log_block.delta[1, 1] * (m.f3(6) - m.f2(3))
+ + log_block.delta[2, 1] * (m.f3(10) - m.f3(6))
+ ),
+ places=10,
+ )
+ assertExpressionsEqual(
+ self, log_block.delta_one_constraint.expr, log_block.delta[0, 1] <= 1
+ )
+ assertExpressionsEqual(
+ self,
+ log_block.deltas_below_y[0].expr,
+ log_block.delta[1, 1] <= log_block.y_binaries[0],
+ )
+ assertExpressionsEqual(
+ self,
+ log_block.deltas_below_y[1].expr,
+ log_block.delta[2, 1] <= log_block.y_binaries[1],
+ )
+ assertExpressionsEqual(
+ self,
+ log_block.y_below_delta[0].expr,
+ log_block.y_binaries[0] <= log_block.delta[0, 1],
+ )
+ assertExpressionsEqual(
+ self,
+ log_block.y_below_delta[1].expr,
+ log_block.y_binaries[1] <= log_block.delta[1, 1],
+ )
+
+ def check_pw_paraboloid(self, m):
+ z = m.pw_paraboloid.get_transformation_var(m.paraboloid_expr)
+ self.assertIsInstance(z, Var)
+ paraboloid_block = z.parent_block()
+
+ # Vars: 8 deltas (2 per simplex), 3 y binaries, one substitute var
+ self.assertEqual(len(paraboloid_block.component_map(Var)), 3)
+ self.assertIsInstance(paraboloid_block.delta, Var)
+ self.assertEqual(len(paraboloid_block.delta), 8)
+ self.assertIsInstance(paraboloid_block.y_binaries, Var)
+ self.assertEqual(len(paraboloid_block.y_binaries), 3)
+ self.assertIsInstance(paraboloid_block.substitute_var, Var)
+ self.assertEqual(len(paraboloid_block.substitute_var), 1)
+
+ # Constraints: 3 delta below y, 3 y below delta, two x constraints (two
+ # coordinates), one each of the three others
+ self.assertEqual(len(paraboloid_block.component_map(Constraint)), 5)
+ self.assertIsInstance(paraboloid_block.deltas_below_y, Constraint)
+ self.assertEqual(len(paraboloid_block.deltas_below_y), 3)
+ self.assertIsInstance(paraboloid_block.y_below_delta, Constraint)
+ self.assertEqual(len(paraboloid_block.y_below_delta), 3)
+ self.assertIsInstance(paraboloid_block.delta_one_constraint, Constraint)
+ self.assertEqual(len(paraboloid_block.delta_one_constraint), 1)
+ self.assertIsInstance(paraboloid_block.x_constraint, Constraint)
+ self.assertEqual(len(paraboloid_block.x_constraint), 2)
+ self.assertIsInstance(paraboloid_block.set_substitute, Constraint)
+ self.assertEqual(len(paraboloid_block.set_substitute), 1)
+
+ ordered_simplices = [
+ [(0, 1), (3, 1), (3, 4)],
+ [(3, 4), (0, 1), (0, 4)],
+ [(0, 4), (0, 7), (3, 4)],
+ [(3, 4), (3, 7), (0, 7)],
+ ]
+
+ # Test methods using the common_tests.py code.
+ def test_transformation_do_not_descend(self):
+ ct.check_transformation_do_not_descend(
+ self,
+ 'contrib.piecewise.incremental',
+ make_log_x_model(simplices=self.ordered_simplices),
+ )
+
+ def test_transformation_PiecewiseLinearFunction_targets(self):
+ ct.check_transformation_PiecewiseLinearFunction_targets(
+ self,
+ 'contrib.piecewise.incremental',
+ make_log_x_model(simplices=self.ordered_simplices),
+ )
+
+ def test_descend_into_expressions(self):
+ ct.check_descend_into_expressions(
+ self,
+ 'contrib.piecewise.incremental',
+ make_log_x_model(simplices=self.ordered_simplices),
+ )
+
+ def test_descend_into_expressions_constraint_target(self):
+ ct.check_descend_into_expressions_constraint_target(
+ self,
+ 'contrib.piecewise.incremental',
+ make_log_x_model(simplices=self.ordered_simplices),
+ )
+
+ def test_descend_into_expressions_objective_target(self):
+ ct.check_descend_into_expressions_objective_target(
+ self,
+ 'contrib.piecewise.incremental',
+ make_log_x_model(simplices=self.ordered_simplices),
+ )
+
+ @unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi is not available')
+ @unittest.skipUnless(SolverFactory('gurobi').license_is_valid(), 'No license')
+ def test_solve_log_model(self):
+ m = make_log_x_model(simplices=self.ordered_simplices)
+ TransformationFactory('contrib.piecewise.incremental').apply_to(m)
+ TransformationFactory('gdp.bigm').apply_to(m)
+ SolverFactory('gurobi').solve(m)
+ ct.check_log_x_model_soln(self, m)
+
+ # Failed during development when ordered j1 vertex ordering got broken
+ @unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi is not available')
+ @unittest.skipUnless(SolverFactory('gurobi').license_is_valid(), 'No license')
+ def test_solve_product_model(self):
+ m = ConcreteModel()
+ m.x1 = Var(bounds=(0.5, 5))
+ m.x2 = Var(bounds=(0.9, 0.95))
+ pts = list(itertools.product([0.5, 2.75, 5], [0.9, 0.925, 0.95]))
+ m.pwlf = PiecewiseLinearFunction(
+ points=pts,
+ function=lambda x, y: x * y,
+ triangulation=Triangulation.OrderedJ1,
+ )
+ m.obj = Objective(sense=minimize, expr=m.pwlf(m.x1, m.x2))
+ TransformationFactory("contrib.piecewise.incremental").apply_to(m)
+ SolverFactory('gurobi').solve(m)
+ self.assertAlmostEqual(0.45, value(m.obj))
diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py
new file mode 100644
index 00000000000..b937e09ce8b
--- /dev/null
+++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py
@@ -0,0 +1,760 @@
+# ___________________________________________________________________________
+#
+# Pyomo: Python Optimization Modeling Objects
+# Copyright (c) 2008-2024
+# National Technology and Engineering Solutions of Sandia, LLC
+# Under the terms of Contract DE-NA0003525 with National Technology and
+# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
+# rights in this software.
+# This software is distributed under the 3-clause BSD License.
+# ___________________________________________________________________________
+
+from io import StringIO
+import logging
+
+from pyomo.common.dependencies import attempt_import, scipy_available, numpy_available
+from pyomo.common.log import LoggingIntercept
+import pyomo.common.unittest as unittest
+from pyomo.contrib.piecewise import PiecewiseLinearFunction
+from pyomo.contrib.piecewise.transform.nonlinear_to_pwl import (
+ NonlinearToPWL,
+ DomainPartitioningMethod,
+)
+from pyomo.core.base.expression import _ExpressionData
+from pyomo.core.expr.compare import (
+ assertExpressionsEqual,
+ assertExpressionsStructurallyEqual,
+)
+from pyomo.core.expr.numeric_expr import SumExpression
+from pyomo.environ import (
+ Binary,
+ ConcreteModel,
+ Var,
+ Constraint,
+ Integers,
+ TransformationFactory,
+ log,
+ Objective,
+ Reals,
+ SolverFactory,
+ TerminationCondition,
+ value,
+)
+
+gurobi_available = (
+ SolverFactory('gurobi').available(exception_flag=False)
+ and SolverFactory('gurobi').license_is_valid()
+)
+lineartree_available = attempt_import('lineartree')[1]
+sklearn_available = attempt_import('sklearn.linear_model')[1]
+
+
+class TestNonlinearToPWL_1D(unittest.TestCase):
+ def make_model(self):
+ m = ConcreteModel()
+ m.x = Var(bounds=(1, 10))
+ m.cons = Constraint(expr=log(m.x) >= 0.35)
+
+ return m
+
+ def check_pw_linear_log_x(self, m, pwlf, x1, x2, x3):
+ n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl')
+
+ points = [(x1,), (x2,), (x3,)]
+ self.assertEqual(pwlf._simplices, [(0, 1), (1, 2)])
+ self.assertEqual(pwlf._points, points)
+ self.assertEqual(len(pwlf._linear_functions), 2)
+
+ assertExpressionsStructurallyEqual(
+ self,
+ pwlf._linear_functions[0](m.x),
+ ((log(x2) - log(x1)) / (x2 - x1)) * m.x
+ + (log(x2) - ((log(x2) - log(x1)) / (x2 - x1)) * x2),
+ places=7,
+ )
+ assertExpressionsStructurallyEqual(
+ self,
+ pwlf._linear_functions[1](m.x),
+ ((log(x3) - log(x2)) / (x3 - x2)) * m.x
+ + (log(x3) - ((log(x3) - log(x2)) / (x3 - x2)) * x3),
+ places=7,
+ )
+
+ self.assertEqual(len(pwlf._expressions), 1)
+ new_cons = n_to_pwl.get_transformed_component(m.cons)
+ self.assertTrue(new_cons.active)
+ self.assertIs(
+ new_cons.body, pwlf._expressions[pwlf._expression_ids[new_cons.body.expr]]
+ )
+ self.assertIsNone(new_cons.ub)
+ self.assertEqual(new_cons.lb, 0.35)
+ self.assertIs(n_to_pwl.get_src_component(new_cons), m.cons)
+
+ quadratic = n_to_pwl.get_transformed_quadratic_constraints(m)
+ self.assertEqual(len(quadratic), 0)
+ nonlinear = n_to_pwl.get_transformed_nonlinear_constraints(m)
+ self.assertEqual(len(nonlinear), 1)
+ self.assertIn(m.cons, nonlinear)
+
+ @unittest.skipUnless(numpy_available, "Numpy is not available")
+ def test_log_constraint_uniform_grid(self):
+ m = self.make_model()
+
+ n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl')
+ n_to_pwl.apply_to(
+ m,
+ num_points=3,
+ domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID,
+ )
+
+ # cons is transformed
+ self.assertFalse(m.cons.active)
+
+ pwlf = list(
+ m.component_data_objects(PiecewiseLinearFunction, descend_into=True)
+ )
+ self.assertEqual(len(pwlf), 1)
+ pwlf = pwlf[0]
+
+ points = [(1.0009,), (5.5,), (9.9991,)]
+ (x1, x2, x3) = 1.0009, 5.5, 9.9991
+ self.check_pw_linear_log_x(m, pwlf, x1, x2, x3)
+
+ @unittest.skipUnless(numpy_available, "Numpy is not available")
+ def test_clone_transformed_model(self):
+ m = self.make_model()
+
+ n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl')
+ n_to_pwl.apply_to(
+ m,
+ num_points=3,
+ domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID,
+ )
+
+ twin = m.clone()
+
+ # cons is transformed
+ self.assertFalse(twin.cons.active)
+
+ pwlf = list(
+ twin.component_data_objects(PiecewiseLinearFunction, descend_into=True)
+ )
+ self.assertEqual(len(pwlf), 1)
+ pwlf = pwlf[0]
+
+ points = [(1.0009,), (5.5,), (9.9991,)]
+ (x1, x2, x3) = 1.0009, 5.5, 9.9991
+
+ self.check_pw_linear_log_x(twin, pwlf, x1, x2, x3)
+
+ @unittest.skipUnless(numpy_available, "Numpy is not available")
+ def test_log_constraint_random_grid(self):
+ m = self.make_model()
+
+ n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl')
+ # [ESJ 3/30/24]: The seed is actually set in the function for getting
+ # the points right now, so this will be deterministic.
+ n_to_pwl.apply_to(
+ m,
+ num_points=3,
+ domain_partitioning_method=DomainPartitioningMethod.RANDOM_GRID,
+ )
+
+ # cons is transformed
+ self.assertFalse(m.cons.active)
+
+ pwlf = list(
+ m.component_data_objects(PiecewiseLinearFunction, descend_into=True)
+ )
+ self.assertEqual(len(pwlf), 1)
+ pwlf = pwlf[0]
+
+ x1 = 4.370861069626263
+ x2 = 7.587945476302646
+ x3 = 9.556428757689245
+ self.check_pw_linear_log_x(m, pwlf, x1, x2, x3)
+
+ @unittest.skipUnless(numpy_available, "Numpy is not available")
+ def test_do_not_transform_quadratic_constraint(self):
+ m = self.make_model()
+ m.quad = Constraint(expr=m.x**2 <= 9)
+ m.lin = Constraint(expr=m.x >= 2)
+
+ n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl')
+ n_to_pwl.apply_to(
+ m,
+ num_points=3,
+ domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID,
+ approximate_quadratic_constraints=False,
+ )
+
+ # cons is transformed
+ self.assertFalse(m.cons.active)
+
+ pwlf = list(
+ m.component_data_objects(PiecewiseLinearFunction, descend_into=True)
+ )
+ self.assertEqual(len(pwlf), 1)
+ pwlf = pwlf[0]
+
+ points = [(1.0009,), (5.5,), (9.9991,)]
+ (x1, x2, x3) = 1.0009, 5.5, 9.9991
+ self.check_pw_linear_log_x(m, pwlf, x1, x2, x3)
+
+ # quad is not
+ self.assertTrue(m.quad.active)
+ # neither is the linear one
+ self.assertTrue(m.lin.active)
+
+ @unittest.skipUnless(numpy_available, "Numpy is not available")
+ def test_constraint_target(self):
+ m = self.make_model()
+ m.quad = Constraint(expr=m.x**2 <= 9)
+
+ n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl')
+ n_to_pwl.apply_to(
+ m,
+ num_points=3,
+ domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID,
+ targets=[m.cons],
+ )
+
+ # cons is transformed
+ self.assertFalse(m.cons.active)
+
+ pwlf = list(
+ m.component_data_objects(PiecewiseLinearFunction, descend_into=True)
+ )
+ self.assertEqual(len(pwlf), 1)
+ pwlf = pwlf[0]
+
+ points = [(1.0009,), (5.5,), (9.9991,)]
+ (x1, x2, x3) = 1.0009, 5.5, 9.9991
+ self.check_pw_linear_log_x(m, pwlf, x1, x2, x3)
+
+ # quad is not
+ self.assertTrue(m.quad.active)
+
+ def test_crazy_target_error(self):
+ m = self.make_model()
+
+ n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl')
+ with self.assertRaisesRegex(
+ ValueError,
+ "Target 'x' is not a Block, Constraint, or Objective. It "
+ "is of type '' and cannot "
+ "be transformed.",
+ ):
+ n_to_pwl.apply_to(
+ m,
+ num_points=3,
+ domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID,
+ targets=[m.x],
+ )
+
+ def test_cannot_approximate_constraints_with_unbounded_vars(self):
+ m = ConcreteModel()
+ m.x = Var()
+ m.quad = Constraint(expr=m.x**2 <= 9)
+
+ n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl')
+ with self.assertRaisesRegex(
+ ValueError,
+ "Cannot automatically approximate constraints with unbounded "
+ "variables. Var 'x' appearing in component 'quad' is missing "
+ "at least one bound",
+ ):
+ n_to_pwl.apply_to(
+ m,
+ num_points=3,
+ domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID,
+ )
+
+ def test_error_for_non_separable_exceeding_max_dimension(self):
+ m = ConcreteModel()
+ m.x = Var([0, 1, 2, 3, 4], bounds=(-4, 5))
+ m.ick = Constraint(expr=m.x[0] ** (m.x[1] * m.x[2] * m.x[3] * m.x[4]) <= 8)
+
+ n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl')
+ with self.assertRaisesRegex(
+ ValueError,
+ "Not approximating expression for component 'ick' as "
+ "it exceeds the maximum dimension of 4. Try increasing "
+ "'max_dimension' or additively separating the expression.",
+ ):
+ n_to_pwl.apply_to(
+ m,
+ num_points=3,
+ domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID,
+ max_dimension=4,
+ )
+
+ @unittest.skipUnless(numpy_available, "Numpy is not available")
+ @unittest.skipUnless(scipy_available, "Scipy is not available")
+ def test_do_not_additively_decompose_below_min_dimension(self):
+ m = ConcreteModel()
+ m.x = Var([0, 1, 2, 3, 4], bounds=(-4, 5))
+ m.c = Constraint(expr=m.x[0] * m.x[1] + m.x[3] <= 4)
+
+ n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl')
+ n_to_pwl.apply_to(
+ m,
+ num_points=3,
+ additively_decompose=True,
+ min_dimension_to_additively_decompose=4,
+ domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID,
+ )
+
+ transformed_c = n_to_pwl.get_transformed_component(m.c)
+ # This is only approximated by one pwlf:
+ self.assertIsInstance(transformed_c.body, _ExpressionData)
+
+ @unittest.skipUnless(numpy_available, "Numpy is not available")
+ def test_uniform_sampling_discrete_vars(self):
+ m = ConcreteModel()
+ m.x = Var(['rocky', 'bullwinkle'], domain=Binary)
+ m.y = Var(domain=Integers, bounds=(0, 5))
+ m.c = Constraint(expr=m.x['rocky'] * m.x['bullwinkle'] + m.y <= 4)
+
+ n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl')
+ output = StringIO()
+ with LoggingIntercept(output, 'pyomo.core', logging.WARNING):
+ n_to_pwl.apply_to(
+ m,
+ num_points=3,
+ additively_decompose=False,
+ domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID,
+ )
+ # No warnings (this is to check that we aren't emitting a bunch of
+ # warnings about setting variables outside of their domains)
+ self.assertEqual(output.getvalue().strip(), "")
+
+ transformed_c = n_to_pwl.get_transformed_component(m.c)
+ pwlf = transformed_c.body.expr.pw_linear_function
+
+ # should sample 0, 1 for th m.x's
+ # should sample 0, 2, 5 for m.y (because of half to even rounding (*sigh*))
+ points = set(pwlf._points)
+ self.assertEqual(len(points), 12)
+ for x in [0, 1]:
+ for y in [0, 1]:
+ for z in [0, 2, 5]:
+ self.assertIn((x, y, z), points)
+
+ @unittest.skipUnless(numpy_available, "Numpy is not available")
+ @unittest.skipUnless(scipy_available, "Scipy is not available")
+ def test_uniform_sampling_discrete_vars(self):
+ m = ConcreteModel()
+ m.x = Var(['rocky', 'bullwinkle'], domain=Binary)
+ m.y = Var(domain=Integers, bounds=(0, 5))
+ m.c = Constraint(expr=m.x['rocky'] * m.x['bullwinkle'] + m.y <= 4)
+
+ n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl')
+ output = StringIO()
+ with LoggingIntercept(output, 'pyomo.core', logging.WARNING):
+ n_to_pwl.apply_to(
+ m,
+ num_points=3,
+ additively_decompose=False,
+ domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID,
+ )
+ # No warnings (this is to check that we aren't emitting a bunch of
+ # warnings about setting variables outside of their domains)
+ self.assertEqual(output.getvalue().strip(), "")
+
+ transformed_c = n_to_pwl.get_transformed_component(m.c)
+ pwlf = transformed_c.body.expr.pw_linear_function
+
+ # should sample 0, 1 for th m.x's
+ # should sample 0, 2, 5 for m.y (because of half to even rounding (*sigh*))
+ points = set(pwlf._points)
+ self.assertEqual(len(points), 12)
+ for x in [0, 1]:
+ for y in [0, 1]:
+ for z in [0, 2, 5]:
+ self.assertIn((x, y, z), points)
+
+ @unittest.skipUnless(numpy_available, "Numpy is not available")
+ @unittest.skipUnless(scipy_available, "Scipy is not available")
+ def test_random_sampling_discrete_vars(self):
+ m = ConcreteModel()
+ m.x = Var(['rocky', 'bullwinkle'], domain=Binary)
+ m.y = Var(domain=Integers, bounds=(0, 5))
+ m.c = Constraint(expr=m.x['rocky'] * m.x['bullwinkle'] + m.y <= 4)
+
+ n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl')
+ output = StringIO()
+ with LoggingIntercept(output, 'pyomo.core', logging.WARNING):
+ n_to_pwl.apply_to(
+ m,
+ num_points=3,
+ additively_decompose=False,
+ domain_partitioning_method=DomainPartitioningMethod.RANDOM_GRID,
+ )
+ # No warnings (this is to check that we aren't emitting a bunch of
+ # warnings about setting variables outside of their domains)
+ self.assertEqual(output.getvalue().strip(), "")
+
+ transformed_c = n_to_pwl.get_transformed_component(m.c)
+ pwlf = transformed_c.body.expr.pw_linear_function
+
+ # should sample 0, 1 for th m.x's
+ # Happen to get 0, 1, 5 for m.y
+ points = set(pwlf._points)
+ self.assertEqual(len(points), 12)
+ for x in [0, 1]:
+ for y in [0, 1]:
+ for z in [0, 1, 5]:
+ self.assertIn((x, y, z), points)
+
+
+class TestNonlinearToPWL_2D(unittest.TestCase):
+ def make_paraboloid_model(self):
+ m = ConcreteModel()
+ m.x1 = Var(bounds=(0, 3))
+ m.x2 = Var(bounds=(1, 7))
+ m.obj = Objective(expr=m.x1**2 + m.x2**2)
+
+ return m
+
+ def check_pw_linear_paraboloid(self, m, pwlf, x1, x2, y1, y2):
+ n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl')
+ points = [(x1, y1), (x1, y2), (x2, y1), (x2, y2)]
+ self.assertEqual(pwlf._points, points)
+ self.assertEqual(pwlf._simplices, [(0, 1, 3), (0, 2, 3)])
+ self.assertEqual(len(pwlf._linear_functions), 2)
+
+ # just check that the linear functions make sense--they intersect the
+ # paraboloid at the vertices of the simplices.
+ self.assertAlmostEqual(pwlf._linear_functions[0](x1, y1), x1**2 + y1**2)
+ self.assertAlmostEqual(pwlf._linear_functions[0](x1, y2), x1**2 + y2**2)
+ self.assertAlmostEqual(pwlf._linear_functions[0](x2, y2), x2**2 + y2**2)
+
+ self.assertAlmostEqual(pwlf._linear_functions[1](x1, y1), x1**2 + y1**2)
+ self.assertAlmostEqual(pwlf._linear_functions[1](x2, y1), x2**2 + y1**2)
+ self.assertAlmostEqual(pwlf._linear_functions[1](x2, y2), x2**2 + y2**2)
+
+ self.assertEqual(len(pwlf._expressions), 1)
+ new_obj = n_to_pwl.get_transformed_component(m.obj)
+ self.assertTrue(new_obj.active)
+ self.assertIs(
+ new_obj.expr, pwlf._expressions[pwlf._expression_ids[new_obj.expr.expr]]
+ )
+ self.assertIs(n_to_pwl.get_src_component(new_obj), m.obj)
+
+ quadratic = n_to_pwl.get_transformed_quadratic_constraints(m)
+ self.assertEqual(len(quadratic), 0)
+ nonlinear = n_to_pwl.get_transformed_nonlinear_constraints(m)
+ self.assertEqual(len(nonlinear), 0)
+ quadratic = n_to_pwl.get_transformed_quadratic_objectives(m)
+ self.assertEqual(len(quadratic), 1)
+ self.assertIn(m.obj, quadratic)
+ nonlinear = n_to_pwl.get_transformed_nonlinear_objectives(m)
+ self.assertEqual(len(nonlinear), 0)
+
+ @unittest.skipUnless(numpy_available, "Numpy is not available")
+ @unittest.skipUnless(scipy_available, "Scipy is not available")
+ def test_paraboloid_objective_uniform_grid(self):
+ m = self.make_paraboloid_model()
+
+ n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl')
+ n_to_pwl.apply_to(
+ m,
+ num_points=2,
+ domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID,
+ )
+
+ # check obj is transformed
+ self.assertFalse(m.obj.active)
+
+ pwlf = list(
+ m.component_data_objects(PiecewiseLinearFunction, descend_into=True)
+ )
+ self.assertEqual(len(pwlf), 1)
+ pwlf = pwlf[0]
+
+ x1 = 0.00030000000000000003
+ x2 = 2.9997
+ y1 = 1.0006
+ y2 = 6.9994
+
+ self.check_pw_linear_paraboloid(m, pwlf, x1, x2, y1, y2)
+
+ @unittest.skipUnless(numpy_available, "Numpy is not available")
+ @unittest.skipUnless(scipy_available, "Scipy is not available")
+ def test_multivariate_clone(self):
+ m = self.make_paraboloid_model()
+
+ n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl')
+ n_to_pwl.apply_to(
+ m,
+ num_points=2,
+ domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID,
+ )
+
+ twin = m.clone()
+
+ # check obj is transformed
+ self.assertFalse(twin.obj.active)
+
+ pwlf = list(
+ twin.component_data_objects(PiecewiseLinearFunction, descend_into=True)
+ )
+ self.assertEqual(len(pwlf), 1)
+ pwlf = pwlf[0]
+
+ x1 = 0.00030000000000000003
+ x2 = 2.9997
+ y1 = 1.0006
+ y2 = 6.9994
+
+ self.check_pw_linear_paraboloid(twin, pwlf, x1, x2, y1, y2)
+
+ @unittest.skipUnless(numpy_available, "Numpy is not available")
+ @unittest.skipUnless(scipy_available, "Scipy is not available")
+ def test_objective_target(self):
+ m = self.make_paraboloid_model()
+
+ m.some_other_nonlinear_constraint = Constraint(expr=m.x1**3 + m.x2 <= 6)
+
+ n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl')
+ n_to_pwl.apply_to(
+ m,
+ num_points=2,
+ domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID,
+ targets=[m.obj],
+ )
+
+ # check obj is transformed
+ self.assertFalse(m.obj.active)
+
+ pwlf = list(
+ m.component_data_objects(PiecewiseLinearFunction, descend_into=True)
+ )
+ self.assertEqual(len(pwlf), 1)
+ pwlf = pwlf[0]
+
+ x1 = 0.00030000000000000003
+ x2 = 2.9997
+ y1 = 1.0006
+ y2 = 6.9994
+
+ self.check_pw_linear_paraboloid(m, pwlf, x1, x2, y1, y2)
+
+ # and check that the constraint isn't transformed
+ self.assertTrue(m.some_other_nonlinear_constraint.active)
+
+ def test_do_not_transform_quadratic_objective(self):
+ m = self.make_paraboloid_model()
+
+ n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl')
+ n_to_pwl.apply_to(
+ m,
+ num_points=2,
+ domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID,
+ approximate_quadratic_objectives=False,
+ )
+
+ # check obj is *not* transformed
+ self.assertTrue(m.obj.active)
+
+ quadratic = n_to_pwl.get_transformed_quadratic_constraints(m)
+ self.assertEqual(len(quadratic), 0)
+ nonlinear = n_to_pwl.get_transformed_nonlinear_constraints(m)
+ self.assertEqual(len(nonlinear), 0)
+ quadratic = n_to_pwl.get_transformed_quadratic_objectives(m)
+ self.assertEqual(len(quadratic), 0)
+ nonlinear = n_to_pwl.get_transformed_nonlinear_objectives(m)
+ self.assertEqual(len(nonlinear), 0)
+
+
+@unittest.skipUnless(lineartree_available, "lineartree not available")
+@unittest.skipUnless(sklearn_available, "sklearn not available")
+class TestLinearTreeDomainPartitioning(unittest.TestCase):
+ def make_absolute_value_model(self):
+ m = ConcreteModel()
+ m.x = Var(bounds=(-10, 10))
+ m.obj = Objective(expr=abs(m.x))
+
+ return m
+
+ def test_linear_model_tree_uniform(self):
+ m = self.make_absolute_value_model()
+ n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl')
+ n_to_pwl.apply_to(
+ m,
+ num_points=301, # sample a lot so we train a good tree
+ domain_partitioning_method=DomainPartitioningMethod.LINEAR_MODEL_TREE_UNIFORM,
+ linear_tree_max_depth=1, # force parsimony
+ )
+
+ transformed_obj = n_to_pwl.get_transformed_component(m.obj)
+ pwlf = transformed_obj.expr.expr.pw_linear_function
+
+ self.assertEqual(len(pwlf._simplices), 2)
+ self.assertEqual(pwlf._simplices, [(0, 1), (1, 2)])
+ self.assertEqual(pwlf._points, [(-10,), (-0.08402,), (10,)])
+ self.assertEqual(len(pwlf._linear_functions), 2)
+ assertExpressionsEqual(self, pwlf._linear_functions[0](m.x), -1.0 * m.x)
+ assertExpressionsStructurallyEqual(
+ self,
+ pwlf._linear_functions[1](m.x),
+ # pretty close to m.x, but we're a bit off because we don't have 0
+ # as a breakpoint.
+ 0.9833360108369479 * m.x + 0.16663989163052034,
+ places=7,
+ )
+
+ def test_linear_model_tree_random(self):
+ m = self.make_absolute_value_model()
+ n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl')
+ n_to_pwl.apply_to(
+ m,
+ num_points=300, # sample a lot so we train a good tree
+ domain_partitioning_method=DomainPartitioningMethod.LINEAR_MODEL_TREE_RANDOM,
+ linear_tree_max_depth=1, # force parsimony
+ )
+
+ transformed_obj = n_to_pwl.get_transformed_component(m.obj)
+ pwlf = transformed_obj.expr.expr.pw_linear_function
+
+ self.assertEqual(len(pwlf._simplices), 2)
+ self.assertEqual(pwlf._simplices, [(0, 1), (1, 2)])
+ self.assertEqual(pwlf._points, [(-10,), (-0.03638,), (10,)])
+ self.assertEqual(len(pwlf._linear_functions), 2)
+ assertExpressionsEqual(self, pwlf._linear_functions[0](m.x), -1.0 * m.x)
+ assertExpressionsStructurallyEqual(
+ self,
+ pwlf._linear_functions[1](m.x),
+ # pretty close to m.x, but we're a bit off because we don't have 0
+ # as a breakpoint.
+ 0.9927503741388829 * m.x + 0.07249625861117256,
+ places=7,
+ )
+
+ def test_linear_model_tree_random_auto_depth_tree(self):
+ m = self.make_absolute_value_model()
+ n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl')
+ n_to_pwl.apply_to(
+ m,
+ num_points=100, # sample a lot but not too many because this one is
+ # more prone to overfitting
+ domain_partitioning_method=DomainPartitioningMethod.LINEAR_MODEL_TREE_RANDOM,
+ )
+
+ transformed_obj = n_to_pwl.get_transformed_component(m.obj)
+ pwlf = transformed_obj.expr.expr.pw_linear_function
+
+ print(pwlf._simplices)
+ print(pwlf._points)
+ for f in pwlf._linear_functions:
+ print(f(m.x))
+
+ # We end up with 8, which is just what happens, but it's not a terrible
+ # approximation
+ self.assertEqual(len(pwlf._simplices), 8)
+ self.assertEqual(
+ pwlf._simplices,
+ [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 7), (7, 8)],
+ )
+ self.assertEqual(
+ pwlf._points,
+ [
+ (-10,),
+ (-9.24119,),
+ (-8.71428,),
+ (-8.11135,),
+ (0.06048,),
+ (0.70015,),
+ (1.9285,),
+ (2.15597,),
+ (10,),
+ ],
+ )
+ self.assertEqual(len(pwlf._linear_functions), 8)
+ for i in range(3):
+ assertExpressionsEqual(self, pwlf._linear_functions[i](m.x), -1.0 * m.x)
+ assertExpressionsStructurallyEqual(
+ self,
+ pwlf._linear_functions[3](m.x),
+ # pretty close to - m.x, but we're a bit off because we don't have 0
+ # as a breakpoint.
+ -0.9851979299618323 * m.x + 0.12006477080409184,
+ places=7,
+ )
+ for i in range(4, 8):
+ assertExpressionsEqual(self, pwlf._linear_functions[i](m.x), m.x)
+
+
+class TestNonlinearToPWLIntegration(unittest.TestCase):
+ @unittest.skipUnless(gurobi_available, "Gurobi is not available")
+ @unittest.skipUnless(scipy_available, "Scipy is not available")
+ def test_transform_and_solve_additively_decomposes_model(self):
+ # A bit of an integration test to make sure that we build additively
+ # decomposed pw-linear approximations in such a way that they are
+ # transformed to MILP and solved correctly. (Largely because we have to
+ # be careful to make sure that we don't ever directly insert
+ # PiecewiseLinearExpression objects into expressions and are instead
+ # using the ExpressionData that points to them (and will eventually be
+ # replaced in transformation))
+ m = ConcreteModel()
+ m.x1 = Var(within=Reals, bounds=(0, 2), initialize=1.745)
+ m.x4 = Var(within=Reals, bounds=(0, 5), initialize=3.048)
+ m.x7 = Var(within=Reals, bounds=(0.9, 0.95), initialize=0.928)
+ m.obj = Objective(expr=-6.3 * m.x4 * m.x7 + 5.04 * m.x1)
+ n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl')
+ xm = n_to_pwl.create_using(
+ m,
+ num_points=4,
+ domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID,
+ additively_decompose=True,
+ )
+
+ self.assertFalse(xm.obj.active)
+ new_obj = n_to_pwl.get_transformed_component(xm.obj)
+ self.assertIs(n_to_pwl.get_src_component(new_obj), xm.obj)
+ self.assertTrue(new_obj.active)
+ # two terms
+ self.assertIsInstance(new_obj.expr, SumExpression)
+ self.assertEqual(len(new_obj.expr.args), 2)
+ first = new_obj.expr.args[0]
+ pwlf = first.expr.pw_linear_function
+ all_pwlf = list(
+ xm.component_data_objects(PiecewiseLinearFunction, descend_into=True)
+ )
+ self.assertEqual(len(all_pwlf), 1)
+ # It is on the active tree.
+ self.assertIs(pwlf, all_pwlf[0])
+
+ second = new_obj.expr.args[1]
+ assertExpressionsEqual(self, second, 5.04 * xm.x1)
+
+ objs = n_to_pwl.get_transformed_nonlinear_objectives(xm)
+ self.assertEqual(len(objs), 0)
+ objs = n_to_pwl.get_transformed_quadratic_objectives(xm)
+ self.assertEqual(len(objs), 1)
+ self.assertIn(xm.obj, objs)
+ self.assertEqual(len(n_to_pwl.get_transformed_nonlinear_constraints(xm)), 0)
+ self.assertEqual(len(n_to_pwl.get_transformed_quadratic_constraints(xm)), 0)
+
+ TransformationFactory('contrib.piecewise.outer_repn_gdp').apply_to(xm)
+ TransformationFactory('gdp.bigm').apply_to(xm)
+ opt = SolverFactory('gurobi')
+ results = opt.solve(xm)
+ self.assertEqual(
+ results.solver.termination_condition, TerminationCondition.optimal
+ )
+
+ # solve the original
+ opt.options['NonConvex'] = 2
+ results = opt.solve(m)
+ self.assertEqual(
+ results.solver.termination_condition, TerminationCondition.optimal
+ )
+
+ # Not a bad approximation:
+ self.assertAlmostEqual(value(xm.obj), value(m.obj), places=2)
+
+ self.assertAlmostEqual(value(xm.x4), value(m.x4), places=3)
+ self.assertAlmostEqual(value(xm.x7), value(m.x7), places=4)
+ self.assertAlmostEqual(value(xm.x1), value(m.x1), places=7)
diff --git a/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py b/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py
index 571601fefbc..a49519ae25e 100644
--- a/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py
+++ b/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py
@@ -16,7 +16,7 @@
from pyomo.common.dependencies import attempt_import
from pyomo.common.log import LoggingIntercept
import pyomo.common.unittest as unittest
-from pyomo.contrib.piecewise import PiecewiseLinearFunction
+from pyomo.contrib.piecewise import PiecewiseLinearFunction, Triangulation
from pyomo.core.expr.compare import (
assertExpressionsEqual,
assertExpressionsStructurallyEqual,
@@ -118,6 +118,23 @@ def test_pw_linear_approx_of_ln_x_tabular_data(self):
)
self.check_ln_x_approx(m.pw, m.x)
+ def test_pw_linear_approx_of_ln_x_j1(self):
+ m = self.make_ln_x_model()
+ m.pw = PiecewiseLinearFunction(
+ points=[1, 3, 6, 10], triangulation=Triangulation.J1, function=m.f
+ )
+ self.check_ln_x_approx(m.pw, m.x)
+ # we disregard their request because it's 1D
+ self.assertEqual(m.pw.triangulation, Triangulation.AssumeValid)
+
+ def test_pw_linear_approx_of_ln_x_user_defined_segments(self):
+ m = self.make_ln_x_model()
+ m.pw = PiecewiseLinearFunction(
+ simplices=[[1, 3], [3, 6], [6, 10]], function=m.f
+ )
+ self.check_ln_x_approx(m.pw, m.x)
+ self.assertEqual(m.pw.triangulation, Triangulation.Unknown)
+
def test_use_pw_function_in_constraint(self):
m = self.make_ln_x_model()
m.pw = PiecewiseLinearFunction(
@@ -302,6 +319,27 @@ def test_pw_linear_approx_of_paraboloid_points(self):
)
self.check_pw_linear_approximation(m)
+ @unittest.skipUnless(numpy_available, "numpy is not available")
+ def test_pw_linear_approx_of_paraboloid_j1(self):
+ m = self.make_model()
+ m.pw = PiecewiseLinearFunction(
+ points=[
+ (0, 1),
+ (0, 4),
+ (0, 7),
+ (3, 1),
+ (3, 4),
+ (3, 7),
+ (4, 1),
+ (4, 4),
+ (4, 7),
+ ],
+ function=m.g,
+ triangulation=Triangulation.OrderedJ1,
+ )
+ self.assertEqual(len(m.pw._simplices), 8)
+ self.assertEqual(m.pw.triangulation, Triangulation.OrderedJ1)
+
@unittest.skipUnless(scipy_available, "scipy is not available")
def test_pw_linear_approx_tabular_data(self):
m = self.make_model()
diff --git a/pyomo/contrib/piecewise/tests/test_triangulations.py b/pyomo/contrib/piecewise/tests/test_triangulations.py
new file mode 100644
index 00000000000..7217750dfb1
--- /dev/null
+++ b/pyomo/contrib/piecewise/tests/test_triangulations.py
@@ -0,0 +1,245 @@
+# ___________________________________________________________________________
+#
+# Pyomo: Python Optimization Modeling Objects
+# Copyright (c) 2008-2024
+# National Technology and Engineering Solutions of Sandia, LLC
+# Under the terms of Contract DE-NA0003525 with National Technology and
+# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
+# rights in this software.
+# This software is distributed under the 3-clause BSD License.
+# ___________________________________________________________________________
+
+
+import itertools
+from unittest import skipUnless
+import pyomo.common.unittest as unittest
+from pyomo.contrib.piecewise.ordered_3d_j1_triangulation_data import (
+ get_hamiltonian_paths,
+ _get_double_cube_graph,
+)
+from pyomo.contrib.piecewise.triangulations import (
+ get_unordered_j1_triangulation,
+ get_ordered_j1_triangulation,
+ _get_Gn_hamiltonian,
+ _get_grid_hamiltonian,
+)
+from pyomo.common.dependencies import numpy as np, numpy_available, networkx_available
+from math import factorial
+import itertools
+
+
+class TestTriangulations(unittest.TestCase):
+
+ # check basic functionality for the unordered j1 triangulation.
+ @unittest.skipUnless(numpy_available, "numpy is not available")
+ def test_J1_small(self):
+ points = [
+ [0.5, 0.5], # 0
+ [0.5, 1.5], # 1
+ [0.5, 2.5], # 2
+ [1.5, 0.5], # 3
+ [1.5, 1.5], # 4
+ [1.5, 2.5], # 5
+ [2.5, 0.5], # 6
+ [2.5, 1.5], # 7
+ [2.5, 2.5], # 8
+ ]
+ triangulation = get_unordered_j1_triangulation(points, 2)
+ self.assertTrue(
+ np.array_equal(
+ triangulation.simplices,
+ np.array(
+ [
+ [0, 1, 4],
+ [1, 2, 4],
+ [4, 6, 7],
+ [4, 7, 8],
+ [0, 3, 4],
+ [2, 4, 5],
+ [3, 4, 6],
+ [4, 5, 8],
+ ]
+ ),
+ )
+ )
+
+ def check_J1_ordered(self, points, num_points, dim):
+ ordered_triangulation = get_ordered_j1_triangulation(points, dim).simplices
+ self.assertEqual(
+ len(ordered_triangulation), factorial(dim) * (num_points - 1) ** dim
+ )
+ for idx, first_simplex in enumerate(ordered_triangulation):
+ if idx != len(ordered_triangulation) - 1:
+ second_simplex = ordered_triangulation[idx + 1]
+ # test property (2) which also guarantees property (1) (from Vielma 2010)
+ self.assertEqual(
+ first_simplex[-1],
+ second_simplex[0],
+ msg="Last and first vertices of adjacent simplices did not match",
+ )
+ # The way I am constructing these, they should always share an (n-1)-face.
+ # Check that too for good measure.
+ count = len(set(first_simplex).intersection(set(second_simplex)))
+ self.assertEqual(count, dim) # (n-1)-face has n points
+
+ @unittest.skipUnless(numpy_available, "numpy is not available")
+ def test_J1_ordered_2d(self):
+ self.check_J1_ordered(list(itertools.product([0, 1, 2], [1, 2.4, 3])), 3, 2)
+ self.check_J1_ordered(
+ list(itertools.product([0, 1, 2, 4, 5], [1, 2.4, 3, 5, 6])), 5, 2
+ )
+ self.check_J1_ordered(
+ list(
+ itertools.product([0, 1, 2, 4, 5, 6.3, 7.1], [1, 2.4, 3, 5, 6, 9.1, 10])
+ ),
+ 7,
+ 2,
+ )
+ self.check_J1_ordered(
+ list(
+ itertools.product(
+ [0, 1, 2, 4, 5, 6.3, 7.1, 7.2, 7.3],
+ [1, 2.4, 3, 5, 6, 9.1, 10, 11, 12],
+ )
+ ),
+ 9,
+ 2,
+ )
+
+ @unittest.skipUnless(numpy_available, "numpy is not available")
+ def test_J1_ordered_3d(self):
+ self.check_J1_ordered(
+ list(itertools.product([0, 1, 2], [1, 2.4, 3], [2, 3, 4])), 3, 3
+ )
+ self.check_J1_ordered(
+ list(
+ itertools.product([0, 1, 2, 4, 5], [1, 2.4, 3, 5, 6], [-1, 0, 1, 2, 3])
+ ),
+ 5,
+ 3,
+ )
+ self.check_J1_ordered(
+ list(
+ itertools.product(
+ [0, 1, 2, 4, 5, 6, 7],
+ [1, 2.4, 3, 5, 6, 6.5, 7],
+ [-1, 0, 1, 2, 3, 4, 5],
+ )
+ ),
+ 7,
+ 3,
+ )
+ self.check_J1_ordered(
+ list(
+ itertools.product(
+ [0, 1, 2, 4, 5, 6, 7, 8, 9],
+ [1, 2.4, 3, 5, 6, 6.5, 7, 8, 9],
+ [-1, 0, 1, 2, 3, 4, 5, 6, 7],
+ )
+ ),
+ 9,
+ 3,
+ )
+
+ @unittest.skipUnless(numpy_available, "numpy is not available")
+ def test_J1_ordered_4d_and_above(self):
+ self.check_J1_ordered(
+ list(
+ itertools.product(
+ [0, 1, 2, 4, 5],
+ [1, 2.4, 3, 5, 6],
+ [-1, 0, 1, 2, 3],
+ [1, 2, 3, 4, 5],
+ )
+ ),
+ 5,
+ 4,
+ )
+ self.check_J1_ordered(
+ list(
+ itertools.product(
+ [0, 1, 2, 4, 5],
+ [1, 2.4, 3, 5, 6],
+ [-1, 0, 1, 2, 3],
+ [1, 2, 3, 4, 5],
+ [2, 3, 4, 5, 6],
+ )
+ ),
+ 5,
+ 5,
+ )
+
+ def check_Gn_hamiltonian_path(self, n, start_permutation, target_symbol, last):
+ path = _get_Gn_hamiltonian(n, start_permutation, target_symbol, last)
+ self.assertEqual(len(path), factorial(n))
+ self.assertEqual(path[0], start_permutation)
+ if last:
+ self.assertEqual(path[-1][-1], target_symbol)
+ else:
+ self.assertEqual(path[-1][0], target_symbol)
+ for pi in itertools.permutations(range(1, n + 1), n):
+ self.assertTrue(tuple(pi) in path)
+ for i in range(len(path) - 1):
+ diff_indices = [j for j in range(n) if path[i][j] != path[i + 1][j]]
+ self.assertEqual(len(diff_indices), 2)
+ self.assertEqual(diff_indices[0], diff_indices[1] - 1)
+ self.assertEqual(path[i][diff_indices[0]], path[i + 1][diff_indices[1]])
+ self.assertEqual(path[i][diff_indices[1]], path[i + 1][diff_indices[0]])
+
+ def test_Gn_hamiltonian_paths(self):
+ # each of the base cases
+ self.check_Gn_hamiltonian_path(4, (1, 2, 3, 4), 1, False)
+ self.check_Gn_hamiltonian_path(4, (1, 2, 3, 4), 2, False)
+ self.check_Gn_hamiltonian_path(4, (1, 2, 3, 4), 3, False)
+ self.check_Gn_hamiltonian_path(4, (1, 2, 3, 4), 4, False)
+ # some variants with start permutations and/or last
+ self.check_Gn_hamiltonian_path(4, (3, 4, 1, 2), 2, False)
+ self.check_Gn_hamiltonian_path(4, (1, 3, 2, 4), 3, True)
+ self.check_Gn_hamiltonian_path(4, (1, 4, 2, 3), 4, True)
+ self.check_Gn_hamiltonian_path(4, (1, 2, 3, 4), 2, True)
+ # some recursive cases
+ self.check_Gn_hamiltonian_path(5, (1, 2, 3, 4, 5), 1, False)
+ self.check_Gn_hamiltonian_path(5, (1, 2, 3, 4, 5), 3, False)
+ self.check_Gn_hamiltonian_path(5, (1, 2, 3, 4, 5), 5, False)
+ self.check_Gn_hamiltonian_path(5, (1, 2, 4, 3, 5), 5, True)
+ self.check_Gn_hamiltonian_path(6, (6, 1, 2, 4, 3, 5), 5, True)
+ self.check_Gn_hamiltonian_path(6, (6, 1, 2, 4, 3, 5), 5, False)
+ self.check_Gn_hamiltonian_path(7, (1, 2, 3, 4, 5, 6, 7), 7, False)
+
+ def check_grid_hamiltonian(self, dim, length):
+ path = _get_grid_hamiltonian(dim, length)
+ self.assertEqual(len(path), length**dim)
+ for x in itertools.product(range(length), repeat=dim):
+ self.assertTrue(list(x) in path)
+ for i in range(len(path) - 1):
+ diff_indices = [j for j in range(dim) if path[i][j] != path[i + 1][j]]
+ self.assertEqual(len(diff_indices), 1)
+ self.assertEqual(
+ abs(path[i][diff_indices[0]] - path[i + 1][diff_indices[0]]), 1
+ )
+
+ def test_grid_hamiltonian_paths(self):
+ self.check_grid_hamiltonian(1, 5)
+ self.check_grid_hamiltonian(2, 5)
+ self.check_grid_hamiltonian(2, 8)
+ self.check_grid_hamiltonian(3, 5)
+ self.check_grid_hamiltonian(4, 3)
+
+
+@unittest.skipUnless(networkx_available, "Networkx is not available")
+class TestHamiltonianPaths(unittest.TestCase):
+ def test_hamiltonian_paths(self):
+ G = _get_double_cube_graph()
+
+ paths = get_hamiltonian_paths()
+ self.assertEqual(len(paths), 60)
+
+ for ((s1, t1), (s2, t2)), path in paths.items():
+ # ESJ: I'm not quite sure how to check this is *the right* path
+ # given the key?
+
+ # Check it's Hamiltonian
+ self.assertEqual(len(path), 48)
+ # Check it's a path
+ for idx in range(1, 48):
+ self.assertTrue(G.has_edge(path[idx - 1], path[idx]))
diff --git a/pyomo/contrib/piecewise/transform/incremental.py b/pyomo/contrib/piecewise/transform/incremental.py
new file mode 100644
index 00000000000..f7143676b58
--- /dev/null
+++ b/pyomo/contrib/piecewise/transform/incremental.py
@@ -0,0 +1,188 @@
+# ___________________________________________________________________________
+#
+# Pyomo: Python Optimization Modeling Objects
+# Copyright (c) 2008-2024
+# National Technology and Engineering Solutions of Sandia, LLC
+# Under the terms of Contract DE-NA0003525 with National Technology and
+# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
+# rights in this software.
+# This software is distributed under the 3-clause BSD License.
+# ___________________________________________________________________________
+
+from pyomo.contrib.piecewise.transform.piecewise_linear_transformation_base import (
+ PiecewiseLinearTransformationBase,
+)
+from pyomo.contrib.piecewise.triangulations import Triangulation
+from pyomo.core import Constraint, Binary, Var, RangeSet, Param
+from pyomo.core.base import TransformationFactory
+
+
+@TransformationFactory.register(
+ "contrib.piecewise.incremental",
+ doc="""
+ The incremental MIP formulation of a piecewise-linear function, as described
+ by [1]. To work in the multivariate case, the underlying triangulation must
+ satisfy these properties:
+ (1) The simplices are ordered T_1, ..., T_N such that T_i has nonempty intersection
+ with T_{i+1}. It doesn't have to be a whole face; just a vertex is enough.
+ (2) On each simplex T_i, the vertices are ordered T_i^1, ..., T_i^n such
+ that T_i^n = T_{i+1}^1
+ In Pyomo, the Triangulation.OrderedJ1 triangulation is compatible with this
+ transformation.
+
+ References
+ ----------
+ [1] J.P. Vielma, S. Ahmed, and G. Nemhauser, "Mixed-integer models
+ for nonseparable piecewise-linear optimization: unifying framework
+ and extensions," Operations Research, vol. 58, no. 2, pp. 305-315,
+ 2010.
+ """,
+)
+class IncrementalMIPTransformation(PiecewiseLinearTransformationBase):
+
+ CONFIG = PiecewiseLinearTransformationBase.CONFIG()
+ _transformation_name = "pw_linear_incremental"
+
+ # Implement to use PiecewiseLinearTransformationBase. This function returns the Var
+ # that replaces the transformed piecewise linear expr
+ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block):
+ if pw_linear_func.triangulation not in (
+ Triangulation.OrderedJ1,
+ Triangulation.AssumeValid,
+ ):
+ # almost certain not to work
+ raise ValueError(
+ "Incremental transformation specified, but the triangulation "
+ f"{pw_linear_func.triangulation} may not be appropriately ordered. This "
+ "would likely lead to incorrect results! The built-in "
+ "Triangulation.OrderedJ1 triangulation has an appropriate ordering for "
+ "this transformation. If you know what you are doing, you can also "
+ "suppress this error by setting the triangulation tag to "
+ "Triangulation.AssumeValid during PiecewiseLinearFunction construction."
+ )
+ # Get a new Block() in transformation_block.transformed_functions, which
+ # is a Block(Any)
+ transBlock = transformation_block.transformed_functions[
+ len(transformation_block.transformed_functions)
+ ]
+
+ # Dimensionality of the PWLF
+ dimension = pw_expr.nargs()
+ transBlock.dimension_indices = RangeSet(0, dimension - 1)
+
+ # Substitute Var that will hold the value of the PWLE
+ substitute_var = transBlock.substitute_var = Var()
+ pw_linear_func.map_transformation_var(pw_expr, substitute_var)
+
+ # Bounds for the substitute_var that we will widen
+ substitute_var_lb = float("inf")
+ substitute_var_ub = -float("inf")
+
+ # Simplices are tuples of indices of points. Give them their own indices, too
+ simplices = pw_linear_func._simplices
+ num_simplices = len(simplices)
+ transBlock.simplex_indices = RangeSet(0, num_simplices - 1)
+ transBlock.simplex_indices_except_last = RangeSet(0, num_simplices - 2)
+ # Assumption: the simplices are really simplices and all have the same number of
+ # points, which is dimension + 1
+ transBlock.simplex_point_indices = RangeSet(0, dimension)
+ transBlock.nonzero_simplex_point_indices = RangeSet(1, dimension)
+ transBlock.last_simplex_point_index = Param(initialize=dimension)
+
+ # We don't seem to get a convenient opportunity later, so let's just widen
+ # the bounds here. All we need to do is go through the corners of each simplex.
+ for P, linear_func in zip(
+ transBlock.simplex_indices, pw_linear_func._linear_functions
+ ):
+ for v in transBlock.simplex_point_indices:
+ val = linear_func(*pw_linear_func._points[simplices[P][v]])
+ if val < substitute_var_lb:
+ substitute_var_lb = val
+ if val > substitute_var_ub:
+ substitute_var_ub = val
+ # Now set those bounds
+ transBlock.substitute_var.setlb(substitute_var_lb)
+ transBlock.substitute_var.setub(substitute_var_ub)
+
+ # Initial vertex (v_0^0 in Vielma)
+ initial_vertex = pw_linear_func._points[simplices[0][0]]
+
+ # delta_i^j = delta[simplex][point]
+ transBlock.delta = Var(
+ transBlock.simplex_indices,
+ transBlock.nonzero_simplex_point_indices,
+ bounds=(0, 1),
+ )
+ transBlock.delta_one_constraint = Constraint(
+ # 0 for for us because we are indexing from zero here (12b.1)
+ expr=sum(
+ transBlock.delta[0, j] for j in transBlock.nonzero_simplex_point_indices
+ )
+ <= 1
+ )
+ # Set up the binary y_i variables, which interleave with the delta_i^j in
+ # an odd way
+ transBlock.y_binaries = Var(
+ transBlock.simplex_indices_except_last, domain=Binary
+ )
+
+ # If the delta for the final point in simplex i is not one, y_i must be zero.
+ # That is, y_i is one for and only for simplices that are completely "used"
+ @transBlock.Constraint(transBlock.simplex_indices_except_last)
+ def y_below_delta(m, i):
+ return (
+ transBlock.y_binaries[i]
+ <= transBlock.delta[i, transBlock.last_simplex_point_index]
+ )
+
+ # The sum of the deltas for simplex i+1 should be less than y_i. The overall
+ # effect of these two constraints is that for simplices with y_i=1, the final
+ # delta being one and others zero is enforced. For the first simplex with y_i=0,
+ # the choice of deltas is free except that they must add to one. For following
+ # simplices with y_i=0, all deltas are fixed at zero.
+ @transBlock.Constraint(transBlock.simplex_indices_except_last)
+ def deltas_below_y(m, i):
+ return (
+ sum(
+ transBlock.delta[i + 1, j]
+ for j in transBlock.nonzero_simplex_point_indices
+ )
+ <= transBlock.y_binaries[i]
+ )
+
+ # Now we can relate the deltas and x. x is a sum along differences of points,
+ # weighted by deltas (12a.1)
+ @transBlock.Constraint(transBlock.dimension_indices)
+ def x_constraint(b, n):
+ return pw_expr.args[n] == initial_vertex[n] + sum(
+ # delta_i^j * (v_i^j - v_i^0)
+ transBlock.delta[i, j]
+ * (
+ pw_linear_func._points[simplices[i][j]][n]
+ - pw_linear_func._points[simplices[i][0]][n]
+ )
+ for j in transBlock.nonzero_simplex_point_indices
+ for i in transBlock.simplex_indices
+ )
+
+ # Now we can set the substitute Var for the PWLE (12a.2)
+ transBlock.set_substitute = Constraint(
+ expr=substitute_var
+ == pw_linear_func._linear_functions[0](*initial_vertex)
+ + sum(
+ # delta_i^j * (f(v_i^j) - f(v_i^0))
+ transBlock.delta[i, j]
+ * (
+ pw_linear_func._linear_functions[i](
+ *pw_linear_func._points[simplices[i][j]]
+ )
+ - pw_linear_func._linear_functions[i](
+ *pw_linear_func._points[simplices[i][0]]
+ )
+ )
+ for j in transBlock.nonzero_simplex_point_indices
+ for i in transBlock.simplex_indices
+ )
+ )
+
+ return substitute_var
diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py
new file mode 100644
index 00000000000..588fa8298f6
--- /dev/null
+++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py
@@ -0,0 +1,753 @@
+# ___________________________________________________________________________
+#
+# Pyomo: Python Optimization Modeling Objects
+# Copyright (c) 2008-2024
+# National Technology and Engineering Solutions of Sandia, LLC
+# Under the terms of Contract DE-NA0003525 with National Technology and
+# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
+# rights in this software.
+# This software is distributed under the 3-clause BSD License.
+# ___________________________________________________________________________
+
+from collections import defaultdict
+import enum
+import itertools
+
+import logging
+
+from pyomo.environ import (
+ TransformationFactory,
+ Transformation,
+ Var,
+ Constraint,
+ Objective,
+ Any,
+ value,
+ BooleanVar,
+ Connector,
+ Expression,
+ Suffix,
+ Param,
+ Set,
+ SetOf,
+ RangeSet,
+ Block,
+ ExternalFunction,
+ SortComponents,
+ LogicalConstraint,
+)
+from pyomo.common.autoslots import AutoSlots
+from pyomo.common.collections import ComponentMap, ComponentSet
+from pyomo.common.config import ConfigDict, ConfigValue, PositiveInt, InEnum
+from pyomo.common.dependencies import attempt_import
+from pyomo.common.dependencies import numpy as np
+from pyomo.common.modeling import unique_component_name
+from pyomo.core.expr.numeric_expr import SumExpression
+from pyomo.core.expr import identify_variables
+from pyomo.core.expr import SumExpression
+from pyomo.core.util import target_list
+from pyomo.contrib.piecewise import PiecewiseLinearExpression, PiecewiseLinearFunction
+from pyomo.gdp import Disjunct, Disjunction
+from pyomo.network import Port
+from pyomo.repn.quadratic import QuadraticRepnVisitor
+from pyomo.repn.util import ExprType
+
+
+lineartree, lineartree_available = attempt_import('lineartree')
+sklearn_lm, sklearn_available = attempt_import('sklearn.linear_model')
+
+logger = logging.getLogger(__name__)
+
+
+class DomainPartitioningMethod(enum.IntEnum):
+ RANDOM_GRID = 1
+ UNIFORM_GRID = 2
+ LINEAR_MODEL_TREE_UNIFORM = 3
+ LINEAR_MODEL_TREE_RANDOM = 4
+
+
+class _NonlinearToPWLTransformationData(AutoSlots.Mixin):
+ __slots__ = (
+ 'transformed_component',
+ 'src_component',
+ 'transformed_constraints',
+ 'transformed_objectives',
+ )
+
+ def __init__(self):
+ self.transformed_component = ComponentMap()
+ self.src_component = ComponentMap()
+ self.transformed_constraints = defaultdict(ComponentSet)
+ self.transformed_objectives = defaultdict(ComponentSet)
+
+
+Block.register_private_data_initializer(_NonlinearToPWLTransformationData)
+
+
+def _get_random_point_grid(bounds, n, func, config, seed=42):
+ # Generate randomized grid of points
+ linspaces = []
+ np.random.seed(seed)
+ for (lb, ub), is_integer in bounds:
+ if not is_integer:
+ linspaces.append(np.random.uniform(lb, ub, n))
+ else:
+ size = min(n, ub - lb + 1)
+ linspaces.append(
+ np.random.choice(range(lb, ub + 1), size=size, replace=False)
+ )
+ return list(itertools.product(*linspaces))
+
+
+def _get_uniform_point_grid(bounds, n, func, config):
+ # Generate non-randomized grid of points
+ linspaces = []
+ for (lb, ub), is_integer in bounds:
+ if not is_integer:
+ # Issues happen when exactly using the boundary
+ nudge = (ub - lb) * 1e-4
+ linspaces.append(np.linspace(lb + nudge, ub - nudge, n))
+ else:
+ size = min(n, ub - lb + 1)
+ pts = np.linspace(lb, ub, size)
+ linspaces.append(np.array([round(i) for i in pts]))
+ return list(itertools.product(*linspaces))
+
+
+def _get_points_lmt_random_sample(bounds, n, func, config, seed=42):
+ points = _get_random_point_grid(bounds, n, func, config, seed=seed)
+ return _get_points_lmt(points, bounds, func, config, seed)
+
+
+def _get_points_lmt_uniform_sample(bounds, n, func, config, seed=42):
+ points = _get_uniform_point_grid(bounds, n, func, config)
+ return _get_points_lmt(points, bounds, func, config, seed)
+
+
+def _get_points_lmt(points, bounds, func, config, seed):
+ x_list = np.array(points)
+ y_list = []
+
+ for point in points:
+ y_list.append(func(*point))
+ max_depth = config.linear_tree_max_depth
+ if max_depth is None:
+ # Want the tree to grow with increasing points but not get too large.
+ max_depth = max(4, int(np.log2(len(points) / 4)))
+ regr = lineartree.LinearTreeRegressor(
+ sklearn_lm.LinearRegression(),
+ criterion='mse',
+ max_bins=120,
+ min_samples_leaf=4,
+ max_depth=max_depth,
+ )
+ regr.fit(x_list, y_list)
+
+ leaves, splits, thresholds = _parse_linear_tree_regressor(regr, bounds)
+
+ bound_point_list = _generate_bound_points(leaves, bounds)
+ return bound_point_list
+
+
+_partition_method_dispatcher = {
+ DomainPartitioningMethod.RANDOM_GRID: _get_random_point_grid,
+ DomainPartitioningMethod.UNIFORM_GRID: _get_uniform_point_grid,
+ DomainPartitioningMethod.LINEAR_MODEL_TREE_UNIFORM: _get_points_lmt_uniform_sample,
+ DomainPartitioningMethod.LINEAR_MODEL_TREE_RANDOM: _get_points_lmt_random_sample,
+}
+
+
+def _get_pwl_function_approximation(func, config, bounds):
+ """
+ Get a piecewise-linear approximation of a function, given:
+
+ func: function to approximate
+ config: ConfigDict for transformation, specifying domain_partitioning_method,
+ num_points, and max_depth (if using linear trees)
+ bounds: list of tuples giving upper and lower bounds and a boolean indicating
+ if the variable's domain is discrete or not, for each of func's arguments
+ """
+ method = config.domain_partitioning_method
+ n = config.num_points
+ points = _partition_method_dispatcher[method](bounds, n, func, config)
+
+ # Don't confuse PiecewiseLinearFunction constructor...
+ dim = len(points[0])
+ if dim == 1:
+ points = [pt[0] for pt in points]
+
+ # After getting the points, construct PWLF using the
+ # function-and-list-of-points constructor
+ logger.debug(
+ f"Constructing PWLF with {len(points)} points, each of which "
+ f"are {dim}-dimensional"
+ )
+ return PiecewiseLinearFunction(points=points, function=func)
+
+
+# Given a leaves dict (as generated by parse_tree) and a list of tuples
+# representing variable bounds, generate the set of vertices separating each
+# subset of the domain
+def _generate_bound_points(leaves, bounds):
+ bound_points = []
+ for leaf in leaves.values():
+ lower_corner_list = []
+ upper_corner_list = []
+ for var_bound in leaf['bounds'].values():
+ lower_corner_list.append(var_bound[0])
+ upper_corner_list.append(var_bound[1])
+
+ for pt in [lower_corner_list, upper_corner_list]:
+ for i in range(len(pt)):
+ # clamp within bounds range
+ pt[i] = max(pt[i], bounds[i][0][0])
+ pt[i] = min(pt[i], bounds[i][0][1])
+
+ if tuple(lower_corner_list) not in bound_points:
+ bound_points.append(tuple(lower_corner_list))
+ if tuple(upper_corner_list) not in bound_points:
+ bound_points.append(tuple(upper_corner_list))
+
+ # This process should have gotten every interior bound point. However, all
+ # but two of the corners of the overall bounding box should have been
+ # missed. Let's fix that now.
+ for outer_corner in itertools.product(*[b[0] for b in bounds]):
+ if outer_corner not in bound_points:
+ bound_points.append(outer_corner)
+ return bound_points
+
+
+# Parse a LinearTreeRegressor and identify features such as bounds, slope, and
+# intercept for leaves. Return some dicts.
+def _parse_linear_tree_regressor(linear_tree_regressor, bounds):
+ leaves = linear_tree_regressor.summary(only_leaves=True)
+ splits = linear_tree_regressor.summary()
+
+ for key, leaf in leaves.items():
+ del splits[key]
+ leaf['bounds'] = {}
+ leaf['slope'] = list(leaf['models'].coef_)
+ leaf['intercept'] = leaf['models'].intercept_
+
+ L = np.array(list(leaves.keys()))
+ features = np.arange(0, len(leaves[L[0]]['slope']))
+
+ for node in splits.values():
+ left_child_node = node['children'][0] # find its left child
+ right_child_node = node['children'][1] # find its right child
+ # create the list to save leaves
+ node['left_leaves'], node['right_leaves'] = [], []
+ if left_child_node in leaves: # if left child is a leaf node
+ node['left_leaves'].append(left_child_node)
+ else: # traverse its left node by calling function to find all the
+ # leaves from its left node
+ node['left_leaves'] = _find_leaves(splits, leaves, splits[left_child_node])
+ if right_child_node in leaves: # if right child is a leaf node
+ node['right_leaves'].append(right_child_node)
+ else: # traverse its right node by calling function to find all the
+ # leaves from its right node
+ node['right_leaves'] = _find_leaves(
+ splits, leaves, splits[right_child_node]
+ )
+
+ # For each feature in each leaf, initialize lower and upper bounds to None
+ for th in features:
+ for leaf in leaves:
+ leaves[leaf]['bounds'][th] = [None, None]
+ for split in splits:
+ var = splits[split]['col']
+ for leaf in splits[split]['left_leaves']:
+ leaves[leaf]['bounds'][var][1] = splits[split]['th']
+
+ for leaf in splits[split]['right_leaves']:
+ leaves[leaf]['bounds'][var][0] = splits[split]['th']
+
+ leaves_new = _reassign_none_bounds(leaves, bounds)
+ splitting_thresholds = {}
+ for split in splits:
+ var = splits[split]['col']
+ splitting_thresholds[var] = {}
+ for split in splits:
+ var = splits[split]['col']
+ splitting_thresholds[var][split] = splits[split]['th']
+ # Make sure every nested dictionary in the splitting_thresholds dictionary
+ # is sorted by value
+ for var in splitting_thresholds:
+ splitting_thresholds[var] = dict(
+ sorted(splitting_thresholds[var].items(), key=lambda x: x[1])
+ )
+
+ return leaves_new, splits, splitting_thresholds
+
+
+# This doesn't catch all additively separable expressions--we really need a
+# walker (as does gdp.partition_disjuncts)
+def _additively_decompose_expr(input_expr, min_dimension):
+ dimension = len(list(identify_variables(input_expr)))
+ if input_expr.__class__ is not SumExpression or dimension < min_dimension:
+ # This isn't separable or we don't want to separate it, so we just have
+ # the one expression
+ return [input_expr]
+ # else, it was a SumExpression, and we will break it into the summands
+ return list(input_expr.args)
+
+
+# Populate the "None" bounds with the bounding box bounds for a leaves-dict-tree
+# amalgamation.
+def _reassign_none_bounds(leaves, input_bounds):
+ L = np.array(list(leaves.keys()))
+ features = np.arange(0, len(leaves[L[0]]['slope']))
+
+ for l in L:
+ for f in features:
+ if leaves[l]['bounds'][f][0] == None:
+ leaves[l]['bounds'][f][0] = input_bounds[f][0][0]
+ if leaves[l]['bounds'][f][1] == None:
+ leaves[l]['bounds'][f][1] = input_bounds[f][0][1]
+ return leaves
+
+
+def _find_leaves(splits, leaves, input_node):
+ root_node = input_node
+ leaves_list = []
+ queue = [root_node]
+ while queue:
+ node = queue.pop()
+ node_left = node['children'][0]
+ node_right = node['children'][1]
+ if node_left in leaves:
+ leaves_list.append(node_left)
+ else:
+ queue.append(splits[node_left])
+ if node_right in leaves:
+ leaves_list.append(node_right)
+ else:
+ queue.append(splits[node_right])
+ return leaves_list
+
+
+@TransformationFactory.register(
+ 'contrib.piecewise.nonlinear_to_pwl',
+ doc="Convert nonlinear constraints and objectives to piecewise-linear "
+ "approximations.",
+)
+class NonlinearToPWL(Transformation):
+ """
+ Convert nonlinear constraints and objectives to piecewise-linear approximations.
+ """
+
+ CONFIG = ConfigDict('contrib.piecewise.nonlinear_to_pwl')
+ CONFIG.declare(
+ 'targets',
+ ConfigValue(
+ default=None,
+ domain=target_list,
+ description="target or list of targets that will be approximated",
+ doc="""
+ This specifies the list of components to approximate. If None (default),
+ the entire model is transformed. Note that if the transformation is
+ done out of place, the list of targets should be attached to the model
+ before it is cloned, and the list will specify the targets on the cloned
+ instance.""",
+ ),
+ )
+ CONFIG.declare(
+ 'num_points',
+ ConfigValue(
+ default=3,
+ domain=PositiveInt,
+ description="Number of breakpoints for each piecewise-linear approximation",
+ doc="""
+ Specifies the number of points in each function domain to triangulate in
+ order to construct the piecewise-linear approximation. Must be an integer
+ greater than 1.""",
+ ),
+ )
+ CONFIG.declare(
+ 'domain_partitioning_method',
+ ConfigValue(
+ default=DomainPartitioningMethod.UNIFORM_GRID,
+ domain=InEnum(DomainPartitioningMethod),
+ description="Method for sampling points that will partition function "
+ "domains.",
+ doc="""
+ The method by which the points used to partition each function domain
+ are selected. By default, the range of each variable is partitioned
+ uniformly, however it is possible to sample randomly or to use the
+ partitions from training a linear model tree based on either uniform
+ or random samples of the ranges.""",
+ ),
+ )
+ CONFIG.declare(
+ 'approximate_quadratic_constraints',
+ ConfigValue(
+ default=True,
+ domain=bool,
+ description="Whether or not to approximate quadratic constraints.",
+ doc="""
+ Whether or not to calculate piecewise-linear approximations for
+ quadratic constraints. If True, the resulting approximation will be
+ a mixed-integer linear program. If False, the resulting approximation
+ will be a mixed-integer quadratic program.""",
+ ),
+ )
+ CONFIG.declare(
+ 'approximate_quadratic_objectives',
+ ConfigValue(
+ default=True,
+ domain=bool,
+ description="Whether or not to approximate quadratic objectives.",
+ doc="""
+ Whether or not to calculate piecewise-linear approximations for
+ quadratic objectives. If True, the resulting approximation will be
+ a mixed-integer linear program. If False, the resulting approximation
+ will be a mixed-integer quadratic program.""",
+ ),
+ )
+ CONFIG.declare(
+ 'additively_decompose',
+ ConfigValue(
+ default=False,
+ domain=bool,
+ description="Whether or not to additively decompose constraint expressions "
+ "and approximate the summands separately.",
+ doc="""
+ If False, each nonlinear constraint expression will be approximated by
+ exactly one piecewise-linear function. If True, constraints will be
+ additively decomposed, and each of the resulting summands will be
+ approximated by a separate piecewise-linear function.
+
+ It is recommended to leave this False as long as no nonlinear constraint
+ involves more than about 5-6 variables. For constraints with higher-
+ dimmensional nonlinear functions, additive decomposition will improve
+ the scalability of the approximation (since partitioning the domain is
+ subject to the curse of dimensionality).""",
+ ),
+ )
+ CONFIG.declare(
+ 'max_dimension',
+ ConfigValue(
+ default=5,
+ domain=PositiveInt,
+ description="The maximum dimension of functions that will be approximated.",
+ doc="""
+ Specifies the maximum dimension function the transformation should
+ attempt to approximate. If a nonlinear function dimension exceeds
+ 'max_dimension' the transformation will log a warning and leave the
+ expression as-is. For functions with dimension significantly above the
+ default (5), it is likely that this transformation will stall
+ triangulating the points in order to partition the function domain.""",
+ ),
+ )
+ CONFIG.declare(
+ 'min_dimension_to_additively_decompose',
+ ConfigValue(
+ default=1,
+ domain=PositiveInt,
+ description="The minimum dimension of functions that will be additively "
+ "decomposed.",
+ doc="""
+ Specifies the minimum dimension of a function that the transformation
+ should attempt to additively decompose. If a nonlinear function dimension
+ exceeds 'min_dimension_to_additively_decompose' the transformation will
+ additively decompose. If a the dimension of an expression is less than
+ the 'min_dimension_to_additively_decompose' then it will not be additively
+ decomposed""",
+ ),
+ )
+ CONFIG.declare(
+ 'linear_tree_max_depth',
+ ConfigValue(
+ default=None,
+ domain=PositiveInt,
+ description="Maximum depth for linear tree training, used if using a "
+ "domain partitioning method based on linear model trees.",
+ doc="""
+ Only used if 'domain_partitioning_method' is LINEAR_MODEL_TREE_UNIFORM or
+ LINEAR_MODEL_TREE_RANDOM: Specifies the maximum depth of the linear model
+ trees trained to determine the points to be triangulated to form the
+ domain of the piecewise-linear approximations. If None (the default),
+ the max depth will be given as max(4, ln(num_points / 4)).
+ """,
+ ),
+ )
+
+ def __init__(self):
+ super(Transformation).__init__()
+ self._handlers = {
+ Constraint: self._transform_constraint,
+ Objective: self._transform_objective,
+ Var: False,
+ BooleanVar: False,
+ Connector: False,
+ Expression: False,
+ Suffix: False,
+ Param: False,
+ Set: False,
+ SetOf: False,
+ RangeSet: False,
+ Disjunction: False,
+ Disjunct: self._transform_block_components,
+ Block: self._transform_block_components,
+ ExternalFunction: False,
+ Port: False,
+ PiecewiseLinearFunction: False,
+ LogicalConstraint: False,
+ }
+ self._transformation_blocks = {}
+ self._transformation_block_set = ComponentSet()
+ self._quadratic_repn_visitor = QuadraticRepnVisitor(
+ subexpression_cache={}, var_map={}, var_order={}, sorter=None
+ )
+
+ def _apply_to(self, instance, **kwds):
+ try:
+ self._apply_to_impl(instance, **kwds)
+ finally:
+ self._transformation_blocks.clear()
+ self._transformation_block_set.clear()
+
+ def _apply_to_impl(self, model, **kwds):
+ config = self.CONFIG(kwds.pop('options', {}))
+ config.set_value(kwds)
+
+ targets = config.targets
+ if targets is None:
+ targets = (model,)
+
+ for target in targets:
+ if target.ctype is Block or target.ctype is Disjunct:
+ self._transform_block_components(target, config)
+ elif target.ctype is Constraint:
+ self._transform_constraint(target, config)
+ elif target.ctype is Objective:
+ self._transform_objective(target, config)
+ else:
+ raise ValueError(
+ "Target '%s' is not a Block, Constraint, or Objective. It "
+ "is of type '%s' and cannot be transformed."
+ % (target.name, type(target))
+ )
+
+ def _get_transformation_block(self, parent):
+ if parent in self._transformation_blocks:
+ return self._transformation_blocks[parent]
+
+ nm = unique_component_name(parent, '_pyomo_contrib_nonlinear_to_pwl')
+ self._transformation_blocks[parent] = transBlock = Block()
+ parent.add_component(nm, transBlock)
+ self._transformation_block_set.add(transBlock)
+
+ transBlock._pwl_cons = Constraint(Any)
+ return transBlock
+
+ def _transform_block_components(self, block, config):
+ blocks = block.values() if block.is_indexed() else (block,)
+ for b in blocks:
+ for obj in b.component_objects(
+ active=True, descend_into=False, sort=SortComponents.deterministic
+ ):
+ if obj in self._transformation_block_set:
+ # This is a Block we created--we know we don't need to look
+ # on it.
+ continue
+ handler = self._handlers.get(obj.ctype, None)
+ if not handler:
+ if handler is None:
+ raise RuntimeError(
+ "No transformation handler registered for modeling "
+ "components of type '%s'." % obj.ctype
+ )
+ continue
+ handler(obj, config)
+
+ def _transform_constraint(self, cons, config):
+ trans_block = self._get_transformation_block(cons.parent_block())
+ trans_data_dict = trans_block.private_data()
+ src_data_dict = cons.parent_block().private_data()
+ constraints = cons.values() if cons.is_indexed() else (cons,)
+ for c in constraints:
+ pw_approx, expr_type = self._approximate_expression(
+ c.body, c, trans_block, config, config.approximate_quadratic_constraints
+ )
+
+ if pw_approx is None:
+ # Didn't need approximated, nothing to do
+ continue
+ c.model().private_data().transformed_constraints[expr_type].add(c)
+
+ idx = len(trans_block._pwl_cons)
+ trans_block._pwl_cons[c.name, idx] = (c.lower, pw_approx, c.upper)
+ new_cons = trans_block._pwl_cons[c.name, idx]
+ trans_data_dict.src_component[new_cons] = c
+ src_data_dict.transformed_component[c] = new_cons
+
+ # deactivate original
+ c.deactivate()
+
+ def _transform_objective(self, objective, config):
+ trans_block = self._get_transformation_block(objective.parent_block())
+ trans_data_dict = trans_block.private_data()
+ objectives = objective.values() if objective.is_indexed() else (objective,)
+ src_data_dict = objective.parent_block().private_data()
+ for obj in objectives:
+ pw_approx, expr_type = self._approximate_expression(
+ obj.expr,
+ obj,
+ trans_block,
+ config,
+ config.approximate_quadratic_objectives,
+ )
+
+ if pw_approx is None:
+ # Didn't need approximated, nothing to do
+ continue
+ obj.model().private_data().transformed_objectives[expr_type].add(obj)
+
+ new_obj = Objective(expr=pw_approx, sense=obj.sense)
+ trans_block.add_component(
+ unique_component_name(trans_block, obj.name), new_obj
+ )
+ trans_data_dict.src_component[new_obj] = obj
+ src_data_dict.transformed_component[obj] = new_obj
+
+ obj.deactivate()
+
+ def _get_bounds_list(self, var_list, obj):
+ bounds = []
+ for v in var_list:
+ if None in v.bounds:
+ raise ValueError(
+ "Cannot automatically approximate constraints with unbounded "
+ "variables. Var '%s' appearing in component '%s' is missing "
+ "at least one bound" % (v.name, obj.name)
+ )
+ else:
+ bounds.append((v.bounds, v.is_integer()))
+ return bounds
+
+ def _needs_approximating(self, expr, approximate_quadratic):
+ repn = self._quadratic_repn_visitor.walk_expression(expr)
+ if repn.nonlinear is None:
+ if repn.quadratic is None:
+ # Linear constraint. Always skip.
+ return ExprType.LINEAR, False
+ else:
+ if not approximate_quadratic:
+ # Didn't need approximated, nothing to do
+ return ExprType.QUADRATIC, False
+ return ExprType.QUADRATIC, True
+ return ExprType.GENERAL, True
+
+ def _approximate_expression(
+ self, expr, obj, trans_block, config, approximate_quadratic
+ ):
+ expr_type, needs_approximating = self._needs_approximating(
+ expr, approximate_quadratic
+ )
+ if not needs_approximating:
+ return None, expr_type
+
+ # Additively decompose expr and work on the pieces
+ pwl_summands = []
+ for k, subexpr in enumerate(
+ _additively_decompose_expr(
+ expr, config.min_dimension_to_additively_decompose
+ )
+ if config.additively_decompose
+ else (expr,)
+ ):
+ # First check if this is a good idea
+ expr_vars = list(identify_variables(subexpr, include_fixed=False))
+ orig_values = ComponentMap((v, v.value) for v in expr_vars)
+
+ dim = len(expr_vars)
+ if dim > config.max_dimension:
+ raise ValueError(
+ "Not approximating expression for component '%s' as "
+ "it exceeds the maximum dimension of %s. Try increasing "
+ "'max_dimension' or additively separating the expression."
+ % (obj.name, config.max_dimension)
+ )
+ pwl_summands.append(subexpr)
+ continue
+ elif not self._needs_approximating(subexpr, approximate_quadratic)[1]:
+ pwl_summands.append(subexpr)
+ continue
+ # else we approximate subexpr
+
+ def eval_expr(*args):
+ for i, v in enumerate(expr_vars):
+ v.value = args[i]
+ return value(subexpr)
+
+ pwlf = _get_pwl_function_approximation(
+ eval_expr, config, self._get_bounds_list(expr_vars, obj)
+ )
+ name = unique_component_name(
+ trans_block, obj.getname(fully_qualified=False)
+ )
+ trans_block.add_component(f"_pwle_{name}_{k}", pwlf)
+ # NOTE: We are *not* using += because it will hit the NamedExpression
+ # implementation of iadd and dereference the ExpressionData holding
+ # the PiecewiseLinearExpression that we later transform my remapping
+ # it to a Var...
+ pwl_summands.append(pwlf(*expr_vars))
+
+ # restore var values
+ for v, val in orig_values.items():
+ v.value = val
+
+ return sum(pwl_summands), expr_type
+
+ def get_src_component(self, cons):
+ data = cons.parent_block().private_data().src_component
+ if cons in data:
+ return data[cons]
+ else:
+ raise ValueError(
+ "It does not appear that '%s' is a transformed Constraint "
+ "created by the 'nonlinear_to_pwl' transformation." % cons.name
+ )
+
+ def get_transformed_component(self, cons):
+ data = cons.parent_block().private_data().transformed_component
+ if cons in data:
+ return data[cons]
+ else:
+ raise ValueError(
+ "It does not appear that '%s' is a Constraint that was "
+ "transformed by the 'nonlinear_to_pwl' transformation." % cons.name
+ )
+
+ def get_transformed_nonlinear_constraints(self, model):
+ """
+ Given a model that has been transformed with contrib.piecewise.nonlinear_to_pwl,
+ return the list of general (not quadratic) nonlinear Constraints that were
+ approximated with PiecewiseLinearFunctions
+ """
+ return model.private_data().transformed_constraints[ExprType.GENERAL]
+
+ def get_transformed_quadratic_constraints(self, model):
+ """
+ Given a model that has been transformed with contrib.piecewise.nonlinear_to_pwl,
+ return the list of quadratic Constraints that were approximated with
+ PiecewiseLinearFunctions
+ """
+ return model.private_data().transformed_constraints[ExprType.QUADRATIC]
+
+ def get_transformed_nonlinear_objectives(self, model):
+ """
+ Given a model that has been transformed with contrib.piecewise.nonlinear_to_pwl,
+ return the list of general (not quadratic) nonlinear Constraints that were
+ approximated with PiecewiseLinearFunctions
+ """
+ return model.private_data().transformed_objectives[ExprType.GENERAL]
+
+ def get_transformed_quadratic_objectives(self, model):
+ """
+ Given a model that has been transformed with contrib.piecewise.nonlinear_to_pwl,
+ return the list of quadratic Constraints that were approximated with
+ PiecewiseLinearFunctions
+ """
+ return model.private_data().transformed_objectives[ExprType.QUADRATIC]
diff --git a/pyomo/contrib/piecewise/transform/piecewise_linear_transformation_base.py b/pyomo/contrib/piecewise/transform/piecewise_linear_transformation_base.py
index 7e96891bbc4..6921ff3a29c 100644
--- a/pyomo/contrib/piecewise/transform/piecewise_linear_transformation_base.py
+++ b/pyomo/contrib/piecewise/transform/piecewise_linear_transformation_base.py
@@ -31,6 +31,7 @@
Connector,
SortComponents,
Any,
+ LogicalConstraint,
)
from pyomo.core.base import Transformation
from pyomo.core.base.block import Block
@@ -42,10 +43,10 @@
class PiecewiseLinearTransformationBase(Transformation):
"""
- Base class for transformations of piecewise-linear models to GDPs
+ Base class for transformations of piecewise-linear models to GDPs, MIPs, etc.
"""
- CONFIG = ConfigDict('contrib.piecewise_to_gdp')
+ CONFIG = ConfigDict('contrib.piecewise_linear_transformation_base')
CONFIG.declare(
'targets',
ConfigValue(
@@ -102,6 +103,7 @@ def __init__(self):
ExternalFunction: False,
Port: False,
PiecewiseLinearFunction: self._transform_piecewise_linear_function,
+ LogicalConstraint: False,
}
self._transformation_blocks = {}
diff --git a/pyomo/contrib/piecewise/transform/piecewise_to_mip_visitor.py b/pyomo/contrib/piecewise/transform/piecewise_to_mip_visitor.py
index fae95a564bf..d40bbd8bb34 100644
--- a/pyomo/contrib/piecewise/transform/piecewise_to_mip_visitor.py
+++ b/pyomo/contrib/piecewise/transform/piecewise_to_mip_visitor.py
@@ -50,7 +50,7 @@ def exitNode(self, node, data):
substitute_var = self.transform_pw_linear_expression(
node, parent, self.transBlock
)
- parent._expressions[id(node)] = substitute_var
+ parent._expressions[parent._expression_ids[node]] = substitute_var
return node
finalizeResult = None
diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py
new file mode 100644
index 00000000000..8eb16a87d86
--- /dev/null
+++ b/pyomo/contrib/piecewise/triangulations.py
@@ -0,0 +1,728 @@
+# ___________________________________________________________________________
+#
+# Pyomo: Python Optimization Modeling Objects
+# Copyright (c) 2008-2024
+# National Technology and Engineering Solutions of Sandia, LLC
+# Under the terms of Contract DE-NA0003525 with National Technology and
+# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
+# rights in this software.
+# This software is distributed under the 3-clause BSD License.
+# ___________________________________________________________________________
+
+import itertools
+from enum import Enum
+from pyomo.common.errors import DeveloperError
+from pyomo.common.dependencies import numpy as np
+from pyomo.contrib.piecewise.ordered_3d_j1_triangulation_data import (
+ get_hamiltonian_paths,
+)
+
+
+class Triangulation(Enum):
+ Unknown = 0
+ AssumeValid = 1
+ Delaunay = 2
+ J1 = 3
+ OrderedJ1 = 4
+
+
+# Duck-typed thing that looks reasonably similar to an instance of
+# scipy.spatial.Delaunay
+# Fields:
+# - points: list of P points as P x n array
+# - simplices: list of M simplices as P x (n + 1) array of point _indices_
+# - coplanar: list of N points omitted from triangulation as tuples of (point index,
+# nearest simplex index, nearest vertex index), stacked into an N x 3 array
+class _Triangulation:
+ def __init__(self, points, simplices, coplanar):
+ self.points = points
+ self.simplices = simplices
+ self.coplanar = coplanar
+
+
+# Get an unordered J1 triangulation, as described by [1], of a finite grid of
+# points in R^n having the same odd number of points along each axis.
+# References
+# ----------
+# [1] J.P. Vielma, S. Ahmed, and G. Nemhauser, "Mixed-integer models
+# for nonseparable piecewise-linear optimization: unifying framework
+# and extensions," Operations Research, vol. 58, no. 2, pp. 305-315,
+# 2010.
+def get_unordered_j1_triangulation(points, dimension):
+ points_map, num_pts = _process_points_j1(points, dimension)
+ simplices_list = _get_j1_triangulation(points_map, num_pts - 1, dimension)
+ return _Triangulation(
+ points=np.array(points),
+ simplices=np.array(simplices_list),
+ coplanar=np.array([]),
+ )
+
+
+# Get an ordered J1 triangulation, according to [1], with the additional condition
+# added from [2] that simplex vertices are also ordered such that the final vertex
+# of each simplex is the first vertex of the next simplex.
+# References
+# ----------
+# [1] Michael J. Todd. "Hamiltonian triangulations of Rn". In: Functional
+# Differential Equations and Approximation of Fixed Points. Ed. by
+# Heinz-Otto Peitgen and Hans-Otto Walther. Berlin, Heidelberg: Springer
+# Berlin Heidelberg, 1979, pp. 470–483. ISBN: 978-3-540-35129-0.
+# [2] J.P. Vielma, S. Ahmed, and G. Nemhauser, "Mixed-integer models
+# for nonseparable piecewise-linear optimization: unifying framework
+# and extensions," Operations Research, vol. 58, no. 2, pp. 305-315,
+# 2010.
+def get_ordered_j1_triangulation(points, dimension):
+ points_map, num_pts = _process_points_j1(points, dimension)
+ if dimension == 2:
+ simplices_list = _get_ordered_j1_triangulation_2d(points_map, num_pts - 1)
+ elif dimension == 3:
+ simplices_list = _get_ordered_j1_triangulation_3d(points_map, num_pts - 1)
+ else:
+ simplices_list = _get_ordered_j1_triangulation_4d_and_above(
+ points_map, num_pts - 1, dimension
+ )
+ return _Triangulation(
+ points=np.array(points),
+ simplices=np.array(simplices_list),
+ coplanar=np.array([]),
+ )
+
+
+# Does some validation but mostly assumes the user did the right thing
+def _process_points_j1(points, dimension):
+ if not len(points[0]) == dimension:
+ raise ValueError("Points not consistent with specified dimension")
+ num_pts = round(len(points) ** (1 / dimension))
+ if not len(points) == num_pts**dimension:
+ raise ValueError(
+ "'points' must have points forming an n-dimensional grid with straight grid"
+ " lines and the same odd number of points in each axis."
+ )
+ if not num_pts % 2 == 1:
+ raise ValueError(
+ "'points' must have points forming an n-dimensional grid with straight grid"
+ " lines and the same odd number of points in each axis."
+ )
+
+ # munge the points into an organized map from n-dimensional keys to original
+ # indices
+ points.sort()
+ points_map = {}
+ for point_index in itertools.product(range(num_pts), repeat=dimension):
+ point_flat_index = 0
+ for n in range(dimension):
+ point_flat_index += point_index[dimension - 1 - n] * num_pts**n
+ points_map[point_index] = point_flat_index
+ return points_map, num_pts
+
+
+# Implement the J1 "Union Jack" triangulation (Todd 79) as explained by
+# Vielma 2010, with no ordering guarantees imposed. This function triangulates
+# {0, ..., K}^n for even K using the J1 triangulation, mapping the
+# obtained simplices through the points_map for a slight generalization.
+def _get_j1_triangulation(points_map, K, n):
+ if K % 2 != 0:
+ raise ValueError("K must be even")
+ # 1, 3, ..., K - 1
+ axis_odds = range(1, K, 2)
+ V_0 = itertools.product(axis_odds, repeat=n)
+ big_iterator = itertools.product(
+ V_0,
+ itertools.permutations(range(0, n), n),
+ itertools.product((-1, 1), repeat=n),
+ )
+ ret = []
+ for v_0, pi, s in big_iterator:
+ simplex = []
+ current = list(v_0)
+ simplex.append(points_map[tuple(current)])
+ for i in range(0, n):
+ current[pi[i]] += s[pi[i]]
+ simplex.append(points_map[tuple(current)])
+ # sort this because it might happen again later and we'd like to stay
+ # consistent. Undo this if it's slow.
+ ret.append(sorted(simplex))
+ return ret
+
+
+class Direction(Enum):
+ left = 0
+ down = 1
+ up = 2
+ right = 3
+
+
+# Implement something similar to proof-by-picture from Todd 79 (Figure 1).
+# However, that drawing is misleading at best so I do it in a working way, and
+# also slightly more regularly. I also go from the outside in instead of from
+# the inside out, to make things easier to implement.
+def _get_ordered_j1_triangulation_2d(points_map, num_pts):
+ # check when square has simplices in top-left and bottom-right
+ square_parity_tlbr = lambda x, y: x % 2 == y % 2
+ # check when we are in a "turnaround square" as seen in the picture
+ is_turnaround = lambda x, y: x >= num_pts / 2 and y == (num_pts / 2) - 1
+
+ facing = None
+
+ simplices = []
+ start_square = (num_pts - 1, (num_pts / 2) - 1)
+
+ # make it easier to read what I'm doing
+ def add_bottom_right():
+ simplices.append(
+ (points_map[x, y], points_map[x + 1, y], points_map[x + 1, y + 1])
+ )
+
+ def add_top_right():
+ simplices.append(
+ (points_map[x, y + 1], points_map[x + 1, y], points_map[x + 1, y + 1])
+ )
+
+ def add_bottom_left():
+ simplices.append((points_map[x, y], points_map[x, y + 1], points_map[x + 1, y]))
+
+ def add_top_left():
+ simplices.append(
+ (points_map[x, y], points_map[x, y + 1], points_map[x + 1, y + 1])
+ )
+
+ # identify square by bottom-left corner
+ x, y = start_square
+ used_squares = set() # not used for the turnaround squares
+
+ # depending on parity we will need to go either up or down to start
+ if square_parity_tlbr(x, y):
+ add_bottom_right()
+ facing = Direction.down
+ y -= 1
+ else:
+ add_top_right()
+ facing = Direction.up
+ y += 1
+
+ # state machine
+ while True:
+ if facing == Direction.left:
+ if square_parity_tlbr(x, y):
+ add_bottom_right()
+ add_top_left()
+ else:
+ add_top_right()
+ add_bottom_left()
+ used_squares.add((x, y))
+ if (x - 1, y) in used_squares or x == 0:
+ # can't keep going left so we need to go up or down depending
+ # on parity
+ if square_parity_tlbr(x, y):
+ y += 1
+ facing = Direction.up
+ continue
+ else:
+ y -= 1
+ facing = Direction.down
+ continue
+ else:
+ x -= 1
+ continue
+ elif facing == Direction.right:
+ if is_turnaround(x, y):
+ # finished; this case should always eventually be reached
+ add_bottom_left()
+ _fix_vertices_incremental_order(simplices)
+ return simplices
+ else:
+ if square_parity_tlbr(x, y):
+ add_top_left()
+ add_bottom_right()
+ else:
+ add_bottom_left()
+ add_top_right()
+ used_squares.add((x, y))
+ if (x + 1, y) in used_squares or x == num_pts - 1:
+ # can't keep going right so we need to go up or down depending
+ # on parity
+ if square_parity_tlbr(x, y):
+ y -= 1
+ facing = Direction.down
+ continue
+ else:
+ y += 1
+ facing = Direction.up
+ continue
+ else:
+ x += 1
+ continue
+ elif facing == Direction.down:
+ if is_turnaround(x, y):
+ # we are always in a TLBR square. Take the TL of this, the TR
+ # of the one on the left, and continue upwards one to the left
+ add_top_left()
+ x -= 1
+ add_top_right()
+ y += 1
+ facing = Direction.up
+ continue
+ else:
+ if square_parity_tlbr(x, y):
+ add_top_left()
+ add_bottom_right()
+ else:
+ add_top_right()
+ add_bottom_left()
+ used_squares.add((x, y))
+ if (x, y - 1) in used_squares or y == 0:
+ # can't keep going down so we need to turn depending
+ # on our parity
+ if square_parity_tlbr(x, y):
+ x += 1
+ facing = Direction.right
+ continue
+ else:
+ x -= 1
+ facing = Direction.left
+ continue
+ else:
+ y -= 1
+ continue
+ elif facing == Direction.up:
+ if is_turnaround(x, y):
+ # we are always in a non-TLBR square. Take the BL of this, the BR
+ # of the one on the left, and continue downwards one to the left
+ add_bottom_left()
+ x -= 1
+ add_bottom_right()
+ y -= 1
+ facing = Direction.down
+ continue
+ else:
+ if square_parity_tlbr(x, y):
+ add_bottom_right()
+ add_top_left()
+ else:
+ add_bottom_left()
+ add_top_right()
+ used_squares.add((x, y))
+ if (x, y + 1) in used_squares or y == num_pts - 1:
+ # can't keep going up so we need to turn depending
+ # on our parity
+ if square_parity_tlbr(x, y):
+ x -= 1
+ facing = Direction.left
+ continue
+ else:
+ x += 1
+ facing = Direction.right
+ continue
+ else:
+ y += 1
+ continue
+
+
+def _get_ordered_j1_triangulation_3d(points_map, num_pts):
+ incremental_3d_simplex_pair_to_path = get_hamiltonian_paths()
+ # To start, we need a hamiltonian path in the grid graph of *double* cubes
+ # (2x2x2 cubes)
+ grid_hamiltonian = _get_grid_hamiltonian(3, round(num_pts / 2)) # division is exact
+
+ # We always start by going from [0, 0, 0] to [0, 0, 1], so we can safely
+ # start from the -x side.
+ # Data format: the first tuple is a basis vector or its negative, representing a
+ # face. The number afterwards is a 1 or 2 disambiguating which, of the two simplices
+ # on that face we consider, we are referring to.
+ start_data = ((-1, 0, 0), 1)
+
+ simplices = []
+ for i in range(len(grid_hamiltonian) - 1):
+ current_double_cube_idx = grid_hamiltonian[i]
+ next_double_cube_idx = grid_hamiltonian[i + 1]
+ direction_to_next = tuple(
+ next_double_cube_idx[j] - current_double_cube_idx[j] for j in range(3)
+ )
+
+ current_v_0 = tuple(2 * current_double_cube_idx[j] + 1 for j in range(3))
+
+ current_cube_path = None
+ if (
+ start_data,
+ (direction_to_next, 1),
+ ) in incremental_3d_simplex_pair_to_path.keys():
+ current_cube_path = incremental_3d_simplex_pair_to_path[
+ (start_data, (direction_to_next, 1))
+ ]
+ # set the start data for the next iteration now
+ start_data = (tuple(-1 * i for i in direction_to_next), 1)
+ else:
+ current_cube_path = incremental_3d_simplex_pair_to_path[
+ (start_data, (direction_to_next, 2))
+ ]
+ start_data = (tuple(-1 * i for i in direction_to_next), 2)
+
+ for simplex_data in current_cube_path:
+ simplices.append(
+ _get_one_j1_simplex(
+ current_v_0, simplex_data[1], simplex_data[0], 3, points_map
+ )
+ )
+
+ # fill in the last cube. We have a good start_data but we need to invent a
+ # direction_to_next. Let's go straight in the direction we came from.
+ direction_to_next = tuple(-1 * i for i in start_data[0])
+ current_v_0 = tuple(2 * grid_hamiltonian[-1][j] + 1 for j in range(3))
+ if (
+ start_data,
+ (direction_to_next, 1),
+ ) in incremental_3d_simplex_pair_to_path.keys():
+ current_cube_path = incremental_3d_simplex_pair_to_path[
+ (start_data, (direction_to_next, 1))
+ ]
+ else:
+ current_cube_path = incremental_3d_simplex_pair_to_path[
+ (start_data, (direction_to_next, 2))
+ ]
+
+ for simplex_data in current_cube_path:
+ simplices.append(
+ _get_one_j1_simplex(
+ current_v_0, simplex_data[1], simplex_data[0], 3, points_map
+ )
+ )
+
+ _fix_vertices_incremental_order(simplices)
+ return simplices
+
+
+def _get_ordered_j1_triangulation_4d_and_above(points_map, num_pts, dim):
+ # step one: get a hamiltonian path in the appropriate grid graph (low-coordinate
+ # corners of the grid squares)
+ grid_hamiltonian = _get_grid_hamiltonian(dim, num_pts)
+
+ # step 1.5: get a starting simplex. Anything that is *not* adjacent to the
+ # second square is fine. Since we always go from [0, ..., 0] to [0, ..., 1],
+ # i.e., j=`dim`, anything where `dim` is not the first or last symbol should
+ # always work. Let's stick it in the second place
+ start_perm = tuple([1] + [dim] + list(range(2, dim)))
+
+ # step two: for each square, get a sequence of simplices from a starting simplex,
+ # through the square, and then ending with a simplex adjacent to the next square.
+ # Then find the appropriate adjacent simplex to start on the next square
+ simplices = []
+ for i in range(len(grid_hamiltonian) - 1):
+ current_corner = grid_hamiltonian[i]
+ next_corner = grid_hamiltonian[i + 1]
+ # differing index
+ j = [k + 1 for k in range(dim) if current_corner[k] != next_corner[k]][0]
+ # border x_j value between this square and next
+ c = max(current_corner[j - 1], next_corner[j - 1])
+ v_0, sign = _get_nearest_odd_and_sign_vec(current_corner)
+ # According to Todd, what we need is to end with a permutation where rho(n) = j
+ # if c is odd, and end with one where rho(1) = j if c is even. I think this
+ # is right -- basically the sign from the sign vector sometimes cancels
+ # out the sign from whether we are entering in the +c or -c direction.
+ if c % 2 == 0:
+ perm_sequence = _get_Gn_hamiltonian(dim, start_perm, j, False)
+ for pi in perm_sequence:
+ simplices.append(_get_one_j1_simplex(v_0, pi, sign, dim, points_map))
+ else:
+ perm_sequence = _get_Gn_hamiltonian(dim, start_perm, j, True)
+ for pi in perm_sequence:
+ simplices.append(_get_one_j1_simplex(v_0, pi, sign, dim, points_map))
+ # should be true regardless of odd or even
+ start_perm = perm_sequence[-1]
+
+ # step three: finish out the last square
+ # Any final permutation is fine; we are going nowhere after this
+ v_0, sign = _get_nearest_odd_and_sign_vec(grid_hamiltonian[-1])
+ for pi in _get_Gn_hamiltonian(dim, start_perm, 1, False):
+ simplices.append(_get_one_j1_simplex(v_0, pi, sign, dim, points_map))
+
+ # fix vertices and return
+ _fix_vertices_incremental_order(simplices)
+ return simplices
+
+
+def _get_one_j1_simplex(v_0, pi, sign, dim, points_map):
+ simplex = []
+ current = list(v_0)
+ simplex.append(points_map[tuple(current)])
+ for i in range(0, dim):
+ current[pi[i] - 1] += sign[pi[i] - 1]
+ simplex.append(points_map[tuple(current)])
+ return sorted(simplex)
+
+
+# get the v_0 and sign vectors corresponding to a given square, identified by its
+# low-coordinate corner
+def _get_nearest_odd_and_sign_vec(corner):
+ v_0 = []
+ sign = []
+ for x in corner:
+ if x % 2 == 0:
+ v_0.append(x + 1)
+ sign.append(-1)
+ else:
+ v_0.append(x)
+ sign.append(1)
+ return v_0, sign
+
+
+def _get_grid_hamiltonian(dim, length):
+ if dim == 1:
+ return [[n] for n in range(length)]
+ else:
+ ret = []
+ prev = _get_grid_hamiltonian(dim - 1, length)
+ for n in range(length):
+ # if n is even, add the previous hamiltonian with n in its new first
+ # coordinate. If odd, do the same with the previous hamiltonian in reverse.
+ if n % 2 == 0:
+ for x in prev:
+ ret.append([n] + x)
+ else:
+ for x in reversed(prev):
+ ret.append([n] + x)
+ return ret
+
+
+# Fix vertices (in place) when the simplices are right but vertices are not
+def _fix_vertices_incremental_order(simplices):
+ last_vertex_index = len(simplices[0]) - 1
+ for i, simplex in enumerate(simplices):
+ # Choose vertices like this: first is always the same as last
+ # of the previous simplex. Last is arbitrarily chosen from the
+ # intersection with the next simplex.
+ first = None
+ last = None
+ if i == 0:
+ first = 0
+ else:
+ first = simplex.index(simplices[i - 1][last_vertex_index])
+
+ if i == len(simplices) - 1:
+ last = last_vertex_index
+ else:
+ for n in range(last_vertex_index + 1):
+ if simplex[n] in simplices[i + 1] and n != first:
+ last = n
+ break
+ else:
+ # For the Python neophytes in the audience (and other sane
+ # people), the 'else' only runs if we do *not* break out of the
+ # for loop.
+ raise DeveloperError("Couldn't fix vertex ordering for incremental.")
+
+ # reorder the simplex with the desired first and last
+ new_simplex = list(simplex)
+ temp = new_simplex[0]
+ new_simplex[0] = new_simplex[first]
+ new_simplex[first] = temp
+ if last == 0:
+ last = first
+ temp = new_simplex[last_vertex_index]
+ new_simplex[last_vertex_index] = new_simplex[last]
+ new_simplex[last] = temp
+ simplices[i] = tuple(new_simplex)
+
+
+# Let G_n be the graph on n! vertices where the vertices are permutations in
+# S_n and two vertices are adjacent if they are related by swapping the values
+# of pi(i - 1) and pi(i) for some i in {2, ..., n}.
+#
+# This function gets a Hamiltonian path through G_n, starting from a fixed
+# starting permutation, such that a fixed target symbol is either the image
+# rho(1), or it is rho(n), depending on whether first or last is requested,
+# where rho is the final permutation.
+def _get_Gn_hamiltonian(n, start_permutation, target_symbol, last, _cache={}):
+ if n < 4:
+ raise ValueError("n must be at least 4 for this operation to be possible")
+ if (n, start_permutation, target_symbol, last) in _cache:
+ return _cache[(n, start_permutation, target_symbol, last)]
+ # first is enough because we can just reverse every permutation
+ if last:
+ ret = [
+ tuple(reversed(pi))
+ for pi in _get_Gn_hamiltonian(
+ n, tuple(reversed(start_permutation)), target_symbol, False
+ )
+ ]
+ _cache[(n, start_permutation, target_symbol, last)] = ret
+ return ret
+ # trivial start permutation is enough because we can map it through at the end
+ if start_permutation != tuple(range(1, n + 1)):
+ new_target_symbol = [
+ x for x in range(1, n + 1) if start_permutation[x - 1] == target_symbol
+ ][
+ 0
+ ] # pi^-1(j)
+ ret = [
+ tuple(start_permutation[pi[i] - 1] for i in range(n))
+ for pi in _get_Gn_hamiltonian_impl(n, new_target_symbol)
+ ]
+ _cache[(n, start_permutation, target_symbol, last)] = ret
+ return ret
+ else:
+ ret = _get_Gn_hamiltonian_impl(n, target_symbol)
+ _cache[(n, start_permutation, target_symbol, last)] = ret
+ return ret
+
+
+# Assume the starting permutation is (1, ..., n) and the target symbol needs to
+# be in the first position of the last permutation
+def _get_Gn_hamiltonian_impl(n, target_symbol):
+ # base case: proof by picture from Todd 79, Figure 2
+ # note: Figure 2 contains an error, careful!
+ if n == 4:
+ if target_symbol == 1:
+ return [
+ (1, 2, 3, 4),
+ (2, 1, 3, 4),
+ (2, 1, 4, 3),
+ (2, 4, 1, 3),
+ (4, 2, 1, 3),
+ (4, 2, 3, 1),
+ (2, 4, 3, 1),
+ (2, 3, 4, 1),
+ (2, 3, 1, 4),
+ (3, 2, 1, 4),
+ (3, 2, 4, 1),
+ (3, 4, 2, 1),
+ (4, 3, 2, 1),
+ (4, 3, 1, 2),
+ (3, 4, 1, 2),
+ (3, 1, 4, 2),
+ (3, 1, 2, 4),
+ (1, 3, 2, 4),
+ (1, 3, 4, 2),
+ (1, 4, 3, 2),
+ (4, 1, 3, 2),
+ (4, 1, 2, 3),
+ (1, 4, 2, 3),
+ (1, 2, 4, 3),
+ ]
+ elif target_symbol == 2:
+ return [
+ (1, 2, 3, 4),
+ (1, 2, 4, 3),
+ (1, 4, 2, 3),
+ (4, 1, 2, 3),
+ (4, 1, 3, 2),
+ (1, 4, 3, 2),
+ (1, 3, 4, 2),
+ (1, 3, 2, 4),
+ (3, 1, 2, 4),
+ (3, 1, 4, 2),
+ (3, 4, 1, 2),
+ (4, 3, 1, 2),
+ (4, 3, 2, 1),
+ (3, 4, 2, 1),
+ (3, 2, 4, 1),
+ (3, 2, 1, 4),
+ (2, 3, 1, 4),
+ (2, 3, 4, 1),
+ (2, 4, 3, 1),
+ (4, 2, 3, 1),
+ (4, 2, 1, 3),
+ (2, 4, 1, 3),
+ (2, 1, 4, 3),
+ (2, 1, 3, 4),
+ ]
+ elif target_symbol == 3:
+ return [
+ (1, 2, 3, 4),
+ (1, 2, 4, 3),
+ (1, 4, 2, 3),
+ (4, 1, 2, 3),
+ (4, 1, 3, 2),
+ (1, 4, 3, 2),
+ (1, 3, 4, 2),
+ (1, 3, 2, 4),
+ (3, 1, 2, 4),
+ (3, 1, 4, 2),
+ (3, 4, 1, 2),
+ (4, 3, 1, 2),
+ (4, 3, 2, 1),
+ (3, 4, 2, 1),
+ (3, 2, 4, 1),
+ (2, 3, 4, 1),
+ (2, 4, 3, 1),
+ (4, 2, 3, 1),
+ (4, 2, 1, 3),
+ (2, 4, 1, 3),
+ (2, 1, 4, 3),
+ (2, 1, 3, 4),
+ (2, 3, 1, 4),
+ (3, 2, 1, 4),
+ ]
+ elif target_symbol == 4:
+ return [
+ (1, 2, 3, 4),
+ (2, 1, 3, 4),
+ (2, 3, 1, 4),
+ (3, 2, 1, 4),
+ (3, 1, 2, 4),
+ (1, 3, 2, 4),
+ (1, 3, 4, 2),
+ (3, 1, 4, 2),
+ (3, 4, 1, 2),
+ (3, 4, 2, 1),
+ (3, 2, 4, 1),
+ (2, 3, 4, 1),
+ (2, 4, 3, 1),
+ (2, 4, 1, 3),
+ (2, 1, 4, 3),
+ (1, 2, 4, 3),
+ (1, 4, 2, 3),
+ (1, 4, 3, 2),
+ (4, 1, 3, 2),
+ (4, 3, 1, 2),
+ (4, 3, 2, 1),
+ (4, 2, 3, 1),
+ (4, 2, 1, 3),
+ (4, 1, 2, 3),
+ ]
+ # unreachable
+ else:
+ # recursive case
+ if target_symbol < n: # Less awful case
+ idx = n - 1
+ facing = -1
+ ret = []
+ for pi in _get_Gn_hamiltonian_impl(n - 1, target_symbol):
+ for _ in range(n):
+ l = list(pi)
+ l.insert(idx, n)
+ ret.append(tuple(l))
+ idx += facing
+ if idx == -1 or idx == n: # went too far
+ facing *= -1
+ idx += facing # stay once because we get a new pi
+ return ret
+ else: # awful case, target_symbol = n
+ idx = 0
+ facing = 1
+ ret = []
+ for pi in _get_Gn_hamiltonian_impl(n - 1, n - 1):
+ for _ in range(n):
+ l = [x + 1 for x in pi]
+ l.insert(idx, 1)
+ ret.append(tuple(l))
+ idx += facing
+ if idx == -1 or idx == n: # went too far
+ facing *= -1
+ idx += facing # stay once because we get a new pi
+ # now we almost have a correct sequence, but it ends with (1, n, ...)
+ # instead of (n, 1, ...) so we need to do some surgery
+ last = ret.pop() # of form (1, n, i, j, ...)
+ second_last = ret.pop() # of form (n, 1, i, j, ...)
+ i = last[2]
+ j = last[3]
+ test = list(
+ last
+ ) # want permutation of form (n, 1, j, i, ...) with same tail
+ test[0] = n
+ test[1] = 1
+ test[2] = j
+ test[3] = i
+ idx = ret.index(tuple(test))
+ ret.insert(idx, second_last)
+ ret.insert(idx, last)
+ return ret
diff --git a/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_solver.py b/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_solver.py
index 0af5a772c98..8a35449d94d 100644
--- a/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_solver.py
+++ b/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_solver.py
@@ -46,6 +46,7 @@
# We don't raise unittest.SkipTest if not cyipopt_available as there is a
# test below that tests an exception when cyipopt is unavailable.
cyipopt_ge_1_3 = hasattr(cyipopt, "CyIpoptEvaluationError")
+ ipopt_ge_3_14 = cyipopt.IPOPT_VERSION >= (3, 14, 0)
def create_model1():
@@ -326,3 +327,91 @@ def test_solve_without_objective(self):
res = solver.solve(m, tee=True)
pyo.assert_optimal_termination(res)
self.assertAlmostEqual(m.x[1].value, 9.0)
+
+ def test_solve_13arg_callback(self):
+ m = create_model1()
+
+ iterate_data = []
+
+ def intermediate(
+ nlp,
+ alg_mod,
+ iter_count,
+ obj_value,
+ inf_pr,
+ inf_du,
+ mu,
+ d_norm,
+ regularization_size,
+ alpha_du,
+ alpha_pr,
+ ls_trials,
+ ):
+ x = nlp.get_primals()
+ y = nlp.get_duals()
+ iterate_data.append((x, y))
+
+ x_sol = np.array([3.85958688, 4.67936007, 3.10358931])
+ y_sol = np.array([-1.0, 53.90357665])
+
+ solver = pyo.SolverFactory("cyipopt", intermediate_callback=intermediate)
+ res = solver.solve(m, tee=True)
+ pyo.assert_optimal_termination(res)
+
+ # Make sure iterate vectors have the right shape and that the final
+ # iterate contains the primal solution we expect.
+ for x, y in iterate_data:
+ self.assertEqual(x.shape, (3,))
+ self.assertEqual(y.shape, (2,))
+ x, y = iterate_data[-1]
+ self.assertTrue(np.allclose(x_sol, x))
+ # Note that we can't assert that dual variables in the NLP are those
+ # at the solution because, at this point in the algorithm, the NLP
+ # only has access to the *previous iteration's* dual values.
+
+ # The 13-arg callback works with cyipopt < 1.3, but we will use the
+ # get_current_iterate method, which is only available in 1.3+ and IPOPT 3.14+
+ @unittest.skipIf(
+ not cyipopt_available or not cyipopt_ge_1_3 or not ipopt_ge_3_14,
+ "cyipopt version < 1.3.0",
+ )
+ def test_solve_get_current_iterate(self):
+ m = create_model1()
+
+ iterate_data = []
+
+ def intermediate(
+ nlp,
+ problem,
+ alg_mod,
+ iter_count,
+ obj_value,
+ inf_pr,
+ inf_du,
+ mu,
+ d_norm,
+ regularization_size,
+ alpha_du,
+ alpha_pr,
+ ls_trials,
+ ):
+ iterate = problem.get_current_iterate()
+ x = iterate["x"]
+ y = iterate["mult_g"]
+ iterate_data.append((x, y))
+
+ x_sol = np.array([3.85958688, 4.67936007, 3.10358931])
+ y_sol = np.array([-1.0, 53.90357665])
+
+ solver = pyo.SolverFactory("cyipopt", intermediate_callback=intermediate)
+ res = solver.solve(m, tee=True)
+ pyo.assert_optimal_termination(res)
+
+ # Make sure iterate vectors have the right shape and that the final
+ # iterate contains the primal and dual solution we expect.
+ for x, y in iterate_data:
+ self.assertEqual(x.shape, (3,))
+ self.assertEqual(y.shape, (2,))
+ x, y = iterate_data[-1]
+ self.assertTrue(np.allclose(x_sol, x))
+ self.assertTrue(np.allclose(y_sol, y))
diff --git a/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py b/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py
index 7845a4c189e..98916e11b48 100644
--- a/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py
+++ b/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py
@@ -21,6 +21,7 @@
objects for the matrices (e.g., AmplNLP and PyomoNLP)
"""
import abc
+import inspect
from pyomo.common.dependencies import attempt_import, numpy as np, numpy_available
from pyomo.contrib.pynumero.exceptions import PyNumeroEvaluationError
@@ -309,6 +310,49 @@ def __init__(self, nlp, intermediate_callback=None, halt_on_evaluation_error=Non
# cyipopt.Problem.__init__
super(CyIpoptNLP, self).__init__()
+ # Pre-Pyomo 6.8.0, we had no way to pass the cyipopt.Problem object
+ # to the user in an intermediate callback. This prevented them from calling
+ # the useful get_current_iterate and get_current_violations methods. Now,
+ # we support this by adding the Problem object to the args we pass to a user's
+ # callback. To preserve backwards compatibility, we inspect the user's
+ # callback to infer whether they want this argument. To preserve backwards
+ # compatibility if the user asked for variable-length *args, we do not pass
+ # the Problem object as an argument in this case.
+ # A more maintainable solution may be to force users to accept **kwds if they
+ # want "extra info." If we find ourselves continuing to augment this callback,
+ # this may be worth considering. -RBP
+ self._use_13arg_callback = None
+ if self._intermediate_callback is not None:
+ signature = inspect.signature(self._intermediate_callback)
+ positional_kinds = {
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
+ inspect.Parameter.POSITIONAL_ONLY,
+ }
+ positional = [
+ param
+ for param in signature.parameters.values()
+ if param.kind in positional_kinds
+ ]
+ has_var_args = any(
+ p.kind is inspect.Parameter.VAR_POSITIONAL
+ for p in signature.parameters.values()
+ )
+
+ if len(positional) == 13 and not has_var_args:
+ # If *args is expected, we do not use the new callback
+ # signature.
+ self._use_13arg_callback = True
+ elif len(positional) == 12 or has_var_args:
+ # If *args is expected, we use the old callback signature
+ # for backwards compatibility.
+ self._use_13arg_callback = False
+ else:
+ raise ValueError(
+ "Invalid intermediate callback. A function with either 12 or 13"
+ " positional arguments, or a variable number of arguments, is"
+ " expected."
+ )
+
def _set_primals_if_necessary(self, x):
if not np.array_equal(x, self._cached_x):
self._nlp.set_primals(x)
@@ -436,19 +480,53 @@ def intermediate(
alpha_pr,
ls_trials,
):
+ """Calls user's intermediate callback
+
+ This method has the call signature expected by CyIpopt. We then extend
+ this call signature to provide users of this interface class additional
+ functionality. Additional arguments are:
+
+ - The ``NLP`` object that was used to construct this class instance.
+ This is useful for querying the variables, constraints, and
+ derivatives during the callback.
+ - The class instance itself. This is useful for calling the
+ ``get_current_iterate`` and ``get_current_violations`` methods, which
+ query Ipopt's internal data structures to provide this information.
+
+ """
if self._intermediate_callback is not None:
- return self._intermediate_callback(
- self._nlp,
- alg_mod,
- iter_count,
- obj_value,
- inf_pr,
- inf_du,
- mu,
- d_norm,
- regularization_size,
- alpha_du,
- alpha_pr,
- ls_trials,
- )
+ if self._use_13arg_callback:
+ # This is the callback signature expected as of Pyomo 6.8.0
+ return self._intermediate_callback(
+ self._nlp,
+ self,
+ alg_mod,
+ iter_count,
+ obj_value,
+ inf_pr,
+ inf_du,
+ mu,
+ d_norm,
+ regularization_size,
+ alpha_du,
+ alpha_pr,
+ ls_trials,
+ )
+ else:
+ # This is the callback signature expected pre-Pyomo 6.8.0 and
+ # is supported for backwards compatibility.
+ return self._intermediate_callback(
+ self._nlp,
+ alg_mod,
+ iter_count,
+ obj_value,
+ inf_pr,
+ inf_du,
+ mu,
+ d_norm,
+ regularization_size,
+ alpha_du,
+ alpha_pr,
+ ls_trials,
+ )
return True
diff --git a/pyomo/contrib/pynumero/interfaces/tests/test_cyipopt_interface.py b/pyomo/contrib/pynumero/interfaces/tests/test_cyipopt_interface.py
index bbcd6d4f26d..b8cfa4058bf 100644
--- a/pyomo/contrib/pynumero/interfaces/tests/test_cyipopt_interface.py
+++ b/pyomo/contrib/pynumero/interfaces/tests/test_cyipopt_interface.py
@@ -96,13 +96,17 @@ def hessian(self, x, y, obj_factor):
problem.solve(x0)
-def _get_model_nlp_interface(halt_on_evaluation_error=None):
+def _get_model_nlp_interface(halt_on_evaluation_error=None, intermediate_callback=None):
m = pyo.ConcreteModel()
m.x = pyo.Var([1, 2, 3], initialize=1.0)
m.obj = pyo.Objective(expr=m.x[1] * pyo.sqrt(m.x[2]) + m.x[1] * m.x[3])
m.eq1 = pyo.Constraint(expr=m.x[1] * pyo.sqrt(m.x[2]) == 1.0)
nlp = PyomoNLP(m)
- interface = CyIpoptNLP(nlp, halt_on_evaluation_error=halt_on_evaluation_error)
+ interface = CyIpoptNLP(
+ nlp,
+ halt_on_evaluation_error=halt_on_evaluation_error,
+ intermediate_callback=intermediate_callback,
+ )
bad_primals = np.array([1.0, -2.0, 3.0])
indices = nlp.get_primal_indices([m.x[1], m.x[2], m.x[3]])
bad_primals = bad_primals[indices]
@@ -219,6 +223,64 @@ def test_error_in_hessian_halt(self):
with self.assertRaisesRegex(PyNumeroEvaluationError, msg):
interface.hessian(bad_x, [1.0], 0.0)
+ def test_intermediate_12arg(self):
+ iterate_data = []
+
+ def intermediate(
+ nlp,
+ alg_mod,
+ iter_count,
+ obj_value,
+ inf_pr,
+ inf_du,
+ mu,
+ d_norm,
+ regularization_size,
+ alpha_du,
+ alpha_pr,
+ ls_trials,
+ ):
+ self.assertIsInstance(nlp, PyomoNLP)
+ iterate_data.append((inf_pr, inf_du))
+
+ m, nlp, interface, bad_x = _get_model_nlp_interface(
+ intermediate_callback=intermediate
+ )
+ # The interface's callback is always called with 11 arguments (by CyIpopt/Ipopt)
+ # but we add the NLP object to the arguments.
+ interface.intermediate(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
+ self.assertEqual(iterate_data, [(4, 5)])
+
+ def test_intermediate_13arg(self):
+ iterate_data = []
+
+ def intermediate(
+ nlp,
+ problem,
+ alg_mod,
+ iter_count,
+ obj_value,
+ inf_pr,
+ inf_du,
+ mu,
+ d_norm,
+ regularization_size,
+ alpha_du,
+ alpha_pr,
+ ls_trials,
+ ):
+ self.assertIsInstance(nlp, PyomoNLP)
+ self.assertIsInstance(problem, cyipopt.Problem)
+ iterate_data.append((inf_pr, inf_du))
+
+ m, nlp, interface, bad_x = _get_model_nlp_interface(
+ intermediate_callback=intermediate
+ )
+ # The interface's callback is always called with 11 arguments (by CyIpopt/Ipopt)
+ # but we add the NLP object *and the cyipopt.Problem object* to the arguments.
+ interface.intermediate(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
+ self.assertEqual(iterate_data, [(4, 5)])
+
if __name__ == "__main__":
unittest.main()
diff --git a/pyomo/core/base/initializer.py b/pyomo/core/base/initializer.py
index c87a4236abe..c15e26855ae 100644
--- a/pyomo/core/base/initializer.py
+++ b/pyomo/core/base/initializer.py
@@ -16,6 +16,7 @@
from collections.abc import Sequence
from collections.abc import Mapping
+from pyomo.common.autoslots import AutoSlots
from pyomo.common.dependencies import numpy, numpy_available, pandas, pandas_available
from pyomo.common.modeling import NOTSET
from pyomo.core.pyomoobject import PyomoObject
@@ -37,6 +38,7 @@ def Initializer(
allow_generators=False,
treat_sequences_as_mappings=True,
arg_not_specified=None,
+ additional_args=0,
):
"""Standardized processing of Component keyword arguments
@@ -69,9 +71,54 @@ def Initializer(
If ``arg`` is ``arg_not_specified``, then the function will
return None (and not an InitializerBase object).
+ additional_args: int
+
+ The number of additional arguments that will be passed to any
+ function calls (provided *before* the index value).
+
"""
if arg is arg_not_specified:
return None
+ if additional_args:
+ if arg.__class__ in function_types:
+ if allow_generators or inspect.isgeneratorfunction(arg):
+ raise ValueError(
+ "Generator functions are not allowed when passing additional args"
+ )
+ _args = inspect.getfullargspec(arg)
+ _nargs = len(_args.args)
+ if inspect.ismethod(arg) and arg.__self__ is not None:
+ # Ignore 'self' for bound instance methods and 'cls' for
+ # @classmethods
+ _nargs -= 1
+ if _nargs == 1 + additional_args and _args.varargs is None:
+ return ParameterizedScalarCallInitializer(arg, constant=True)
+ else:
+ return ParameterizedIndexedCallInitializer(arg)
+ else:
+ base_initializer = Initializer(
+ arg=arg,
+ allow_generators=allow_generators,
+ treat_sequences_as_mappings=treat_sequences_as_mappings,
+ arg_not_specified=arg_not_specified,
+ )
+ if type(base_initializer) in (
+ ScalarCallInitializer,
+ IndexedCallInitializer,
+ ):
+ # This is an edge case: if we are providing additional
+ # args, but this is the first time we are seeing a
+ # callable type, we will (potentially) incorrectly
+ # categorize this as an IndexedCallInitializer. Re-try
+ # now that we know this is a function_type.
+ return Initializer(
+ arg=base_initializer._fcn,
+ allow_generators=allow_generators,
+ treat_sequences_as_mappings=treat_sequences_as_mappings,
+ arg_not_specified=arg_not_specified,
+ additional_args=additional_args,
+ )
+ return ParameterizedInitializer(base_initializer)
if arg.__class__ in initializer_map:
return initializer_map[arg.__class__](arg)
if arg.__class__ in sequence_types:
@@ -193,27 +240,13 @@ def Initializer(
return ConstantInitializer(arg)
-class InitializerBase(object):
+class InitializerBase(AutoSlots.Mixin, object):
"""Base class for all Initializer objects"""
__slots__ = ()
verified = False
- def __getstate__(self):
- """Class serializer
-
- This class must declare __getstate__ because it is slotized.
- This implementation should be sufficient for simple derived
- classes (where __slots__ are only declared on the most derived
- class).
- """
- return {k: getattr(self, k) for k in self.__slots__}
-
- def __setstate__(self, state):
- for key, val in state.items():
- object.__setattr__(self, key, val)
-
def constant(self):
"""Return True if this initializer is constant across all indices"""
return False
@@ -316,6 +349,18 @@ def __call__(self, parent, idx):
return self._fcn(parent, idx)
+class ParameterizedIndexedCallInitializer(IndexedCallInitializer):
+ """IndexedCallInitializer that accepts additional arguments"""
+
+ __slots__ = ()
+
+ def __call__(self, parent, idx, *args):
+ if idx.__class__ is tuple:
+ return self._fcn(parent, *args, *idx)
+ else:
+ return self._fcn(parent, *args, idx)
+
+
class CountedCallGenerator(object):
"""Generator implementing the "counted call" initialization scheme
@@ -442,6 +487,15 @@ def constant(self):
return self._constant
+class ParameterizedScalarCallInitializer(ScalarCallInitializer):
+ """ScalarCallInitializer that accepts additional arguments"""
+
+ __slots__ = ()
+
+ def __call__(self, parent, idx, *args):
+ return self._fcn(parent, *args)
+
+
class DefaultInitializer(InitializerBase):
"""Initializer wrapper that maps exceptions to default values.
@@ -485,6 +539,34 @@ def indices(self):
return self._initializer.indices()
+class ParameterizedInitializer(InitializerBase):
+ """Base class for all Initializer objects"""
+
+ __slots__ = ('_base_initializer',)
+
+ def __init__(self, base):
+ self._base_initializer = base
+
+ def constant(self):
+ """Return True if this initializer is constant across all indices"""
+ return self._base_initializer.constant()
+
+ def contains_indices(self):
+ """Return True if this initializer contains embedded indices"""
+ return self._base_initializer.contains_indices()
+
+ def indices(self):
+ """Return a generator over the embedded indices
+
+ This will raise a RuntimeError if this initializer does not
+ contain embedded indices
+ """
+ return self._base_initializer.indices()
+
+ def __call__(self, parent, idx, *args):
+ return self._base_initializer(parent, idx)(parent, *args)
+
+
_bound_sequence_types = collections.defaultdict(None.__class__)
diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py
index e4a6d13e96e..69b21c4d78b 100644
--- a/pyomo/core/base/set.py
+++ b/pyomo/core/base/set.py
@@ -40,10 +40,13 @@
)
from pyomo.core.base.disable_methods import disable_methods
from pyomo.core.base.initializer import (
- InitializerBase,
- Initializer,
CountedCallInitializer,
IndexedCallInitializer,
+ Initializer,
+ InitializerBase,
+ ParameterizedIndexedCallInitializer,
+ ParameterizedInitializer,
+ ParameterizedScalarCallInitializer,
)
from pyomo.core.base.range import (
NumericRange,
@@ -507,6 +510,7 @@ def _tuplize(self, _val, parent, index):
class _NotFound(object):
"Internal type flag used to indicate if an object is not found in a set"
+
pass
@@ -1297,7 +1301,7 @@ def ranges(self):
class FiniteSetData(_FiniteSetMixin, SetData):
"""A general unordered iterable Set"""
- __slots__ = ('_values', '_domain', '_validate', '_filter', '_dimen')
+ __slots__ = ('_values', '_domain', '_dimen')
def __init__(self, component):
SetData.__init__(self, component=component)
@@ -1306,8 +1310,6 @@ def __init__(self, component):
if not hasattr(self, '_values'):
self._values = set()
self._domain = Any
- self._validate = None
- self._filter = None
self._dimen = UnknownSetDimen
def get(self, value, default=None):
@@ -1423,11 +1425,12 @@ def update(self, values):
if self._domain is not Any:
val_iter = self._cb_domain_verifier(self._domain, val_iter)
- if self._filter is not None:
- val_iter = filter(partial(self._filter, self.parent_block()), val_iter)
+ comp = self.parent_component()
+ if comp._filter is not None:
+ val_iter = self._cb_validate_filter('filter', val_iter)
- if self._validate is not None:
- val_iter = self._cb_validate(self._validate, self.parent_block(), val_iter)
+ if comp._validate is not None:
+ val_iter = self._cb_validate_filter('validate', val_iter)
# We wrap this check in a try-except because some values
# (like lists) are not hashable and can raise exceptions.
@@ -1455,22 +1458,83 @@ def _cb_check_set_end(self, val_iter):
return
yield value
- def _cb_validate(self, validate, block, val_iter):
+ def _cb_validate_filter(self, mode, val_iter):
+ fail_false = mode == 'validate'
+ comp = self.parent_component()
+ fcn = getattr(comp, '_' + mode)
+ block = comp.parent_block()
+ idx = self.index()
for value in val_iter:
try:
- flag = validate(block, value)
- except:
- logger.error(
- "Exception raised while validating element '%s' "
- "for Set %s" % (value, self.name)
- )
- raise
- if not flag:
- raise ValueError(
- "The value=%s violates the validation rule of Set %s"
- % (value, self.name)
- )
- yield value
+ flag = fcn(block, idx, value)
+ if flag:
+ yield value
+ continue
+ except Exception as e:
+ flag = None
+ exc = e
+
+ if isinstance(value, tuple):
+ vstar = value
+ else:
+ vstar = (value,)
+
+ # First: try the old format: *values and no index
+ if fcn.__class__ is ParameterizedIndexedCallInitializer:
+ try:
+ flag = fcn(block, (), *vstar)
+ if flag:
+ deprecation_warning(
+ f"{self.__class__.__name__} {self.name}: '{mode}=' "
+ "callback signature matched (block, *value). "
+ "Please update the callback to match the signature "
+ f"(block, value{', *index' if comp.is_indexed() else ''}).",
+ version='6.8.0',
+ )
+ orig_fcn = fcn._fcn
+ fcn = ParameterizedScalarCallInitializer(
+ lambda m, v: orig_fcn(m, *v), True
+ )
+ setattr(comp, '_' + mode, fcn)
+ yield value
+ continue
+ except TypeError:
+ pass
+ except Exception as e:
+ exc = e
+
+ # Now try *values and index
+ try:
+ flag = fcn(block, idx, *value)
+ if flag:
+ deprecation_warning(
+ f"{self.__class__.__name__} {self.name}: '{mode}=' "
+ "callback signature matched (block, *value, *index). "
+ "Please update the callback to match the signature "
+ "(block, value, *index).",
+ version='6.8.0',
+ )
+ if fcn.__class__ is not ParameterizedInitializer:
+ orig_fcn = fcn._fcn
+ fcn._fcn = lambda m, v, *i: orig_fcn(m, *v, *i)
+ yield value
+ continue
+ except TypeError:
+ pass
+ except Exception as e:
+ exc = e
+ if flag is not None:
+ if fail_false:
+ raise ValueError(
+ "The value=%s violates the validation rule of Set %s"
+ % (value, self.name)
+ )
+ continue
+ logger.error(
+ "Exception raised while validating element '%s' "
+ "for Set %s" % (value, self.name)
+ )
+ raise exc from None
def _cb_normalized_dimen_verifier(self, dimen, val_iter):
for value in val_iter:
@@ -2169,8 +2233,8 @@ def __init__(self, *args, **kwds):
allow_generators=True,
)
)
- self._init_validate = Initializer(kwds.pop('validate', None))
- self._init_filter = Initializer(kwds.pop('filter', None))
+ self._validate = Initializer(kwds.pop('validate', None), additional_args=1)
+ self._filter = Initializer(kwds.pop('filter', None), additional_args=1)
if 'virtual' in kwds:
deprecation_warning(
@@ -2290,30 +2354,6 @@ def _getitem_when_not_present(self, index):
obj._domain = domain
if _d is not UnknownSetDimen:
obj._dimen = _d
- if self._init_validate is not None:
- try:
- obj._validate = Initializer(self._init_validate(_block, index))
- if obj._validate.constant():
- # _init_validate was the actual validate function; use it.
- obj._validate = self._init_validate
- except:
- # We will assume any exceptions raised when getting the
- # validator for this index indicate that the function
- # should have been passed directly to the underlying sets.
- obj._validate = self._init_validate
- if self._init_filter is not None:
- try:
- obj._filter = Initializer(self._init_filter(_block, index))
- if obj._filter.constant():
- # _init_filter was the actual filter function; use it.
- obj._filter = self._init_filter
- except:
- # We will assume any exceptions raised when getting the
- # filter for this index indicate that the function
- # should have been passed directly to the underlying sets.
- obj._filter = self._init_filter
- else:
- obj._filter = None
if self._init_values is not None:
# record the user-provided dimen in the initializer
self._init_values._dimen = _d
@@ -2996,8 +3036,8 @@ def __init__(self, *args, **kwds):
)
kwds.pop('finite', None)
self._init_data = (args, kwds.pop('ranges', ()))
- self._init_validate = Initializer(kwds.pop('validate', None))
- self._init_filter = Initializer(kwds.pop('filter', None))
+ self._validate = Initializer(kwds.pop('validate', None), additional_args=1)
+ self._filter = Initializer(kwds.pop('filter', None), additional_args=1)
self._init_bounds = kwds.pop('bounds', None)
if self._init_bounds is not None:
self._init_bounds = BoundsInitializer(self._init_bounds)
@@ -3148,23 +3188,13 @@ def construct(self, data=None):
self._ranges = ranges
- if self._init_filter is not None:
+ if self._filter is not None:
if not self.isfinite():
raise ValueError(
"The 'filter' keyword argument is not valid for "
"non-finite RangeSet component (%s)" % (self.name,)
)
-
- try:
- _filter = Initializer(self._init_filter(_block, None))
- if _filter.constant():
- # _init_filter was the actual filter function; use it.
- _filter = self._init_filter
- except:
- # We will assume any exceptions raised when getting the
- # filter for this index indicate that the function
- # should have been passed directly to the underlying sets.
- _filter = self._init_filter
+ _filter = self._filter
# If this is a finite set, then we can go ahead and filter
# all the ranges. This allows pprint and len to be correct,
@@ -3175,7 +3205,7 @@ def construct(self, data=None):
while old_ranges:
r = old_ranges.pop()
for i, val in enumerate(FiniteRangeSetData._range_gen(r)):
- if not _filter(_block, val):
+ if not _filter(_block, (), val):
split_r = r.range_difference((NumericRange(val, val, 0),))
if len(split_r) == 2:
new_ranges.append(split_r[0])
@@ -3191,38 +3221,25 @@ def construct(self, data=None):
new_ranges.append(r)
self._ranges = new_ranges
- if self._init_validate is not None:
+ if self._validate is not None:
if not self.isfinite():
raise ValueError(
"The 'validate' keyword argument is not valid for "
"non-finite RangeSet component (%s)" % (self.name,)
)
-
try:
- _validate = Initializer(self._init_validate(_block, None))
- if _validate.constant():
- # _init_validate was the actual validate function; use it.
- _validate = self._init_validate
+ for val in self:
+ if not self._validate(_block, None, val):
+ raise ValueError(
+ "The value=%s violates the validation rule of "
+ "Set %s" % (val, self.name)
+ )
except:
- # We will assume any exceptions raised when getting the
- # validator for this index indicate that the function
- # should have been passed directly to the underlying set.
- _validate = self._init_validate
-
- for val in self:
- try:
- flag = _validate(_block, val)
- except:
- logger.error(
- "Exception raised while validating element '%s' "
- "for Set %s" % (val, self.name)
- )
- raise
- if not flag:
- raise ValueError(
- "The value=%s violates the validation rule of "
- "Set %s" % (val, self.name)
- )
+ logger.error(
+ "Exception raised while validating element '%s' "
+ "for Set %s" % (val, self.name)
+ )
+ raise
# Defer the warning about non-constant args until after the
# component has been constructed, so that the conversion of the
diff --git a/pyomo/core/plugins/transform/scaling.py b/pyomo/core/plugins/transform/scaling.py
index 0835e5fd060..d449d479475 100644
--- a/pyomo/core/plugins/transform/scaling.py
+++ b/pyomo/core/plugins/transform/scaling.py
@@ -9,6 +9,7 @@
# This software is distributed under the 3-clause BSD License.
# ___________________________________________________________________________
+import logging
from pyomo.common.collections import ComponentMap
from pyomo.core.base import Block, Var, Constraint, Objective, Suffix, value
from pyomo.core.plugins.transform.hierarchy import Transformation
@@ -17,6 +18,8 @@
from pyomo.core.expr import replace_expressions
from pyomo.util.components import rename_components
+logger = logging.getLogger("pyomo.core.plugins.transform.scaling")
+
@TransformationFactory.register(
'core.scale_model', doc="Scale model variables, constraints, and objectives."
@@ -26,7 +29,7 @@ class ScaleModel(Transformation):
Transformation to scale a model.
This plugin performs variable, constraint, and objective scaling on
- a model based on the scaling factors in the suffix 'scaling_parameter'
+ a model based on the scaling factors in the suffix 'scaling_factor'
set for the variables, constraints, and/or objective. This is typically
done to scale the problem for improved numerical properties.
@@ -35,6 +38,10 @@ class ScaleModel(Transformation):
* :py:meth:`create_using `
* :py:meth:`propagate_solution `
+ By default, scaling components are renamed with the prefix ``scaled_``. To disable
+ this behavior and scale variables in-place (or keep the same names in a new model),
+ use the ``rename=False`` argument to ``apply_to`` or ``create_using``.
+
Examples
--------
@@ -67,8 +74,6 @@ class ScaleModel(Transformation):
>>> print(value(scaled_model.scaled_obj))
101.0
- .. todo:: Implement an option to change the variables names or not
-
"""
def __init__(self, **kwds):
@@ -308,10 +313,18 @@ def propagate_solution(self, scaled_model, original_model):
original_v = original_model.find_component(original_v_path)
for k in scaled_v:
- original_v[k].set_value(
- value(scaled_v[k]) / component_scaling_factor_map[scaled_v[k]],
- skip_validation=True,
- )
+ if scaled_v[k].value is None and original_v[k].value is not None:
+ logger.warning(
+ "Variable with value None in the scaled model is replacing"
+ f" value of variable {original_v[k].name} in the original"
+ f" model with None (was {original_v[k].value})."
+ )
+ original_v[k].set_value(None, skip_validation=True)
+ elif scaled_v[k].value is not None:
+ original_v[k].set_value(
+ value(scaled_v[k]) / component_scaling_factor_map[scaled_v[k]],
+ skip_validation=True,
+ )
if check_reduced_costs and scaled_v[k] in scaled_model.rc:
original_model.rc[original_v[k]] = (
scaled_model.rc[scaled_v[k]]
diff --git a/pyomo/core/tests/transform/test_scaling.py b/pyomo/core/tests/transform/test_scaling.py
index 7168f6bb707..94798535e9c 100644
--- a/pyomo/core/tests/transform/test_scaling.py
+++ b/pyomo/core/tests/transform/test_scaling.py
@@ -10,10 +10,12 @@
# ___________________________________________________________________________
#
+import io
import pyomo.common.unittest as unittest
import pyomo.environ as pyo
from pyomo.opt.base.solvers import UnknownSolver
from pyomo.core.plugins.transform.scaling import ScaleModel, SuffixFinder
+from pyomo.common.log import LoggingIntercept
class TestScaleModelTransformation(unittest.TestCase):
@@ -708,6 +710,31 @@ def test_get_float_scaling_factor_intermediate_level(self):
# v2 should get SF from highest level, ignoring b3 level
self.assertEqual(_finder.find(m.b1.b2.b3.v3), 0.3)
+ def test_propagate_solution_uninitialized_variable(self):
+ m = pyo.ConcreteModel()
+ m.x = pyo.Var([1, 2], initialize=1.0)
+ m.scaling_factor = pyo.Suffix(direction=pyo.Suffix.EXPORT)
+ m.scaling_factor[m.x[1]] = 10.0
+ m.scaling_factor[m.x[2]] = 10.0
+ scaled_model = pyo.TransformationFactory("core.scale_model").create_using(m)
+ scaled_model.scaled_x[1] = 20.0
+ scaled_model.scaled_x[2] = None
+
+ OUTPUT = io.StringIO()
+ with LoggingIntercept(OUTPUT, "pyomo.core.plugins.transform.scaling"):
+ pyo.TransformationFactory("core.scale_model").propagate_solution(
+ scaled_model, m
+ )
+ msg = (
+ "Variable with value None in the scaled model is replacing value of"
+ " variable x[2] in the original model with None (was 1.0).\n"
+ )
+ self.assertEqual(OUTPUT.getvalue(), msg)
+ self.assertAlmostEqual(m.x[1].value, 2.0, delta=1e-8)
+ # Note that value of x[2] in original model *has* been overridden to None.
+ # In this case, a warning has been raised.
+ self.assertIs(m.x[2].value, None)
+
if __name__ == "__main__":
unittest.main()
diff --git a/pyomo/core/tests/unit/test_initializer.py b/pyomo/core/tests/unit/test_initializer.py
index c0f9ddc9565..2b1d44b422f 100644
--- a/pyomo/core/tests/unit/test_initializer.py
+++ b/pyomo/core/tests/unit/test_initializer.py
@@ -27,6 +27,7 @@
from pyomo.core.base.util import flatten_tuple
from pyomo.core.base.initializer import (
Initializer,
+ BoundInitializer,
ConstantInitializer,
ItemInitializer,
ScalarCallInitializer,
@@ -35,6 +36,10 @@
CountedCallGenerator,
DataFrameInitializer,
DefaultInitializer,
+ ParameterizedInitializer,
+ ParameterizedIndexedCallInitializer,
+ ParameterizedScalarCallInitializer,
+ function_types,
)
from pyomo.environ import ConcreteModel, Var
@@ -550,6 +555,54 @@ def _indexed(m, i):
self.assertFalse(a.verified)
self.assertEqual(a(None, 5), 15)
+ def test_function(self):
+ def _scalar(m):
+ return 10
+
+ a = Initializer(_scalar)
+ self.assertIs(type(a), ScalarCallInitializer)
+ self.assertTrue(a.constant())
+ self.assertFalse(a.verified)
+ self.assertEqual(a(None, None), 10)
+
+ def _indexed(m, i):
+ return 10 + i
+
+ a = Initializer(_indexed)
+ self.assertIs(type(a), IndexedCallInitializer)
+ self.assertFalse(a.constant())
+ self.assertFalse(a.verified)
+ self.assertEqual(a(None, 5), 15)
+
+ try:
+ original_fcn_types = set(function_types)
+ function_types.clear()
+ self.assertEqual(len(function_types), 0)
+
+ a = Initializer(_scalar)
+ self.assertIs(type(a), ScalarCallInitializer)
+ self.assertTrue(a.constant())
+ self.assertFalse(a.verified)
+ self.assertEqual(a(None, None), 10)
+ self.assertEqual(len(function_types), 1)
+ finally:
+ function_types.clear()
+ function_types.update(original_fcn_types)
+
+ try:
+ original_fcn_types = set(function_types)
+ function_types.clear()
+ self.assertEqual(len(function_types), 0)
+
+ a = Initializer(_indexed)
+ self.assertIs(type(a), IndexedCallInitializer)
+ self.assertFalse(a.constant())
+ self.assertFalse(a.verified)
+ self.assertEqual(a(None, 5), 15)
+ finally:
+ function_types.clear()
+ function_types.update(original_fcn_types)
+
def test_no_argspec(self):
a = Initializer(getattr)
self.assertIs(type(a), IndexedCallInitializer)
@@ -805,3 +858,87 @@ def test_config_integration(self):
self.assertEqual(a(None, 'opt_1'), 1)
self.assertEqual(a(None, 'opt_3'), 3)
self.assertEqual(a(None, 'opt_5'), 5)
+
+ def _bound_function1(self, m, i):
+ return m, i
+
+ def _bound_function2(self, m, i, j):
+ return m, i, j
+
+ def test_additional_args(self):
+ def a_init(m):
+ yield 0
+ yield 3
+
+ with self.assertRaisesRegex(
+ ValueError,
+ "Generator functions are not allowed when passing additional args",
+ ):
+ a = Initializer(a_init, additional_args=1)
+
+ a = Initializer(self._bound_function1, additional_args=1)
+ self.assertIs(type(a), ParameterizedScalarCallInitializer)
+ self.assertEqual(a('m', None, 5), ('m', 5))
+
+ a = Initializer(self._bound_function2, additional_args=1)
+ self.assertIs(type(a), ParameterizedIndexedCallInitializer)
+ self.assertEqual(a('m', 1, 5), ('m', 5, 1))
+
+ class Functor(object):
+ def __init__(self, i):
+ self.i = i
+
+ def __call__(self, m, i):
+ return m, i * self.i
+
+ a = Initializer(Functor(10), additional_args=1)
+ self.assertIs(type(a), ParameterizedScalarCallInitializer)
+ self.assertEqual(a('m', None, 5), ('m', 50))
+
+ a_init = {1: lambda m, i: ('m', i), 2: lambda m, i: ('m', 2 * i)}
+ a = Initializer(a_init, additional_args=1)
+ self.assertIs(type(a), ParameterizedInitializer)
+ self.assertFalse(a.constant())
+ self.assertTrue(a.contains_indices())
+ self.assertEqual(list(a.indices()), [1, 2])
+ self.assertEqual(a('m', 1, 5), ('m', 5))
+ self.assertEqual(a('m', 2, 5), ('m', 10))
+
+ def test_bound_initializer(self):
+ m = ConcreteModel()
+ m.x = Var([0, 1, 2])
+ m.y = Var()
+
+ b = BoundInitializer(None, m.x)
+ self.assertIsNone(b)
+
+ b = BoundInitializer((0, 1), m.x)
+ self.assertIs(type(b), BoundInitializer)
+ self.assertTrue(b.constant())
+ self.assertFalse(b.verified)
+ self.assertFalse(b.contains_indices())
+ self.assertEqual(b(None, 1), (0, 1))
+
+ b = BoundInitializer([(0, 1)], m.x)
+ self.assertIs(type(b), BoundInitializer)
+ self.assertFalse(b.constant())
+ self.assertFalse(b.verified)
+ self.assertTrue(b.contains_indices())
+ self.assertTrue(list(b.indices()), [0])
+ self.assertEqual(b(None, 0), (0, 1))
+
+ init = {1: (2, 3), 4: (5, 6)}
+ b = BoundInitializer(init, m.x)
+ self.assertIs(type(b), BoundInitializer)
+ self.assertFalse(b.constant())
+ self.assertFalse(b.verified)
+ self.assertTrue(b.contains_indices())
+ self.assertEqual(list(b.indices()), [1, 4])
+ self.assertEqual(b(None, 1), (2, 3))
+ self.assertEqual(b(None, 4), (5, 6))
+
+ b = BoundInitializer((0, 1), m.y)
+ self.assertEqual(b(None, None), (0, 1))
+
+ b = BoundInitializer(5, m.y)
+ self.assertEqual(b(None, None), (5, 5))
diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py
index 8f3fab23bbb..6529c2b60a9 100644
--- a/pyomo/core/tests/unit/test_set.py
+++ b/pyomo/core/tests/unit/test_set.py
@@ -34,6 +34,7 @@
ConstantInitializer,
ItemInitializer,
IndexedCallInitializer,
+ ParameterizedScalarCallInitializer,
)
from pyomo.core.base.set import (
NumericRange as NR,
@@ -4301,8 +4302,7 @@ def _l_tri(model, i, j):
return i >= j
m.K = Set(initialize=RangeSet(3) * RangeSet(3), filter=_l_tri)
- self.assertIsInstance(m.K.filter, IndexedCallInitializer)
- self.assertIs(m.K.filter._fcn, _l_tri)
+ self.assertIsInstance(m.K.filter, ParameterizedScalarCallInitializer)
self.assertEqual(list(m.K), [(1, 1), (2, 1), (2, 2), (3, 1), (3, 2), (3, 3)])
output = StringIO()
@@ -4336,7 +4336,8 @@ def _lt_3(model, i):
m = ConcreteModel()
- def _validate(model, i, j):
+ def _validate(model, val):
+ i, j = val
self.assertIs(model, m)
if i + j < 2:
return True
@@ -4344,44 +4345,185 @@ def _validate(model, i, j):
return False
raise RuntimeError("Bogus value")
- m.I = Set(validate=_validate)
+ m.I1 = Set(validate=_validate)
output = StringIO()
with LoggingIntercept(output, 'pyomo.core'):
- self.assertTrue(m.I.add((0, 1)))
+ self.assertTrue(m.I1.add((0, 1)))
self.assertEqual(output.getvalue(), "")
with self.assertRaisesRegex(
ValueError,
- r"The value=\(4, 1\) violates the validation rule of " r"Set I",
+ r"The value=\(4, 1\) violates the validation rule of " r"Set I1",
):
- m.I.add((4, 1))
+ m.I1.add((4, 1))
self.assertEqual(output.getvalue(), "")
with self.assertRaisesRegex(RuntimeError, "Bogus value"):
- m.I.add((2, 2))
+ m.I1.add((2, 2))
self.assertEqual(
output.getvalue(),
- "Exception raised while validating element '(2, 2)' for Set I\n",
+ "Exception raised while validating element '(2, 2)' for Set I1\n",
)
- # Note: one of these indices will trigger the exception in the
- # validot when it is called for the index.
- m.J = Set([(0, 0), (2, 2)], validate=_validate)
- output = StringIO()
- with LoggingIntercept(output, 'pyomo.core'):
- self.assertTrue(m.J[2, 2].add((0, 1)))
- self.assertEqual(output.getvalue(), "")
+ def _validate(model, i, j):
+ self.assertIs(model, m)
+ if i + j < 2:
+ return True
+ if i - j > 2:
+ return False
+ raise RuntimeError("Bogus value")
+
+ m.I2 = Set(validate=_validate)
+ with LoggingIntercept(module='pyomo.core') as output:
+ self.assertTrue(m.I2.add((0, 1)))
+ self.assertRegex(
+ output.getvalue().replace('\n', ' '),
+ r"DEPRECATED: OrderedScalarSet I2: 'validate=' callback "
+ r"signature matched \(block, \*value\). Please update the "
+ r"callback to match the signature \(block, value\)",
+ )
+ with LoggingIntercept(module='pyomo.core') as output:
with self.assertRaisesRegex(
ValueError,
- r"The value=\(4, 1\) violates the validation rule of " r"Set J\[0,0\]",
+ r"The value=\(4, 1\) violates the validation rule of " r"Set I2",
):
- m.J[0, 0].add((4, 1))
- self.assertEqual(output.getvalue(), "")
+ m.I2.add((4, 1))
+ self.assertEqual(output.getvalue(), "")
+ with LoggingIntercept(module='pyomo.core') as output:
with self.assertRaisesRegex(RuntimeError, "Bogus value"):
- m.J[2, 2].add((2, 2))
+ m.I2.add((2, 2))
self.assertEqual(
output.getvalue(),
- "Exception raised while validating element '(2, 2)' for Set J[2,2]\n",
+ "Exception raised while validating element '(2, 2)' for Set I2\n",
)
+ m.J1 = Set([(0, 0), (2, 2)], validate=_validate)
+ with LoggingIntercept() as OUT:
+ self.assertTrue(m.J1[2, 2].add((0, 1)))
+ self.assertRegex(
+ OUT.getvalue().replace('\n', ' '),
+ r"DEPRECATED: InsertionOrderSetData J1\[2,2\]: 'validate=' callback "
+ r"signature matched \(block, \*value\). Please update the "
+ r"callback to match the signature \(block, value, \*index\)",
+ )
+ with LoggingIntercept() as OUT:
+ with self.assertRaisesRegex(
+ ValueError,
+ r"The value=\(4, 1\) violates the validation rule of " r"Set J1\[0,0\]",
+ ):
+ m.J1[0, 0].add((4, 1))
+ with self.assertRaisesRegex(RuntimeError, "Bogus value"):
+ m.J1[2, 2].add((2, 2))
+ self.assertEqual(
+ OUT.getvalue(),
+ "Exception raised while validating element '(2, 2)' for Set J1[2,2]\n",
+ )
+
+ def _validate(model, i, j, ind1, ind2):
+ self.assertIs(model, m)
+ if i + j < ind1 + ind2:
+ return True
+ if i - j > ind1 + ind2:
+ return False
+ raise RuntimeError("Bogus value")
+
+ m.J2 = Set([(0, 0), (2, 2)], validate=_validate)
+ with LoggingIntercept() as OUT:
+ self.assertTrue(m.J2[2, 2].add((0, 1)))
+ self.assertRegex(
+ OUT.getvalue().replace('\n', ' '),
+ r"DEPRECATED: InsertionOrderSetData J2\[2,2\]: 'validate=' callback "
+ r"signature matched \(block, \*value, \*index\). Please update the "
+ r"callback to match the signature \(block, value, \*index\)",
+ )
+
+ with LoggingIntercept() as OUT:
+ self.assertEqual(OUT.getvalue(), "")
+ with self.assertRaisesRegex(
+ ValueError,
+ r"The value=\(1, 0\) violates the validation rule of Set J2\[0,0\]",
+ ):
+ m.J2[0, 0].add((1, 0))
+ with self.assertRaisesRegex(
+ ValueError,
+ r"The value=\(4, 1\) violates the validation rule of Set J2\[0,0\]",
+ ):
+ m.J2[0, 0].add((4, 1))
+ self.assertEqual(OUT.getvalue(), "")
+ with self.assertRaisesRegex(RuntimeError, "Bogus value"):
+ m.J2[2, 2].add((2, 2))
+ self.assertEqual(
+ OUT.getvalue(),
+ "Exception raised while validating element '(2, 2)' for Set J2[2,2]\n",
+ )
+
+ def _validate(model, v, ind1, ind2):
+ self.assertIs(model, m)
+ i, j = v
+ if i + j < ind1 + ind2:
+ return True
+ if i - j > ind1 + ind2:
+ return False
+ raise RuntimeError("Bogus value")
+
+ m.J3 = Set([(0, 0), (2, 2)], validate=_validate)
+ with LoggingIntercept() as OUT:
+ self.assertTrue(m.J3[2, 2].add((0, 1)))
+ self.assertEqual(OUT.getvalue(), "")
+
+ with LoggingIntercept() as OUT:
+ self.assertEqual(OUT.getvalue(), "")
+ with self.assertRaisesRegex(
+ ValueError,
+ r"The value=\(1, 0\) violates the validation rule of Set J3\[0,0\]",
+ ):
+ m.J3[0, 0].add((1, 0))
+ with self.assertRaisesRegex(
+ ValueError,
+ r"The value=\(4, 1\) violates the validation rule of Set J3\[0,0\]",
+ ):
+ m.J3[0, 0].add((4, 1))
+ self.assertEqual(OUT.getvalue(), "")
+ with self.assertRaisesRegex(RuntimeError, "Bogus value"):
+ m.J3[2, 2].add((2, 2))
+ self.assertEqual(
+ OUT.getvalue(),
+ "Exception raised while validating element '(2, 2)' for Set J3[2,2]\n",
+ )
+
+ # Testing the processing of (deprecated) APIs that raise exceptions
+ def _validate(m, i, j):
+ assert i == 2
+ assert j == 3
+ raise RuntimeError("Bogus value")
+
+ m.K1 = Set([1], dimen=2, validate=_validate)
+ with self.assertRaisesRegex(RuntimeError, "Bogus value"):
+ m.K1[1].add((2, 3))
+
+ # Testing the processing of (deprecated) APIs that raise exceptions
+ def _validate(m, i, j, k):
+ assert i == 2
+ assert j == 3
+ assert k == 1
+ raise RuntimeError("Bogus value")
+
+ m.K2 = Set([1], dimen=2, validate=_validate)
+ with self.assertRaisesRegex(RuntimeError, "Bogus value"):
+ m.K2[1].add((2, 3))
+
+ # Testing passing the validation rule by dict
+ _validate = {1: lambda m, i: i == 10, 2: lambda m, i: i == 20}
+ m.L = Set([1, 2], validate=_validate)
+ m.L[1].add(10)
+ with self.assertRaisesRegex(
+ ValueError, r"The value=20 violates the validation rule of Set L\[1\]"
+ ):
+ m.L[1].add(20)
+ with self.assertRaisesRegex(
+ ValueError, r"The value=10 violates the validation rule of Set L\[2\]"
+ ):
+ m.L[2].add(10)
+ m.L[2].add(20)
+
def test_domain(self):
m = ConcreteModel()
m.I = Set()
@@ -5854,7 +5996,7 @@ def test_filter(self):
output = StringIO()
with LoggingIntercept(output, 'pyomo.core', logging.DEBUG):
- self.assertIsInstance(m.K.filter, IndexedCallInitializer)
+ self.assertIsInstance(m.K.filter, ParameterizedScalarCallInitializer)
self.assertRegex(
output.getvalue(), "^DEPRECATED: 'filter' is no longer a public attribute"
)
diff --git a/pyomo/core/tests/unit/test_sets.py b/pyomo/core/tests/unit/test_sets.py
index 4d305ebab86..52c4523eaba 100644
--- a/pyomo/core/tests/unit/test_sets.py
+++ b/pyomo/core/tests/unit/test_sets.py
@@ -2396,24 +2396,18 @@ def test_dimen1(self):
self.model.A = Set(initialize=[1, 2, 3], dimen=1)
self.instance = self.model.create_instance()
#
- try:
+ with self.assertRaisesRegex(ValueError, ".*Cannot tuplize list data for set"):
self.model.A = Set(initialize=[4, 5, 6], dimen=2)
self.instance = self.model.create_instance()
- except ValueError:
- pass
- else:
- self.fail("test_dimen")
- #
+
self.model.A = Set(initialize=[(1, 2), (2, 3), (3, 4)], dimen=2)
self.instance = self.model.create_instance()
#
- try:
+ with self.assertRaisesRegex(
+ ValueError, ".*has dimension 2 and is not valid for "
+ ):
self.model.A = Set(initialize=[(1, 2), (2, 3), (3, 4)], dimen=1)
self.instance = self.model.create_instance()
- except ValueError:
- pass
- else:
- self.fail("test_dimen")
#
def f(model):
@@ -2422,22 +2416,19 @@ def f(model):
self.model.A = Set(initialize=f, dimen=2)
self.instance = self.model.create_instance()
#
- try:
+ with self.assertRaisesRegex(
+ ValueError, ".*has dimension 2 and is not valid for "
+ ):
self.model.A = Set(initialize=f, dimen=3)
self.instance = self.model.create_instance()
- except ValueError:
- pass
- else:
- self.fail("test_dimen")
def test_dimen2(self):
- try:
+ with self.assertRaisesRegex(
+ ValueError, ".*has dimension 2 and is not valid for "
+ ):
self.model.A = Set(initialize=[1, 2, (3, 4)])
self.instance = self.model.create_instance()
- except ValueError:
- pass
- else:
- self.fail("test_dimen2")
+
self.model.A = Set(dimen=None, initialize=[1, 2, (3, 4)])
self.instance = self.model.create_instance()
@@ -2496,7 +2487,7 @@ def tmp_init(model, z):
self.instance = self.model.create_instance(currdir + "setA.dat")
self.assertEqual(len(self.instance.A), 5)
- def test_within1(self):
+ def test_within_fail(self):
#
# Create Set 'A' data file
#
@@ -2507,14 +2498,10 @@ def test_within1(self):
# Create A with an error
#
self.model.A = Set(within=Integers)
- try:
+ with self.assertRaisesRegex(ValueError, ".*Cannot add value "):
self.instance = self.model.create_instance(currdir + "setA.dat")
- except ValueError:
- pass
- else:
- self.fail("fail test_within1")
- def test_within2(self):
+ def test_within_pass(self):
#
# Create Set 'A' data file
#
@@ -2522,17 +2509,12 @@ def test_within2(self):
OUTPUT.write("data; set A := 1 3 5 7.5; end;")
OUTPUT.close()
#
- # Create A with an error
+ # Create A without an error
#
self.model.A = Set(within=Reals)
- try:
- self.instance = self.model.create_instance(currdir + "setA.dat")
- except ValueError:
- self.fail("fail test_within2")
- else:
- pass
+ self.instance = self.model.create_instance(currdir + "setA.dat")
- def test_validation1(self):
+ def test_validation_fail(self):
#
# Create Set 'A' data file
#
@@ -2543,14 +2525,10 @@ def test_validation1(self):
# Create A with an error
#
self.model.A = Set(validate=lambda model, x: x < 6)
- try:
+ with self.assertRaisesRegex(ValueError, ".*violates the validation rule of"):
self.instance = self.model.create_instance(currdir + "setA.dat")
- except ValueError:
- pass
- else:
- self.fail("fail test_validation1")
- def test_validation2(self):
+ def test_validation_pass(self):
#
# Create Set 'A' data file
#
@@ -2558,35 +2536,22 @@ def test_validation2(self):
OUTPUT.write("data; set A := 1 3 5 5.5; end;")
OUTPUT.close()
#
- # Create A with an error
+ # Create A without an error
#
self.model.A = Set(validate=lambda model, x: x < 6)
- try:
- self.instance = self.model.create_instance(currdir + "setA.dat")
- except ValueError:
- self.fail("fail test_validation2")
- else:
- pass
+ self.instance = self.model.create_instance(currdir + "setA.dat")
def test_other1(self):
self.model.A = Set(
initialize=[1, 2, 3, 'A'], validate=lambda model, x: x in Integers
)
- try:
+ with self.assertRaisesRegex(ValueError, ".*violates the validation rule of"):
self.instance = self.model.create_instance()
- except ValueError:
- pass
- else:
- self.fail("fail test_other1")
def test_other2(self):
self.model.A = Set(initialize=[1, 2, 3, 'A'], within=Integers)
- try:
+ with self.assertRaisesRegex(ValueError, ".*Cannot add value"):
self.instance = self.model.create_instance()
- except ValueError:
- pass
- else:
- self.fail("fail test_other1")
def test_other3(self):
OUTPUT = open(currdir + "setA.dat", "w")
@@ -2601,12 +2566,8 @@ def tmp_init(model):
self.model.n = Param()
self.model.A = Set(initialize=tmp_init, validate=lambda model, x: x in Integers)
- try:
+ with self.assertRaisesRegex(ValueError, ".*violates the validation rule of"):
self.instance = self.model.create_instance(currdir + "setA.dat")
- except ValueError:
- pass
- else:
- self.fail("fail test_other1")
def test_other4(self):
OUTPUT = open(currdir + "setA.dat", "w")
@@ -2621,12 +2582,8 @@ def tmp_init(model):
self.model.n = Param()
self.model.A = Set(initialize=tmp_init, within=Integers)
- try:
+ with self.assertRaisesRegex(ValueError, ".*Cannot add value "):
self.instance = self.model.create_instance(currdir + "setA.dat")
- except ValueError:
- pass
- else:
- self.fail("fail test_other1")
class TestSetArgs2(PyomoModel):
@@ -2666,24 +2623,18 @@ def test_dimen(self):
self.model.Z = Set(initialize=[1, 2])
self.model.A = Set(self.model.Z, initialize=[1, 2, 3], dimen=1)
self.instance = self.model.create_instance()
- try:
+ with self.assertRaisesRegex(ValueError, ".*Cannot tuplize list data for set"):
self.model.A = Set(self.model.Z, initialize=[4, 5, 6], dimen=2)
self.instance = self.model.create_instance()
- except ValueError:
- pass
- else:
- self.fail("test_dimen")
self.model.A = Set(self.model.Z, initialize=[(1, 2), (2, 3), (3, 4)], dimen=2)
self.instance = self.model.create_instance()
- try:
+ with self.assertRaisesRegex(
+ ValueError, ".*has dimension 2 and is not valid for"
+ ):
self.model.A = Set(
self.model.Z, initialize=[(1, 2), (2, 3), (3, 4)], dimen=1
)
self.instance = self.model.create_instance()
- except ValueError:
- pass
- else:
- self.fail("test_dimen")
def test_rule(self):
#
@@ -2753,12 +2704,8 @@ def test_within1(self):
#
self.model.Z = Set()
self.model.A = Set(self.model.Z, within=Integers)
- try:
+ with self.assertRaisesRegex(ValueError, ".*Cannot add value "):
self.instance = self.model.create_instance(currdir + "setA.dat")
- except ValueError:
- pass
- else:
- self.fail("fail test_within1")
def test_within2(self):
#
@@ -2768,16 +2715,11 @@ def test_within2(self):
OUTPUT.write("data; set Z := A C; set A[A] := 1 3 5 7.5; end;")
OUTPUT.close()
#
- # Create A with an error
+ # Create A without an error
#
self.model.Z = Set()
self.model.A = Set(self.model.Z, within=Reals)
- try:
- self.instance = self.model.create_instance(currdir + "setA.dat")
- except ValueError:
- self.fail("fail test_within2")
- else:
- pass
+ self.instance = self.model.create_instance(currdir + "setA.dat")
def test_validation1(self):
#
@@ -2791,12 +2733,8 @@ def test_validation1(self):
#
self.model.Z = Set()
self.model.A = Set(self.model.Z, validate=lambda model, x: x < 6)
- try:
+ with self.assertRaisesRegex(ValueError, ".*violates the validation rule of"):
self.instance = self.model.create_instance(currdir + "setA.dat")
- except ValueError:
- pass
- else:
- self.fail("fail test_within1")
def test_validation2(self):
#
@@ -2806,16 +2744,120 @@ def test_validation2(self):
OUTPUT.write("data; set Z := A C; set A[A] := 1 3 5 5.5; end;")
OUTPUT.close()
#
+ # Create A without an error
+ #
+ self.model.Z = Set()
+ self.model.A = Set(self.model.Z, validate=lambda model, x, i: x < 6)
+ self.instance = self.model.create_instance(currdir + "setA.dat")
+
+ def test_validation3_pass(self):
+ #
+ # Create data file to test a successful validation using indexed sets
+ #
+ OUTPUT = open(currdir + "setsAB.dat", "w")
+ OUTPUT.write(
+ "data; set Z := A C; set A[A] := 1 3 5 5.5; set B[A] := 1 3 5; end;"
+ )
+ OUTPUT.close()
+ #
# Create A with an error
#
self.model.Z = Set()
- self.model.A = Set(self.model.Z, validate=lambda model, x: x < 6)
- try:
- self.instance = self.model.create_instance(currdir + "setA.dat")
- except ValueError:
- self.fail("fail test_within2")
- else:
- pass
+ self.model.A = Set(self.model.Z, validate=lambda model, x, i: x < 6)
+ self.model.B = Set(self.model.Z, validate=lambda model, x, i: x in model.A[i])
+ self.instance = self.model.create_instance(currdir + "setsAB.dat")
+
+ def test_validation3_fail(self):
+ #
+ # Create data file to test a failed validation using indexed sets
+ #
+ OUTPUT = open(currdir + "setsAB.dat", "w")
+ OUTPUT.write(
+ "data; set Z := A C; set A[A] := 1 3 5 5.5; set B[A] := 1 3 5 6; end;"
+ )
+ OUTPUT.close()
+ #
+ # Create A with an error
+ #
+ self.model.Z = Set()
+ self.model.A = Set(self.model.Z, validate=lambda model, x, i: x < 6)
+ self.model.B = Set(self.model.Z, validate=lambda model, x, i: x in model.A[i])
+ with self.assertRaisesRegex(ValueError, ".*violates the validation rule of"):
+ self.instance = self.model.create_instance(currdir + "setsAB.dat")
+
+ def test_validation4_pass(self):
+ #
+ # Test a successful validation using indexed sets and tuple entries
+ #
+ self.model.Z = Set(initialize=['A', 'B'])
+ self.model.A = Set(
+ self.model.Z, dimen=2, initialize={'A': [(1, 2), (3, 4)], 'B': [(5, 6)]}
+ )
+ self.model.B = Set(
+ self.model.Z,
+ dimen=2,
+ initialize={'A': [(1, 2), (3, 4)]},
+ validate=lambda model, x, y, i: (x, y) in model.A[i],
+ )
+ self.instance = self.model.create_instance()
+
+ def test_validation4_fail(self):
+ #
+ # Test a failed validation using indexed sets and tuple entries
+ #
+ self.model.Z = Set(initialize=['A', 'B'])
+ self.model.A = Set(
+ self.model.Z, dimen=2, initialize={'A': [(1, 2), (3, 4)], 'B': [(5, 6)]}
+ )
+ self.model.B = Set(
+ self.model.Z,
+ dimen=2,
+ initialize={'A': [(1, 2), (3, 4), (5, 6)]},
+ validate=lambda model, x, y, i: (x, y) in model.A[i],
+ )
+ with self.assertRaisesRegex(ValueError, ".*violates the validation rule of"):
+ self.instance = self.model.create_instance()
+
+ def test_validation5_pass(self):
+ #
+ # Test a successful validation using indexed sets and tuple entries
+ #
+ self.model.Z = Set(initialize=['A', 'B'])
+ self.model.A = Set(
+ self.model.Z, dimen=2, initialize={'A': [(1, 2), (3, 4)], 'B': [(5, 6)]}
+ )
+
+ def validate_B(m, e1, e2, i):
+ return (e1, e2) in m.A[i]
+
+ self.model.B = Set(
+ self.model.Z,
+ dimen=2,
+ initialize={'A': [(1, 2), (3, 4)]},
+ validate=validate_B,
+ )
+ self.instance = self.model.create_instance()
+
+ def test_validation5_fail(self):
+ #
+ # Test a failed validation using indexed sets and tuple entries
+ #
+ self.model.Z = Set(initialize=['A', 'B'])
+ self.model.A = Set(
+ self.model.Z, dimen=2, initialize={'A': [(1, 2), (3, 4)], 'B': [(5, 6)]}
+ )
+
+ def validate_B(m, e1, e2, i):
+ return (e1, e2) in m.A[i]
+
+ self.model.B = Set(
+ self.model.Z,
+ dimen=2,
+ initialize={'A': [(1, 2), (3, 4), (5, 6)]},
+ validate=validate_B,
+ )
+ with self.assertRaisesRegex(ValueError, ".*violates the validation rule of"):
+ self.instance = self.model.create_instance()
def test_other1(self):
self.model.Z = Set(initialize=['A'])
@@ -2824,24 +2866,16 @@ def test_other1(self):
initialize={'A': [1, 2, 3, 'A']},
validate=lambda model, x: x in Integers,
)
- try:
+ with self.assertRaisesRegex(ValueError, ".*violates the validation rule of"):
self.instance = self.model.create_instance()
- except ValueError:
- pass
- else:
- self.fail("fail test_other1")
def test_other2(self):
self.model.Z = Set(initialize=['A'])
self.model.A = Set(
self.model.Z, initialize={'A': [1, 2, 3, 'A']}, within=Integers
)
- try:
+ with self.assertRaisesRegex(ValueError, ".*Cannot add value "):
self.instance = self.model.create_instance()
- except ValueError:
- pass
- else:
- self.fail("fail test_other1")
def test_other3(self):
def tmp_init(model, i):
@@ -2855,12 +2889,8 @@ def tmp_init(model, i):
self.model.A = Set(
self.model.Z, initialize=tmp_init, validate=lambda model, x: x in Integers
)
- try:
+ with self.assertRaisesRegex(ValueError, ".*violates the validation rule of"):
self.instance = self.model.create_instance()
- except ValueError:
- pass
- else:
- self.fail("fail test_other1")
def test_other4(self):
def tmp_init(model, i):
@@ -2873,12 +2903,8 @@ def tmp_init(model, i):
self.model.Z = Set(initialize=['A'])
self.model.A = Set(self.model.Z, initialize=tmp_init, within=Integers)
self.model.B = Set(self.model.Z, initialize=tmp_init, within=Integers)
- try:
+ with self.assertRaisesRegex(ValueError, ".*Cannot add value "):
self.instance = self.model.create_instance()
- except ValueError:
- pass
- else:
- self.fail("fail test_other1")
class TestMisc(PyomoModel):
diff --git a/pyomo/repn/parameterized_quadratic.py b/pyomo/repn/parameterized_quadratic.py
new file mode 100644
index 00000000000..d818c7c3ed2
--- /dev/null
+++ b/pyomo/repn/parameterized_quadratic.py
@@ -0,0 +1,421 @@
+# ___________________________________________________________________________
+#
+# Pyomo: Python Optimization Modeling Objects
+# Copyright (c) 2008-2024
+# National Technology and Engineering Solutions of Sandia, LLC
+# Under the terms of Contract DE-NA0003525 with National Technology and
+# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
+# rights in this software.
+# This software is distributed under the 3-clause BSD License.
+# ___________________________________________________________________________
+
+from pyomo.common.numeric_types import native_numeric_types
+from pyomo.core.expr.numeric_expr import (
+ DivisionExpression,
+ Expr_ifExpression,
+ mutable_expression,
+ PowExpression,
+ ProductExpression,
+)
+from pyomo.repn.linear import (
+ ExitNodeDispatcher,
+ _handle_division_ANY_constant,
+ _handle_expr_if_const,
+ _handle_pow_ANY_constant,
+ _handle_product_ANY_constant,
+ _handle_product_constant_ANY,
+ _initialize_exit_node_dispatcher,
+)
+from pyomo.repn.parameterized_linear import (
+ define_exit_node_handlers as _param_linear_def_exit_node_handlers,
+ ParameterizedLinearRepnVisitor,
+ to_expression,
+ _handle_division_ANY_pseudo_constant,
+ _merge_dict,
+)
+from pyomo.repn.quadratic import QuadraticRepn, _mul_linear_linear
+from pyomo.repn.util import ExprType
+
+
+_FIXED = ExprType.FIXED
+_CONSTANT = ExprType.CONSTANT
+_LINEAR = ExprType.LINEAR
+_GENERAL = ExprType.GENERAL
+_QUADRATIC = ExprType.QUADRATIC
+
+
+class ParameterizedQuadraticRepn(QuadraticRepn):
+ def __str__(self):
+ return (
+ "ParameterizedQuadraticRepn("
+ f"mult={self.multiplier}, "
+ f"const={self.constant}, "
+ f"linear={self.linear}, "
+ f"quadratic={self.quadratic}, "
+ f"nonlinear={self.nonlinear})"
+ )
+
+ def __repr__(self):
+ return str(self)
+
+ def walker_exitNode(self):
+ if self.nonlinear is not None:
+ return _GENERAL, self
+ elif self.quadratic:
+ return _QUADRATIC, self
+ elif self.linear:
+ return _LINEAR, self
+ elif self.constant.__class__ in native_numeric_types:
+ return _CONSTANT, self.multiplier * self.constant
+ else:
+ return _FIXED, self.multiplier * self.constant
+
+ def to_expression(self, visitor):
+ var_map = visitor.var_map
+ if self.nonlinear is not None:
+ # We want to start with the nonlinear term (and use
+ # assignment) in case the term is a non-numeric node (like a
+ # relational expression)
+ ans = self.nonlinear
+ else:
+ ans = 0
+ if self.quadratic:
+ with mutable_expression() as e:
+ for (x1, x2), coef in self.quadratic.items():
+ if x1 == x2:
+ e += coef * var_map[x1] ** 2
+ else:
+ e += coef * (var_map[x1] * var_map[x2])
+ ans += e
+ if self.linear:
+ var_map = visitor.var_map
+ with mutable_expression() as e:
+ for vid, coef in self.linear.items():
+ if not is_zero(coef):
+ e += coef * var_map[vid]
+ if e.nargs() > 1:
+ ans += e
+ elif e.nargs() == 1:
+ ans += e.arg(0)
+ if not is_zero(self.constant):
+ ans += self.constant
+ if not is_equal_to(self.multiplier, 1):
+ ans *= self.multiplier
+ return ans
+
+ def append(self, other):
+ """Append a child result from acceptChildResult
+
+ Notes
+ -----
+ This method assumes that the operator was "+". It is implemented
+ so that we can directly use a ParameterizedLinearRepn() as a `data` object in
+ the expression walker (thereby allowing us to use the default
+ implementation of acceptChildResult [which calls
+ `data.append()`] and avoid the function call for a custom
+ callback).
+
+ """
+ _type, other = other
+ if _type is _CONSTANT or _type is _FIXED:
+ self.constant += other
+ return
+
+ mult = other.multiplier
+ try:
+ _mult = bool(mult)
+ if not _mult:
+ return
+ if mult == 1:
+ _mult = False
+ except:
+ _mult = True
+
+ const = other.constant
+ try:
+ _const = bool(const)
+ except:
+ _const = True
+
+ if _mult:
+ if _const:
+ self.constant += mult * const
+ if other.linear:
+ _merge_dict(self.linear, mult, other.linear)
+ if other.quadratic:
+ if not self.quadratic:
+ self.quadratic = {}
+ _merge_dict(self.quadratic, mult, other.quadratic)
+ if other.nonlinear is not None:
+ nl = mult * other.nonlinear
+ if self.nonlinear is None:
+ self.nonlinear = nl
+ else:
+ self.nonlinear += nl
+ else:
+ if _const:
+ self.constant += const
+ if other.linear:
+ _merge_dict(self.linear, 1, other.linear)
+ if other.quadratic:
+ if not self.quadratic:
+ self.quadratic = {}
+ _merge_dict(self.quadratic, 1, other.quadratic)
+ if other.nonlinear is not None:
+ nl = other.nonlinear
+ if self.nonlinear is None:
+ self.nonlinear = nl
+ else:
+ self.nonlinear += nl
+
+
+def is_zero(obj):
+ """Return true if expression/constant is zero, False otherwise."""
+ return obj.__class__ in native_numeric_types and not obj
+
+
+def is_zero_product(e1, e2):
+ """
+ Return True if e1 is zero and e2 is not known to be an indeterminate
+ (e.g., NaN, inf), or vice versa, False otherwise.
+ """
+ return (is_zero(e1) and e2 == e2) or (e1 == e1 and is_zero(e2))
+
+
+def is_equal_to(obj, val):
+ return obj.__class__ in native_numeric_types and obj == val
+
+
+def _handle_product_linear_linear(visitor, node, arg1, arg2):
+ _, arg1 = arg1
+ _, arg2 = arg2
+ # Quadratic first, because we will update linear in a minute
+ arg1.quadratic = _mul_linear_linear(
+ visitor.var_order.__getitem__, arg1.linear, arg2.linear
+ )
+ # Linear second, as this relies on knowing the original constants
+ if is_zero(arg2.constant):
+ arg1.linear = {}
+ elif not is_equal_to(arg2.constant, 1):
+ c = arg2.constant
+ for vid, coef in arg1.linear.items():
+ arg1.linear[vid] = c * coef
+ if not is_zero(arg1.constant):
+ # TODO: what if a linear coefficient is indeterminate (nan/inf)?
+ # might that also affect nonlinear product handler?
+ _merge_dict(arg1.linear, arg1.constant, arg2.linear)
+
+ # Finally, the constant and multipliers
+ if is_zero_product(arg1.constant, arg2.constant):
+ arg1.constant = 0
+ else:
+ arg1.constant *= arg2.constant
+
+ arg1.multiplier *= arg2.multiplier
+ return _QUADRATIC, arg1
+
+
+def _handle_product_nonlinear(visitor, node, arg1, arg2):
+ ans = visitor.Result()
+ if not visitor.expand_nonlinear_products:
+ ans.nonlinear = to_expression(visitor, arg1) * to_expression(visitor, arg2)
+ return _GENERAL, ans
+
+ # multiplying (A1 + B1x + C1x^2 + D1(x)) * (A2 + B2x + C2x^2 + D2x))
+ _, x1 = arg1
+ _, x2 = arg2
+ ans.multiplier = x1.multiplier * x2.multiplier
+ x1.multiplier = x2.multiplier = 1
+
+ # constant term [A1A2]
+ if is_zero_product(x1.constant, x2.constant):
+ ans.constant = 0
+ else:
+ ans.constant = x1.constant * x2.constant
+
+ # linear & quadratic terms
+ if not is_zero(x2.constant):
+ # [B1A2], [C1A2]
+ x2_c = x2.constant
+ if is_equal_to(x2_c, 1):
+ ans.linear = dict(x1.linear)
+ if x1.quadratic:
+ ans.quadratic = dict(x1.quadratic)
+ else:
+ ans.linear = {vid: x2_c * coef for vid, coef in x1.linear.items()}
+ if x1.quadratic:
+ ans.quadratic = {k: x2_c * coef for k, coef in x1.quadratic.items()}
+ if not is_zero(x1.constant):
+ # [A1B2]
+ _merge_dict(ans.linear, x1.constant, x2.linear)
+ # [A1C2]
+ if x2.quadratic:
+ if ans.quadratic:
+ _merge_dict(ans.quadratic, x1.constant, x2.quadratic)
+ elif is_equal_to(x1.constant, 1):
+ ans.quadratic = dict(x2.quadratic)
+ else:
+ c = x1.constant
+ ans.quadratic = {k: c * coef for k, coef in x2.quadratic.items()}
+ # [B1B2]
+ if x1.linear and x2.linear:
+ quad = _mul_linear_linear(visitor.var_order.__getitem__, x1.linear, x2.linear)
+ if ans.quadratic:
+ _merge_dict(ans.quadratic, 1, quad)
+ else:
+ ans.quadratic = quad
+
+ # nonlinear portion
+ # [D1A2] + [D1B2] + [D1C2] + [D1D2]
+ ans.nonlinear = 0
+ if x1.nonlinear is not None:
+ ans.nonlinear += x1.nonlinear * x2.to_expression(visitor)
+ x1.nonlinear = None
+ x2.constant = 0
+ x1_c = x1.constant
+ x1.constant = 0
+ x1_lin = x1.linear
+ x1.linear = {}
+ # [C1B2] + [C1C2] + [C1D2]
+ if x1.quadratic:
+ ans.nonlinear += x1.to_expression(visitor) * x2.to_expression(visitor)
+ x1.quadratic = None
+ x2.linear = {}
+ # [B1C2] + [B1D2]
+ if x1_lin and (x2.nonlinear is not None or x2.quadratic):
+ x1.linear = x1_lin
+ ans.nonlinear += x1.to_expression(visitor) * x2.to_expression(visitor)
+ # [A1D2]
+ if not is_zero(x1_c) and x2.nonlinear is not None:
+ # TODO: what if nonlinear contains nan?
+ ans.nonlinear += x1_c * x2.nonlinear
+ return _GENERAL, ans
+
+
+def define_exit_node_handlers(exit_node_handlers=None):
+ if exit_node_handlers is None:
+ exit_node_handlers = {}
+ _param_linear_def_exit_node_handlers(exit_node_handlers)
+
+ exit_node_handlers[ProductExpression].update(
+ {
+ None: _handle_product_nonlinear,
+ (_CONSTANT, _QUADRATIC): _handle_product_constant_ANY,
+ (_QUADRATIC, _CONSTANT): _handle_product_ANY_constant,
+ # Replace handler from the linear walker
+ (_LINEAR, _LINEAR): _handle_product_linear_linear,
+ (_QUADRATIC, _FIXED): _handle_product_ANY_constant,
+ (_FIXED, _QUADRATIC): _handle_product_constant_ANY,
+ }
+ )
+ exit_node_handlers[DivisionExpression].update(
+ {
+ (_QUADRATIC, _CONSTANT): _handle_division_ANY_constant,
+ (_QUADRATIC, _FIXED): _handle_division_ANY_pseudo_constant,
+ }
+ )
+ exit_node_handlers[PowExpression].update(
+ {(_QUADRATIC, _CONSTANT): _handle_pow_ANY_constant}
+ )
+ exit_node_handlers[Expr_ifExpression].update(
+ {
+ (_CONSTANT, i, _QUADRATIC): _handle_expr_if_const
+ for i in (_CONSTANT, _LINEAR, _QUADRATIC, _GENERAL)
+ }
+ )
+ exit_node_handlers[Expr_ifExpression].update(
+ {
+ (_CONSTANT, _QUADRATIC, i): _handle_expr_if_const
+ for i in (_CONSTANT, _LINEAR, _GENERAL)
+ }
+ )
+ return exit_node_handlers
+
+
+class ParameterizedQuadraticRepnVisitor(ParameterizedLinearRepnVisitor):
+ Result = ParameterizedQuadraticRepn
+ exit_node_dispatcher = ExitNodeDispatcher(
+ _initialize_exit_node_dispatcher(define_exit_node_handlers())
+ )
+ max_exponential_expansion = 2
+ expand_nonlinear_products = True
+
+ def _factor_multiplier_into_quadratic_terms(self, ans, mult):
+ linear = ans.linear
+ zeros = []
+ for vid, coef in linear.items():
+ if not is_zero(coef):
+ linear[vid] = mult * coef
+ else:
+ zeros.append(vid)
+ for vid in zeros:
+ del linear[vid]
+
+ quadratic = ans.quadratic
+ if quadratic is not None:
+ quad_zeros = []
+ for vid_pair, coef in ans.quadratic.items():
+ if not is_zero(coef):
+ ans.quadratic[vid_pair] = mult * coef
+ else:
+ quad_zeros.append(vid_pair)
+ for vid_pair in quad_zeros:
+ del quadratic[vid_pair]
+
+ if ans.nonlinear is not None:
+ ans.nonlinear *= mult
+ if not is_zero(ans.constant):
+ ans.constant *= mult
+ ans.multiplier = 1
+
+ def finalizeResult(self, result):
+ ans = result[1]
+ if ans.__class__ is self.Result:
+ mult = ans.multiplier
+ if mult.__class__ not in native_numeric_types:
+ # mult is an expression--we should push it back into the other terms
+ self._factor_multiplier_into_quadratic_terms(ans, mult)
+ return ans
+ if mult == 1:
+ linear_zeros = [
+ (vid, coef) for vid, coef in ans.linear.items() if is_zero(coef)
+ ]
+ for vid, coef in linear_zeros:
+ del ans.linear[vid]
+
+ if ans.quadratic:
+ quadratic_zeros = [
+ (vidpair, coef)
+ for vidpair, coef in ans.quadratic.items()
+ if is_zero(coef)
+ ]
+ for vidpair, coef in quadratic_zeros:
+ del ans.quadratic[vidpair]
+ elif not mult:
+ # the multiplier has cleared out the entire expression.
+ # check if this is suppressing a NaN because we can't
+ # clear everything out if it is
+ has_nan_coefficient = (
+ ans.constant != ans.constant
+ or any(lcoeff != lcoeff for lcoeff in ans.linear.values())
+ or (
+ ans.quadratic is not None
+ and any(qcoeff != qcoeff for qcoeff in ans.quadratic.values())
+ )
+ )
+ if has_nan_coefficient:
+ # There's a nan in here, so we distribute the 0
+ self._factor_multiplier_into_quadratic_terms(ans, mult)
+ return ans
+ return self.Result()
+ else:
+ # mult not in {0, 1}: factor it into the constant,
+ # linear coefficients, quadratic coefficients,
+ # and nonlinear term
+ self._factor_multiplier_into_quadratic_terms(ans, mult)
+ return ans
+
+ ans = self.Result()
+ assert result[0] in (_CONSTANT, _FIXED)
+ ans.constant = result[1]
+ return ans
diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py
index b4b3e018493..2fcb7679df1 100644
--- a/pyomo/repn/plugins/nl_writer.py
+++ b/pyomo/repn/plugins/nl_writer.py
@@ -74,7 +74,7 @@
logger = logging.getLogger(__name__)
-relocated_module_attribute('AMPLRepn', 'pyomo.repn.ampl.AMPLRepn', version='6.7.4.dev0')
+relocated_module_attribute('AMPLRepn', 'pyomo.repn.ampl.AMPLRepn', version='6.8.0')
inf = float('inf')
minus_inf = -inf
diff --git a/pyomo/repn/tests/test_parameterized_quadratic.py b/pyomo/repn/tests/test_parameterized_quadratic.py
new file mode 100644
index 00000000000..38f5f8ec8ad
--- /dev/null
+++ b/pyomo/repn/tests/test_parameterized_quadratic.py
@@ -0,0 +1,1463 @@
+# ___________________________________________________________________________
+#
+# Pyomo: Python Optimization Modeling Objects
+# Copyright (c) 2008-2024
+# National Technology and Engineering Solutions of Sandia, LLC
+# Under the terms of Contract DE-NA0003525 with National Technology and
+# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
+# rights in this software.
+# This software is distributed under the 3-clause BSD License.
+# ___________________________________________________________________________
+
+
+from math import isnan
+import unittest
+
+from pyomo.core.expr import SumExpression, MonomialTermExpression
+from pyomo.core.expr.compare import assertExpressionsEqual
+from pyomo.environ import Any, ConcreteModel, log, Param, Var
+from pyomo.repn.parameterized_quadratic import ParameterizedQuadraticRepnVisitor
+from pyomo.repn.tests.test_linear import VisitorConfig
+from pyomo.repn.util import InvalidNumber
+
+
+def build_test_model():
+ m = ConcreteModel()
+ m.x = Var()
+ m.y = Var()
+ m.z = Var()
+ m.p = Param(initialize=1, mutable=True)
+
+ return m
+
+
+class TestParameterizedQuadratic(unittest.TestCase):
+ def test_constant_literal(self):
+ """
+ Ensure ParameterizedQuadraticRepnVisitor(*args, wrt=[]) works
+ like QuadraticRepnVisitor.
+ """
+ expr = 2
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {})
+ self.assertEqual(cfg.var_order, {})
+ self.assertEqual(repn.multiplier, 1)
+ self.assertEqual(repn.constant, 2)
+ self.assertEqual(repn.linear, {})
+ self.assertIsNone(repn.quadratic)
+ self.assertIsNone(repn.nonlinear)
+ self.assertEqual(repn.to_expression(visitor), 2)
+
+ def test_constant_param(self):
+ m = build_test_model()
+ m.p.set_value(2)
+ expr = 2 + m.p
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {})
+ self.assertEqual(cfg.var_order, {})
+ self.assertEqual(repn.multiplier, 1)
+ assertExpressionsEqual(self, repn.constant, 4)
+ self.assertEqual(repn.linear, {})
+ self.assertIsNone(repn.quadratic)
+ self.assertIsNone(repn.nonlinear)
+ assertExpressionsEqual(self, repn.to_expression(visitor), 4)
+
+ def test_binary_sum_identical_terms(self):
+ m = build_test_model()
+ expr = m.x + m.x
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.y, m.z])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x})
+ self.assertEqual(cfg.var_order, {id(m.x): 0})
+ self.assertEqual(repn.multiplier, 1)
+ self.assertEqual(repn.constant, 0)
+ self.assertEqual(repn.linear, {id(m.x): 2})
+ self.assertIsNone(repn.quadratic)
+ self.assertIsNone(repn.nonlinear)
+ assertExpressionsEqual(self, repn.to_expression(visitor), 2 * m.x)
+
+ def test_binary_sum_identical_terms_wrt_x(self):
+ m = build_test_model()
+ expr = m.x + m.x
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.x])
+ # note: covers walker_exitNode for case where
+ # constant is a fixed expression
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {})
+ self.assertEqual(cfg.var_order, {})
+ self.assertEqual(repn.multiplier, 1)
+ assertExpressionsEqual(self, repn.constant, m.x + m.x)
+ self.assertEqual(repn.linear, {})
+ self.assertIsNone(repn.quadratic)
+ self.assertIsNone(repn.nonlinear)
+ assertExpressionsEqual(self, repn.to_expression(visitor), m.x + m.x)
+
+ def test_binary_sum_nonidentical_terms(self):
+ m = build_test_model()
+ expr = m.x + m.y
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y})
+ self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1})
+ self.assertEqual(repn.multiplier, 1)
+ self.assertEqual(repn.constant, 0)
+ self.assertEqual(repn.linear, {id(m.x): 1, id(m.y): 1})
+ self.assertIsNone(repn.quadratic)
+ self.assertIsNone(repn.nonlinear)
+ assertExpressionsEqual(self, repn.to_expression(visitor), m.x + m.y)
+
+ def test_binary_sum_nonidentical_terms_wrt_x(self):
+ m = build_test_model()
+ expr = m.x + m.y
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.x])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.y): m.y})
+ self.assertEqual(cfg.var_order, {id(m.y): 0})
+ self.assertEqual(repn.multiplier, 1)
+ assertExpressionsEqual(self, repn.constant, m.x)
+ self.assertEqual(repn.linear, {id(m.y): 1})
+ self.assertIsNone(repn.quadratic)
+ self.assertIsNone(repn.nonlinear)
+ assertExpressionsEqual(self, repn.to_expression(visitor), m.y + m.x)
+
+ def test_ternary_sum_with_product(self):
+ m = build_test_model()
+ e = m.x + m.z * m.y + m.z
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[])
+ repn = visitor.walk_expression(e)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.z): m.z, id(m.y): m.y})
+ self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.z): 1, id(m.y): 2})
+ self.assertEqual(repn.multiplier, 1)
+ self.assertEqual(repn.constant, 0)
+ self.assertEqual(len(repn.linear), 2)
+ self.assertEqual(repn.linear[id(m.x)], 1)
+ self.assertEqual(repn.linear[id(m.z)], 1)
+ self.assertEqual(len(repn.quadratic), 1)
+ self.assertEqual(repn.quadratic[(id(m.z), id(m.y))], 1)
+ self.assertIsNone(repn.nonlinear)
+ assertExpressionsEqual(
+ self, repn.to_expression(visitor), m.z * m.y + (m.x + m.z)
+ )
+
+ def test_ternary_sum_with_product_wrt_z(self):
+ m = build_test_model()
+ e = m.x + m.z * m.y + m.z
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.z])
+ repn = visitor.walk_expression(e)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y})
+ self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1})
+ self.assertEqual(repn.multiplier, 1)
+ self.assertIs(repn.constant, m.z)
+ self.assertEqual(len(repn.linear), 2)
+ self.assertEqual(repn.linear[id(m.x)], 1)
+ self.assertIs(repn.linear[id(m.y)], m.z)
+ self.assertIsNone(repn.quadratic)
+ self.assertIsNone(repn.nonlinear)
+ assertExpressionsEqual(self, repn.to_expression(visitor), m.x + m.z * m.y + m.z)
+
+ def test_nonlinear_wrt_x(self):
+ m = build_test_model()
+ expr = log(m.x)
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.x])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {})
+ self.assertEqual(cfg.var_order, {})
+ self.assertEqual(repn.multiplier, 1)
+ assertExpressionsEqual(self, repn.constant, log(m.x))
+ self.assertEqual(repn.linear, {})
+ self.assertIsNone(repn.quadratic)
+ self.assertIsNone(repn.nonlinear)
+ assertExpressionsEqual(self, repn.to_expression(visitor), log(m.x))
+
+ def test_linear_constant_coeffs(self):
+ m = build_test_model()
+ e = 2 + 3 * m.x
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[])
+ visitor.expand_nonlinear_products = True
+ repn = visitor.walk_expression(e)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x})
+ self.assertEqual(cfg.var_order, {id(m.x): 0})
+ self.assertEqual(repn.multiplier, 1)
+ self.assertEqual(repn.constant, 2)
+ self.assertEqual(repn.linear, {id(m.x): 3})
+ self.assertIsNone(repn.quadratic)
+ self.assertIsNone(repn.nonlinear)
+ assertExpressionsEqual(self, repn.to_expression(visitor), 3 * m.x + 2)
+
+ def test_linear_constant_coeffs_wrt_x(self):
+ m = build_test_model()
+ e = 2 + 3 * m.x
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.x])
+ visitor.expand_nonlinear_products = True
+ repn = visitor.walk_expression(e)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {})
+ self.assertEqual(cfg.var_order, {})
+ self.assertEqual(repn.multiplier, 1)
+ assertExpressionsEqual(self, repn.constant, 2 + 3 * m.x)
+ self.assertEqual(repn.linear, {})
+ self.assertIsNone(repn.quadratic)
+ self.assertIsNone(repn.nonlinear)
+ assertExpressionsEqual(self, repn.to_expression(visitor), 2 + 3 * m.x)
+
+ def test_quadratic(self):
+ m = build_test_model()
+ e = 2 + 3 * m.x + 4 * m.x**2
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[])
+ visitor.expand_nonlinear_products = True
+ repn = visitor.walk_expression(e)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x})
+ self.assertEqual(cfg.var_order, {id(m.x): 0})
+ self.assertEqual(repn.multiplier, 1)
+ self.assertEqual(repn.constant, 2)
+ self.assertEqual(repn.linear, {id(m.x): 3})
+ self.assertEqual(repn.quadratic, {(id(m.x), id(m.x)): 4})
+ self.assertIsNone(repn.nonlinear)
+ assertExpressionsEqual(
+ self, repn.to_expression(visitor), 4 * m.x**2 + 3 * m.x + 2
+ )
+
+ def test_product_quadratic_quadratic(self):
+ m = build_test_model()
+ e = (2 + 3 * m.x + 4 * m.x**2) * (5 + 6 * m.x + 7 * m.x**2)
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[])
+ visitor.expand_nonlinear_products = True
+ repn = visitor.walk_expression(e)
+
+ QE4 = SumExpression([4 * m.x**2])
+ QE7 = SumExpression([7 * m.x**2])
+ LE3 = MonomialTermExpression((3, m.x))
+ LE6 = MonomialTermExpression((6, m.x))
+ NL = +QE4 * (QE7 + LE6) + (LE3) * (QE7)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x})
+ self.assertEqual(cfg.var_order, {id(m.x): 0})
+ self.assertEqual(repn.multiplier, 1)
+ self.assertEqual(repn.constant, 10)
+ self.assertEqual(repn.linear, {id(m.x): 27})
+ self.assertEqual(repn.quadratic, {(id(m.x), id(m.x)): 52})
+ assertExpressionsEqual(self, repn.nonlinear, NL)
+ assertExpressionsEqual(
+ self, repn.to_expression(visitor), NL + 52 * m.x**2 + 27 * m.x + 10
+ )
+
+ def test_product_quadratic_quadratic_2(self):
+ m = build_test_model()
+ e = (2 + 3 * m.x + 4 * m.x**2) * (5 + 6 * m.x + 7 * m.x**2)
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[])
+ visitor.expand_nonlinear_products = False
+ repn = visitor.walk_expression(e)
+
+ NL = (4 * m.x**2 + 3 * m.x + 2) * (7 * m.x**2 + 6 * m.x + 5)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x})
+ self.assertEqual(cfg.var_order, {id(m.x): 0})
+ self.assertEqual(repn.multiplier, 1)
+ self.assertEqual(repn.constant, 0)
+ self.assertEqual(repn.linear, {})
+ self.assertIsNone(repn.quadratic)
+ assertExpressionsEqual(self, repn.nonlinear, NL)
+ assertExpressionsEqual(self, repn.to_expression(visitor), NL)
+
+ def test_product_linear_linear(self):
+ m = build_test_model()
+ e = (1 + 2 * m.x + 3 * m.y) * (4 + 5 * m.x + 6 * m.y)
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[])
+ repn = visitor.walk_expression(e)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y})
+ self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1})
+ self.assertEqual(repn.multiplier, 1)
+ self.assertEqual(repn.constant, 4)
+ self.assertEqual(repn.linear, {id(m.x): 13, id(m.y): 18})
+ self.assertEqual(
+ repn.quadratic,
+ {(id(m.x), id(m.x)): 10, (id(m.y), id(m.y)): 18, (id(m.x), id(m.y)): 27},
+ )
+ self.assertIsNone(repn.nonlinear)
+ assertExpressionsEqual(
+ self,
+ repn.to_expression(visitor),
+ (10 * m.x**2 + 27 * (m.x * m.y) + 18 * m.y**2 + (13 * m.x + 18 * m.y) + 4),
+ )
+
+ def test_product_linear_linear_wrt_y(self):
+ m = build_test_model()
+ e = (1 + 2 * m.x + 3 * m.y) * (4 + 5 * m.x + 6 * m.y)
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.y, m.z])
+ repn = visitor.walk_expression(e)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x})
+ self.assertEqual(cfg.var_order, {id(m.x): 0})
+ self.assertEqual(repn.multiplier, 1)
+ assertExpressionsEqual(self, repn.constant, (1 + 3 * m.y) * (4 + 6 * m.y))
+ self.assertEqual(len(repn.linear), 1)
+ assertExpressionsEqual(
+ self, repn.linear[id(m.x)], (4 + 6 * m.y) * 2 + (1 + 3 * m.y) * 5
+ )
+ self.assertEqual(repn.quadratic, {(id(m.x), id(m.x)): 10})
+ self.assertIsNone(repn.nonlinear)
+ assertExpressionsEqual(
+ self,
+ repn.to_expression(visitor),
+ (
+ 10 * m.x**2
+ + ((4 + 6 * m.y) * 2 + (1 + 3 * m.y) * 5) * m.x
+ + (1 + 3 * m.y) * (4 + 6 * m.y)
+ ),
+ )
+
+ def test_product_linear_linear_const_0(self):
+ m = build_test_model()
+ expr = (0 + 3 * m.x + 4 * m.y) * (5 + 3 * m.x + 7 * m.y)
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y})
+ self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1})
+ self.assertEqual(repn.multiplier, 1)
+ self.assertEqual(repn.constant, 0)
+ self.assertEqual(repn.linear, {id(m.x): 15, id(m.y): 20})
+ self.assertEqual(
+ repn.quadratic,
+ {(id(m.x), id(m.x)): 9, (id(m.x), id(m.y)): 33, (id(m.y), id(m.y)): 28},
+ )
+ self.assertIsNone(repn.nonlinear)
+ assertExpressionsEqual(
+ self,
+ repn.to_expression(visitor),
+ 9 * m.x**2 + 33 * (m.x * m.y) + 28 * m.y**2 + (15 * m.x + 20 * m.y),
+ )
+
+ def test_product_linear_quadratic(self):
+ m = build_test_model()
+ expr = (5 + 3 * m.x + 7 * m.y) * (1 + 3 * m.x + 4 * m.y + 8 * m.y * m.x)
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y})
+ self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1})
+ self.assertEqual(repn.multiplier, 1)
+ self.assertEqual(repn.constant, 5)
+ self.assertEqual(repn.linear, {id(m.x): 18, id(m.y): 27})
+ self.assertEqual(
+ repn.quadratic,
+ {(id(m.x), id(m.y)): 73, (id(m.x), id(m.x)): 9, (id(m.y), id(m.y)): 28},
+ )
+ assertExpressionsEqual(
+ self, repn.nonlinear, (3 * m.x + 7 * m.y) * SumExpression([8 * (m.x * m.y)])
+ )
+ assertExpressionsEqual(
+ self,
+ repn.to_expression(visitor),
+ (
+ 73 * (m.x * m.y)
+ + 9 * m.x**2
+ + 28 * m.y**2
+ + (3 * m.x + 7 * m.y) * SumExpression([8 * (m.x * m.y)])
+ + (18 * m.x + 27 * m.y)
+ + 5
+ ),
+ )
+
+ def test_product_linear_quadratic_wrt_x(self):
+ m = build_test_model()
+ expr = (0 + 3 * m.x + 4 * m.y + 8 * m.y * m.x) * (5 + 3 * m.x + 7 * m.y)
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.x])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.y): m.y})
+ self.assertEqual(cfg.var_order, {id(m.y): 0})
+ self.assertEqual(repn.multiplier, 1)
+ assertExpressionsEqual(self, repn.constant, 3 * m.x * (5 + 3 * m.x))
+ self.assertEqual(len(repn.linear), 1)
+ assertExpressionsEqual(
+ self, repn.linear[id(m.y)], (5 + 3 * m.x) * (4 + 8 * m.x) + 21 * m.x
+ )
+ self.assertEqual(len(repn.quadratic), 1)
+ assertExpressionsEqual(
+ self, repn.quadratic[id(m.y), id(m.y)], (4 + 8 * m.x) * 7
+ )
+ self.assertIsNone(repn.nonlinear)
+ assertExpressionsEqual(
+ self,
+ repn.to_expression(visitor),
+ (4 + 8 * m.x) * 7 * m.y**2
+ + ((5 + 3 * m.x) * (4 + 8 * m.x) + 21 * m.x) * m.y
+ + 3 * m.x * (5 + 3 * m.x),
+ )
+
+ def test_product_nonlinear_var_expand_false(self):
+ m = build_test_model()
+ e = (m.x + m.y + log(m.x)) * m.x
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[])
+ visitor.expand_nonlinear_products = False
+ repn = visitor.walk_expression(e)
+
+ NL = (log(m.x) + (m.x + m.y)) * m.x
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y})
+ self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1})
+ self.assertEqual(repn.multiplier, 1)
+ self.assertEqual(repn.constant, 0)
+ self.assertEqual(repn.linear, {})
+ self.assertIsNone(repn.quadratic)
+ assertExpressionsEqual(self, repn.nonlinear, NL)
+ assertExpressionsEqual(self, repn.to_expression(visitor), NL)
+
+ def test_product_nonlinear_var_expand_true(self):
+ m = build_test_model()
+ e = (m.x + m.y + log(m.x)) * m.x
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[])
+ visitor.expand_nonlinear_products = True
+ repn = visitor.walk_expression(e)
+
+ NL = log(m.x) * m.x
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y})
+ self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1})
+ self.assertEqual(repn.multiplier, 1)
+ self.assertEqual(repn.constant, 0)
+ self.assertEqual(repn.linear, {})
+ self.assertEqual(repn.quadratic, {(id(m.x), id(m.x)): 1, (id(m.x), id(m.y)): 1})
+ assertExpressionsEqual(self, repn.nonlinear, NL)
+
+ def test_product_nonlinear_var_2_expand_false(self):
+ m = build_test_model()
+ e = m.x * (m.x + m.y + log(m.x) + 2)
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[])
+ visitor.expand_nonlinear_products = False
+ repn = visitor.walk_expression(e)
+
+ NL = m.x * (log(m.x) + (m.x + m.y) + 2)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y})
+ self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1})
+ self.assertEqual(repn.multiplier, 1)
+ self.assertEqual(repn.constant, 0)
+ self.assertEqual(repn.linear, {})
+ self.assertIsNone(repn.quadratic)
+ assertExpressionsEqual(self, repn.nonlinear, NL)
+ assertExpressionsEqual(self, repn.to_expression(visitor), NL)
+
+ def test_product_nonlinear_var_2_expand_true(self):
+ m = build_test_model()
+ e = m.x * (m.x + m.y + log(m.x) + 2)
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[])
+ visitor.expand_nonlinear_products = True
+ repn = visitor.walk_expression(e)
+
+ NL = m.x * log(m.x)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y})
+ self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1})
+ self.assertEqual(repn.multiplier, 1)
+ self.assertEqual(repn.constant, 0)
+ self.assertEqual(repn.linear, {id(m.x): 2})
+ self.assertEqual(repn.quadratic, {(id(m.x), id(m.x)): 1, (id(m.x), id(m.y)): 1})
+ assertExpressionsEqual(self, repn.nonlinear, NL)
+ assertExpressionsEqual(
+ self, repn.to_expression(visitor), m.x**2 + m.x * m.y + NL + 2 * m.x
+ )
+
+ def test_zero_elimination(self):
+ m = ConcreteModel()
+ m.x = Var(range(4))
+ e = 0 * m.x[0] + 0 * m.x[1] * m.x[2] + 0 * log(m.x[3])
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[])
+ repn = visitor.walk_expression(e)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(
+ cfg.var_map,
+ {
+ id(m.x[0]): m.x[0],
+ id(m.x[1]): m.x[1],
+ id(m.x[2]): m.x[2],
+ id(m.x[3]): m.x[3],
+ },
+ )
+ self.assertEqual(
+ cfg.var_order, {id(m.x[0]): 0, id(m.x[1]): 1, id(m.x[2]): 2, id(m.x[3]): 3}
+ )
+ self.assertEqual(repn.multiplier, 1)
+ self.assertEqual(repn.constant, 0)
+ self.assertEqual(repn.linear, {})
+ self.assertIsNone(repn.quadratic)
+ self.assertIsNone(repn.nonlinear)
+ assertExpressionsEqual(self, repn.to_expression(visitor), 0)
+
+ def test_uninitialized_param_expansion(self):
+ m = ConcreteModel()
+ m.x = Var(range(4))
+ m.p = Param(mutable=True, within=Any, initialize=None)
+ e = m.p * m.x[0] + m.p * m.x[1] * m.x[2] + m.p * log(m.x[3])
+
+ cfg = VisitorConfig()
+ repn = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]).walk_expression(e)
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(
+ cfg.var_map,
+ {
+ id(m.x[0]): m.x[0],
+ id(m.x[1]): m.x[1],
+ id(m.x[2]): m.x[2],
+ id(m.x[3]): m.x[3],
+ },
+ )
+ self.assertEqual(
+ cfg.var_order, {id(m.x[0]): 0, id(m.x[1]): 1, id(m.x[2]): 2, id(m.x[3]): 3}
+ )
+ self.assertEqual(repn.multiplier, 1)
+ self.assertEqual(repn.constant, 0)
+ self.assertEqual(repn.linear, {id(m.x[0]): InvalidNumber(None)})
+ self.assertEqual(
+ repn.quadratic, {(id(m.x[1]), id(m.x[2])): InvalidNumber(None)}
+ )
+ self.assertEqual(repn.nonlinear, InvalidNumber(None))
+
+ def test_zero_times_var(self):
+ m = build_test_model()
+ e = 0 * m.x
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, [])
+ repn = visitor.walk_expression(e)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x})
+ self.assertEqual(cfg.var_order, {id(m.x): 0})
+ self.assertEqual(repn.multiplier, 1)
+ self.assertEqual(repn.constant, 0)
+ self.assertEqual(repn.linear, {})
+ self.assertIsNone(repn.quadratic)
+ self.assertIsNone(repn.nonlinear)
+ assertExpressionsEqual(self, repn.to_expression(visitor), 0)
+
+ def test_square_linear(self):
+ m = build_test_model()
+ expr = (1 + 3 * m.x + 4 * m.y) ** 2
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, [])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y})
+ self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1})
+ self.assertEqual(repn.multiplier, 1)
+ self.assertEqual(repn.constant, 1)
+ self.assertEqual(repn.linear, {id(m.x): 6, id(m.y): 8})
+ self.assertEqual(
+ repn.quadratic,
+ {(id(m.x), id(m.x)): 9, (id(m.y), id(m.y)): 16, (id(m.x), id(m.y)): 24},
+ )
+ self.assertEqual(repn.nonlinear, None)
+ assertExpressionsEqual(
+ self,
+ repn.to_expression(visitor),
+ 9 * m.x**2 + 24 * (m.x * m.y) + 16 * m.y**2 + (6 * m.x + 8 * m.y) + 1,
+ )
+
+ def test_square_linear_wrt_y(self):
+ m = build_test_model()
+ expr = (1 + 3 * m.x + 4 * m.y) ** 2
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.y, m.z])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x})
+ self.assertEqual(cfg.var_order, {id(m.x): 0})
+ self.assertEqual(repn.multiplier, 1)
+ assertExpressionsEqual(self, repn.constant, (1 + 4 * m.y) * (1 + 4 * m.y))
+ self.assertEqual(len(repn.linear), 1)
+ assertExpressionsEqual(
+ self, repn.linear[id(m.x)], (1 + 4 * m.y) * 3 + (1 + 4 * m.y) * 3
+ )
+ self.assertEqual(repn.quadratic, {(id(m.x), id(m.x)): 9})
+ self.assertEqual(repn.nonlinear, None)
+ assertExpressionsEqual(
+ self,
+ repn.to_expression(visitor),
+ (
+ 9 * m.x**2
+ + ((1 + 4 * m.y) * 3 + (1 + 4 * m.y) * 3) * m.x
+ + ((1 + 4 * m.y) * (1 + 4 * m.y))
+ ),
+ )
+
+ def test_square_linear_float(self):
+ m = build_test_model()
+ expr = (1 + 3 * m.x + 4 * m.y) ** 2.0
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, [])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y})
+ self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1})
+ self.assertEqual(repn.multiplier, 1)
+ self.assertEqual(repn.constant, 1)
+ self.assertEqual(repn.linear, {id(m.x): 6, id(m.y): 8})
+ self.assertEqual(
+ repn.quadratic,
+ {(id(m.x), id(m.x)): 9, (id(m.y), id(m.y)): 16, (id(m.x), id(m.y)): 24},
+ )
+ self.assertEqual(repn.nonlinear, None)
+ assertExpressionsEqual(
+ self,
+ repn.to_expression(visitor),
+ 9 * m.x**2 + 24 * (m.x * m.y) + 16 * m.y**2 + (6 * m.x + 8 * m.y) + 1,
+ )
+
+ def test_division_quadratic_nonlinear(self):
+ m = build_test_model()
+ expr = (1 + 3 * m.x + 4 * log(m.x) * m.y + 4 * m.y**2) / (2 * m.x)
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, [])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y})
+ self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1})
+ self.assertEqual(repn.multiplier, 1)
+ self.assertEqual(repn.constant, 0)
+ self.assertIsNone(repn.quadratic)
+ assertExpressionsEqual(
+ self,
+ repn.nonlinear,
+ (4 * m.y**2 + 4 * (log(m.x) * m.y) + 3 * m.x + 1) / (2 * m.x),
+ )
+ assertExpressionsEqual(self, repn.to_expression(visitor), repn.nonlinear)
+
+ def test_division_quadratic_nonlinear_wrt_x(self):
+ m = build_test_model()
+ expr = (1 + 3 * m.x + 4 * log(m.x) * m.y + 4 * m.y**2) / (2 * m.x)
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.x])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.y): m.y})
+ self.assertEqual(cfg.var_order, {id(m.y): 0})
+ self.assertEqual(repn.multiplier, 1)
+ assertExpressionsEqual(self, repn.constant, (1 + 3 * m.x) * (1 / (2 * m.x)))
+ self.assertEqual(len(repn.linear), 1)
+ assertExpressionsEqual(
+ self, repn.linear[id(m.y)], (1 / (2 * m.x)) * (4 * log(m.x))
+ )
+ self.assertEqual(len(repn.quadratic), 1)
+ assertExpressionsEqual(
+ self, repn.quadratic[id(m.y), id(m.y)], (1 / (2 * m.x)) * 4
+ )
+ self.assertEqual(repn.nonlinear, None)
+ assertExpressionsEqual(
+ self,
+ repn.to_expression(visitor),
+ ((1 / (2 * m.x)) * 4) * m.y**2
+ + ((1 / (2 * m.x)) * (4 * log(m.x))) * m.y
+ + (1 + 3 * m.x) * (1 / (2 * m.x)),
+ )
+
+ def test_constant_expr_multiplier(self):
+ m = build_test_model()
+ expr = 5 * (2 * m.x + m.x**2)
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, [])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x})
+ self.assertEqual(cfg.var_order, {id(m.x): 0})
+ self.assertEqual(repn.multiplier, 1)
+ self.assertEqual(repn.constant, 0)
+ self.assertEqual(repn.linear, {id(m.x): 10})
+ self.assertEqual(repn.quadratic, {(id(m.x), id(m.x)): 5})
+ self.assertIsNone(repn.nonlinear)
+ assertExpressionsEqual(self, repn.to_expression(visitor), 5 * m.x**2 + 10 * m.x)
+
+ def test_0_mult_nan_linear_coeff(self):
+ m = build_test_model()
+ expr = 0 * (float("nan") * m.x + m.y + log(m.x) + m.y * m.x**2 + 2 * m.x)
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x})
+ self.assertEqual(cfg.var_order, {id(m.x): 0})
+ self.assertEqual(repn.multiplier, 1)
+ assertExpressionsEqual(self, repn.constant, 0 * m.y)
+ self.assertEqual(len(repn.linear), 1)
+ assertExpressionsEqual(self, repn.linear[id(m.x)], float("nan"))
+ self.assertEqual(len(repn.quadratic), 1)
+ assertExpressionsEqual(self, repn.quadratic[id(m.x), id(m.x)], 0 * m.y)
+ assertExpressionsEqual(self, repn.nonlinear, (log(m.x)) * 0)
+ assertExpressionsEqual(
+ self,
+ repn.to_expression(visitor),
+ 0 * m.y * m.x**2 + (log(m.x)) * 0 + float("nan") * m.x + 0 * m.y,
+ )
+
+ def test_0_mult_nan_quadratic_coeff(self):
+ m = build_test_model()
+ expr = 0 * (m.x + m.y + log(m.x) + float("nan") * m.x**2 + 2 * m.x)
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x})
+ self.assertEqual(cfg.var_order, {id(m.x): 0})
+ self.assertEqual(repn.multiplier, 1)
+ assertExpressionsEqual(self, repn.constant, 0 * m.y)
+ self.assertEqual(repn.linear, {id(m.x): 0})
+ self.assertEqual(len(repn.quadratic), 1)
+ assertExpressionsEqual(self, repn.quadratic[id(m.x), id(m.x)], float("nan"))
+ assertExpressionsEqual(self, repn.nonlinear, (log(m.x)) * 0)
+ assertExpressionsEqual(
+ self,
+ repn.to_expression(visitor),
+ float("nan") * m.x**2 + (log(m.x)) * 0 + 0 * m.y,
+ )
+
+ def test_square_quadratic(self):
+ m = build_test_model()
+ expr = (1 + m.x + m.y + m.x**2 + m.x * m.y) ** 2.0
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, [])
+ repn = visitor.walk_expression(expr)
+
+ NL = (m.x**2 + m.x * m.y) * (m.x**2 + m.x * m.y + (m.x + m.y)) + (
+ m.x + m.y
+ ) * (m.x**2 + m.x * m.y)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y})
+ self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1})
+ self.assertEqual(repn.multiplier, 1)
+ self.assertEqual(repn.constant, 1)
+ self.assertEqual(repn.linear, {id(m.x): 2, id(m.y): 2})
+ self.assertEqual(
+ repn.quadratic,
+ {(id(m.x), id(m.x)): 3, (id(m.x), id(m.y)): 4, (id(m.y), id(m.y)): 1},
+ )
+ assertExpressionsEqual(self, repn.nonlinear, NL)
+ assertExpressionsEqual(
+ self,
+ repn.to_expression(visitor),
+ NL + 3 * m.x**2 + 4 * (m.x * m.y) + m.y**2 + (2 * m.x + 2 * m.y) + 1,
+ )
+
+ def test_square_quadratic_wrt_y(self):
+ m = build_test_model()
+ expr = (1 + m.x + m.y + m.x**2 + m.x * m.y) ** 2.0
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y])
+ repn = visitor.walk_expression(expr)
+
+ NL = SumExpression([m.x**2]) * (m.x**2 + (1 + m.y) * m.x) + (
+ (1 + m.y) * m.x
+ ) * SumExpression([m.x**2])
+ QC = 1 + m.y + 1 + m.y + (1 + m.y) * (1 + m.y)
+ LC = (1 + m.y) * (1 + m.y) + (1 + m.y) * (1 + m.y)
+ CON = (1 + m.y) * (1 + m.y)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x})
+ self.assertEqual(cfg.var_order, {id(m.x): 0})
+ self.assertEqual(repn.multiplier, 1)
+ assertExpressionsEqual(self, repn.constant, (1 + m.y) * (1 + m.y))
+ self.assertEqual(len(repn.linear), 1)
+ assertExpressionsEqual(
+ self, repn.linear[id(m.x)], (1 + m.y) * (1 + m.y) + (1 + m.y) * (1 + m.y)
+ )
+ self.assertEqual(len(repn.quadratic), 1)
+ assertExpressionsEqual(
+ self,
+ repn.quadratic[id(m.x), id(m.x)],
+ 1 + m.y + 1 + m.y + (1 + m.y) * (1 + m.y),
+ )
+ assertExpressionsEqual(self, repn.nonlinear, NL)
+ assertExpressionsEqual(
+ self, repn.to_expression(visitor), NL + QC * m.x**2 + LC * m.x + CON
+ )
+
+ def test_cube_linear(self):
+ m = build_test_model()
+ expr = (1 + m.x + m.y) ** 3
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, [])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y})
+ self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1})
+ self.assertEqual(repn.multiplier, 1)
+ self.assertEqual(repn.constant, 0)
+ self.assertEqual(repn.linear, {})
+ self.assertIsNone(repn.quadratic)
+ # cubic expansion not supported
+ assertExpressionsEqual(self, repn.nonlinear, (m.x + m.y + 1) ** 3)
+ assertExpressionsEqual(self, repn.to_expression(visitor), (m.x + m.y + 1) ** 3)
+
+ def test_nonlinear_product_with_constant_terms(self):
+ m = build_test_model()
+ # test product of nonlinear expressions where one
+ # multiplicand has constant of value 1
+ expr = (1 + log(m.x)) * (log(m.x) + m.y**2)
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.z])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y})
+ self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1})
+ self.assertEqual(repn.multiplier, 1)
+ self.assertEqual(repn.constant, 0)
+ self.assertEqual(repn.linear, {})
+ self.assertEqual(repn.quadratic, {(id(m.y), id(m.y)): 1})
+ assertExpressionsEqual(
+ self, repn.nonlinear, log(m.x) * (m.y**2 + log(m.x)) + log(m.x)
+ )
+ assertExpressionsEqual(
+ self,
+ repn.to_expression(visitor),
+ log(m.x) * (m.y**2 + log(m.x)) + log(m.x) + m.y**2,
+ )
+
+ def test_finalize_simplify_coefficients(self):
+ m = build_test_model()
+ expr = m.x + m.p * m.x**2 + 2 * m.y**2 - m.x - m.p * m.x**2 - m.p * m.z
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.z): m.z})
+ self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.z): 1})
+ self.assertEqual(repn.multiplier, 1)
+ assertExpressionsEqual(self, repn.constant, 2 * m.y**2)
+ self.assertEqual(repn.linear, {id(m.z): -1})
+ self.assertEqual(repn.quadratic, {})
+ self.assertIsNone(repn.nonlinear)
+ assertExpressionsEqual(self, repn.to_expression(visitor), -1 * m.z + 2 * m.y**2)
+
+ def test_factor_multiplier_simplify_coefficients(self):
+ m = build_test_model()
+ expr = 2 * (m.x + m.x**2 + 2 * m.y**2 - m.x - m.x**2 - m.p * m.z)
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y])
+ # this tests case where there are zeros in the `linear`
+ # and `quadratic` dicts of the unfinalized repn
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.z): m.z})
+ self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.z): 1})
+ self.assertEqual(repn.multiplier, 1)
+ self.assertIsNone(repn.nonlinear)
+ self.assertEqual(repn.quadratic, {})
+ self.assertEqual(repn.linear, {id(m.z): -2})
+ assertExpressionsEqual(self, repn.constant, (2 * m.y**2) * 2)
+ assertExpressionsEqual(
+ self, repn.to_expression(visitor), -2 * m.z + (2 * m.y**2) * 2
+ )
+
+ def test_sum_nonlinear_custom_multiplier(self):
+ m = build_test_model()
+ expr = 2 * (1 + log(m.x)) + (2 * (m.y + m.y**2 + log(m.x)))
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x})
+ self.assertEqual(cfg.var_order, {id(m.x): 0})
+ self.assertEqual(repn.multiplier, 1)
+ assertExpressionsEqual(self, repn.constant, 2 + 2 * (m.y + m.y**2))
+ self.assertEqual(repn.linear, {})
+ self.assertIsNone(repn.quadratic)
+ assertExpressionsEqual(self, repn.nonlinear, 2 * log(m.x) + 2 * log(m.x))
+ assertExpressionsEqual(
+ self,
+ repn.to_expression(visitor),
+ 2 * log(m.x) + 2 * log(m.x) + 2 + 2 * (m.y + m.y**2),
+ )
+
+ def test_negation_linear(self):
+ m = build_test_model()
+ expr = -(2 + 3 * m.x + 5 * m.x * m.y)
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x})
+ self.assertEqual(cfg.var_order, {id(m.x): 0})
+ self.assertEqual(repn.multiplier, 1)
+ self.assertEqual(repn.constant, -2)
+ self.assertEqual(len(repn.linear), 1)
+ assertExpressionsEqual(self, repn.linear[id(m.x)], -1 * (3 + 5 * m.y))
+ self.assertIsNone(repn.quadratic)
+ self.assertIsNone(repn.nonlinear)
+ assertExpressionsEqual(
+ self, repn.to_expression(visitor), -1 * (3 + 5 * m.y) * m.x - 2
+ )
+
+ def test_negation_nonlinear_wrt_y_fix_z(self):
+ m = build_test_model()
+ m.z.fix(2)
+ expr = -(
+ 2
+ + 3 * m.x
+ + 4 * m.y * m.z
+ + 5 * m.x**2 * m.y
+ + 6 * m.x * (m.z - 2)
+ + m.z**2
+ + m.z * log(m.x)
+ )
+
+ cfg = VisitorConfig()
+ # note: variable fixing takes precedence over inclusion in
+ # the `wrt` list; that is tested here
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.y, m.z])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x})
+ self.assertEqual(cfg.var_order, {id(m.x): 0})
+ self.assertEqual(repn.multiplier, 1)
+ assertExpressionsEqual(self, repn.constant, (2 + 8 * m.y + 4) * -1)
+ self.assertEqual(repn.linear, {id(m.x): -3})
+ self.assertEqual(len(repn.quadratic), 1)
+ assertExpressionsEqual(self, repn.quadratic[(id(m.x), id(m.x))], -5 * m.y)
+ assertExpressionsEqual(self, repn.nonlinear, 2 * log(m.x) * -1)
+ assertExpressionsEqual(
+ self,
+ repn.to_expression(visitor),
+ +(-5 * m.y) * (m.x**2)
+ + 2 * log(m.x) * -1
+ + (-3) * m.x
+ + (2 + 8 * m.y + 4) * (-1),
+ )
+
+ def test_negation_product_linear_linear(self):
+ m = build_test_model()
+ expr = -(1 + 2 * m.x + 3 * m.y) * (4 + 5 * m.x + 6 * m.y * 7 * m.z)
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.y, m.z])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x})
+ self.assertEqual(cfg.var_order, {id(m.x): 0})
+ self.assertEqual(repn.multiplier, 1)
+ assertExpressionsEqual(
+ self, repn.constant, (1 + 3 * m.y) * (4 + 42 * m.y * m.z) * (-1)
+ )
+ self.assertEqual(len(repn.linear), 1)
+ assertExpressionsEqual(
+ self,
+ repn.linear[id(m.x)],
+ (-1) * ((4 + 42 * m.y * m.z) * 2 + (1 + 3 * m.y) * 5),
+ )
+ self.assertEqual(len(repn.quadratic), 1)
+ assertExpressionsEqual(self, repn.quadratic[id(m.x), id(m.x)], -10)
+ self.assertIsNone(repn.nonlinear)
+ assertExpressionsEqual(
+ self,
+ repn.to_expression(visitor),
+ (
+ -10 * m.x**2
+ + (-1) * ((4 + 42 * m.y * m.z) * 2 + (1 + 3 * m.y) * 5) * m.x
+ + (1 + 3 * m.y) * (4 + 42 * m.y * m.z) * (-1)
+ ),
+ )
+
+ def test_expanded_monomial_square_term(self):
+ m = build_test_model()
+ expr = m.x * m.x * m.p
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.z])
+ # ensure overcomplication issues with standard repn
+ # are not repeated by quadratic repn
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x})
+ self.assertEqual(cfg.var_order, {id(m.x): 0})
+ self.assertEqual(repn.multiplier, 1)
+ self.assertEqual(repn.constant, 0)
+ self.assertEqual(repn.linear, {})
+ self.assertEqual(repn.quadratic, {(id(m.x), id(m.x)): 1})
+ self.assertIsNone(repn.nonlinear)
+ assertExpressionsEqual(
+ self, repn.to_expression(visitor), SumExpression([m.x**2])
+ )
+
+ def test_sum_bilinear_terms_commute_product(self):
+ m = build_test_model()
+ expr = m.x * m.y + m.y * m.x
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.z])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y})
+ self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1})
+ self.assertEqual(repn.multiplier, 1)
+ self.assertEqual(repn.constant, 0)
+ self.assertEqual(repn.linear, {})
+ self.assertEqual(repn.quadratic, {(id(m.x), id(m.y)): 2})
+ self.assertIsNone(repn.nonlinear)
+ assertExpressionsEqual(
+ self, repn.to_expression(visitor), SumExpression([2 * (m.x * m.y)])
+ )
+
+ def test_sum_nonlinear(self):
+ m = build_test_model()
+ expr = (1 + log(m.x)) + (m.x + m.y + m.y**2 + log(m.x))
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z])
+ # tests special case of `repn.append` where multiplier
+ # is 1 and both summands have a nonlinear term
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x})
+ self.assertEqual(cfg.var_order, {id(m.x): 0})
+ self.assertEqual(repn.multiplier, 1)
+ assertExpressionsEqual(self, repn.constant, 1 + m.y + m.y**2)
+ self.assertEqual(repn.linear, {id(m.x): 1})
+ self.assertIsNone(repn.quadratic)
+ assertExpressionsEqual(self, repn.nonlinear, log(m.x) + log(m.x))
+ assertExpressionsEqual(
+ self,
+ repn.to_expression(visitor),
+ log(m.x) + log(m.x) + m.x + (1 + m.y) + m.y**2,
+ )
+
+ def test_product_linear_linear_0_nan(self):
+ m = build_test_model()
+ m.p.set_value(0)
+ expr = (m.p + 0 * m.x) * (float("nan") + float("nan") * m.x)
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x})
+ self.assertEqual(cfg.var_order, {id(m.x): 0})
+ self.assertEqual(repn.multiplier, 1)
+ self.assertTrue(isnan(repn.constant))
+ self.assertEqual(len(repn.linear), 1)
+ self.assertTrue(isnan(repn.linear[id(m.x)]))
+ self.assertIsNone(repn.quadratic)
+ self.assertIsNone(repn.nonlinear)
+ assertExpressionsEqual(
+ self, repn.to_expression(visitor), float("nan") * m.x + float("nan")
+ )
+
+ def test_product_quadratic_quadratic_nan_0(self):
+ m = build_test_model()
+ m.p.set_value(0)
+ expr = (float("nan") + float("nan") * m.x + float("nan") * m.x**2) * (
+ m.p + 0 * m.x + 0 * m.x**2
+ )
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x})
+ self.assertEqual(cfg.var_order, {id(m.x): 0})
+ self.assertEqual(repn.multiplier, 1)
+ self.assertTrue(isnan(repn.constant))
+ self.assertEqual(len(repn.linear), 1)
+ self.assertTrue(isnan(repn.linear[id(m.x)]))
+ self.assertEqual(len(repn.quadratic), 1)
+ self.assertTrue(isnan(repn.quadratic[id(m.x), id(m.x)]))
+ self.assertIsNone(repn.nonlinear)
+ assertExpressionsEqual(
+ self,
+ repn.to_expression(visitor),
+ float("nan") * m.x**2 + float("nan") * m.x + float("nan"),
+ )
+
+ def test_product_quadratic_quadratic_0_nan(self):
+ m = build_test_model()
+ m.p.set_value(0)
+ expr = (m.p + 0 * m.x + 0 * m.x**2) * (
+ float("nan") + float("nan") * m.x + float("nan") * m.x**2
+ )
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x})
+ self.assertEqual(cfg.var_order, {id(m.x): 0})
+ self.assertEqual(repn.multiplier, 1)
+ self.assertTrue(isnan(repn.constant))
+ self.assertEqual(len(repn.linear), 1)
+ self.assertTrue(isnan(repn.linear[id(m.x)]))
+ self.assertEqual(len(repn.quadratic), 1)
+ self.assertTrue(isnan(repn.quadratic[id(m.x), id(m.x)]))
+ self.assertIsNone(repn.nonlinear)
+ assertExpressionsEqual(
+ self,
+ repn.to_expression(visitor),
+ float("nan") * m.x**2 + float("nan") * m.x + float("nan"),
+ )
+
+ def test_nary_sum_products(self):
+ m = build_test_model()
+ expr = (
+ m.x**2 * (m.z - 1)
+ + m.x * (m.y**4 + 0.8)
+ - 5 * m.x * m.y * m.z
+ + m.x * (m.y + 2)
+ )
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x})
+ self.assertEqual(cfg.var_order, {id(m.x): 0})
+ self.assertEqual(repn.multiplier, 1)
+ self.assertEqual(repn.constant, 0)
+ self.assertEqual(len(repn.linear), 1)
+ assertExpressionsEqual(
+ self, repn.linear[id(m.x)], m.y**4 + 0.8 + 5 * m.y * m.z * (-1) + (m.y + 2)
+ )
+ assertExpressionsEqual(self, repn.quadratic[id(m.x), id(m.x)], m.z - 1)
+ self.assertIsNone(repn.nonlinear)
+ assertExpressionsEqual(
+ self,
+ repn.to_expression(visitor),
+ (m.z - 1) * m.x**2
+ + (m.y**4 + 0.8 + 5 * m.y * m.z * (-1) + (m.y + 2)) * m.x,
+ )
+
+ def test_ternary_product_linear(self):
+ m = build_test_model()
+ expr = (1 + 2 * m.x) * (3 + 4 * m.y) * (5 + 6 * m.z)
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.z): m.z})
+ self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.z): 1})
+ self.assertEqual(repn.multiplier, 1)
+ assertExpressionsEqual(self, repn.constant, 5 * (3 + 4 * m.y))
+ self.assertEqual(len(repn.linear), 2)
+ assertExpressionsEqual(self, repn.linear[id(m.x)], (3 + 4 * m.y) * 10)
+ assertExpressionsEqual(self, repn.linear[id(m.z)], (3 + 4 * m.y) * 6)
+ self.assertEqual(len(repn.quadratic), 1)
+ assertExpressionsEqual(
+ self, repn.quadratic[id(m.x), id(m.z)], (3 + 4 * m.y) * 12
+ )
+ self.assertIsNone(repn.nonlinear)
+ assertExpressionsEqual(
+ self,
+ repn.to_expression(visitor),
+ (
+ (3 + 4 * m.y) * 12 * (m.x * m.z)
+ + (3 + 4 * m.y) * 10 * m.x
+ + (3 + 4 * m.y) * 6 * m.z
+ + 5 * (3 + 4 * m.y)
+ ),
+ )
+
+ def test_noninteger_pow_linear(self):
+ m = build_test_model()
+ expr = (1 + 2 * m.x + 3 * m.y) ** 1.5
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x})
+ self.assertEqual(cfg.var_order, {id(m.x): 0})
+ self.assertEqual(repn.multiplier, 1)
+ self.assertEqual(repn.constant, 0)
+ self.assertEqual(repn.linear, {})
+ self.assertIsNone(repn.quadratic)
+ assertExpressionsEqual(self, repn.nonlinear, (1 + 3 * m.y + 2 * m.x) ** 1.5)
+ assertExpressionsEqual(
+ self, repn.to_expression(visitor), (1 + 3 * m.y + 2 * m.x) ** 1.5
+ )
+
+ def test_variable_pow_linear(self):
+ m = build_test_model()
+ expr = (1 + 2 * m.x + 3 * m.y) ** (m.y)
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x})
+ self.assertEqual(cfg.var_order, {id(m.x): 0})
+ self.assertEqual(repn.multiplier, 1)
+ self.assertEqual(repn.constant, 0)
+ self.assertEqual(repn.linear, {})
+ self.assertIsNone(repn.quadratic)
+ assertExpressionsEqual(self, repn.nonlinear, (1 + 3 * m.y + 2 * m.x) ** m.y)
+ assertExpressionsEqual(
+ self, repn.to_expression(visitor), (1 + 3 * m.y + 2 * m.x) ** m.y
+ )
+
+ def test_pow_integer_fixed_var(self):
+ m = build_test_model()
+ m.z.fix(2)
+ expr = (1 + 2 * m.x + 3 * m.y) ** (m.z)
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x})
+ self.assertEqual(cfg.var_order, {id(m.x): 0})
+ self.assertEqual(repn.multiplier, 1)
+ assertExpressionsEqual(self, repn.constant, (1 + 3 * m.y) * (1 + 3 * m.y))
+ self.assertEqual(len(repn.linear), 1)
+ assertExpressionsEqual(
+ self, repn.linear[id(m.x)], (1 + 3 * m.y) * 2 + (1 + 3 * m.y) * 2
+ )
+ self.assertEqual(repn.quadratic, {(id(m.x), id(m.x)): 4})
+ self.assertIsNone(repn.nonlinear)
+ assertExpressionsEqual(
+ self,
+ repn.to_expression(visitor),
+ (
+ 4 * m.x**2
+ + ((1 + 3 * m.y) * 2 + (1 + 3 * m.y) * 2) * m.x
+ + (1 + 3 * m.y) * (1 + 3 * m.y)
+ ),
+ )
+
+ def test_repr_parameterized_quadratic_repn(self):
+ m = build_test_model()
+ expr = 2 + m.x + m.x**2 + log(m.x)
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z])
+ repn = visitor.walk_expression(expr)
+
+ linear_dict = {id(m.x): 1}
+ quad_dict = {(id(m.x), id(m.x)): 1}
+ expected_repn_str = (
+ "ParameterizedQuadraticRepn("
+ "mult=1, "
+ "const=2, "
+ f"linear={linear_dict}, "
+ f"quadratic={quad_dict}, "
+ "nonlinear=log(x))"
+ )
+ self.assertEqual(repr(repn), expected_repn_str)
+ self.assertEqual(str(repn), expected_repn_str)
+
+ def test_product_var_linear_wrt_yz(self):
+ """
+ Test product of Var and quadratic expression.
+
+ Aimed at testing what happens when one multiplicand
+ of a product
+ has a constant term of 0, and the other has a
+ constant term that is an expression.
+ """
+ m = build_test_model()
+ expr = m.x * (m.y + m.x * m.y + m.z)
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x})
+ self.assertEqual(cfg.var_order, {id(m.x): 0})
+ self.assertEqual(repn.multiplier, 1)
+ assertExpressionsEqual(self, repn.constant, 0)
+ self.assertEqual(len(repn.linear), 1)
+ assertExpressionsEqual(self, repn.linear[id(m.x)], m.y + m.z)
+ self.assertEqual(len(repn.quadratic), 1)
+ assertExpressionsEqual(self, repn.quadratic[id(m.x), id(m.x)], m.y)
+ self.assertIsNone(repn.nonlinear)
+ assertExpressionsEqual(
+ self, repn.to_expression(visitor), m.y * m.x**2 + (m.y + m.z) * m.x
+ )
+
+ def test_product_linear_var_wrt_yz(self):
+ """
+ Test product of Var and quadratic expression.
+
+ Checks what happens when multiplicands of
+ `test_product_var_linear` are swapped/commuted.
+ """
+ m = build_test_model()
+ expr = (m.y + m.x * m.y + m.z) * m.x
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x})
+ self.assertEqual(cfg.var_order, {id(m.x): 0})
+ self.assertEqual(repn.multiplier, 1)
+ assertExpressionsEqual(self, repn.constant, 0)
+ self.assertEqual(len(repn.linear), 1)
+ assertExpressionsEqual(self, repn.linear[id(m.x)], m.y + m.z)
+ self.assertEqual(len(repn.quadratic), 1)
+ assertExpressionsEqual(self, repn.quadratic[id(m.x), id(m.x)], m.y)
+ self.assertIsNone(repn.nonlinear)
+ assertExpressionsEqual(
+ self, repn.to_expression(visitor), m.y * m.x**2 + (m.y + m.z) * m.x
+ )
+
+ def test_product_var_quadratic(self):
+ """
+ Test product of Var and quadratic expression.
+
+ Aimed at testing what happens when one multiplicand
+ of a product
+ has a constant term of 0, and the other has a
+ constant term that is an expression.
+ """
+ m = build_test_model()
+ expr = m.x * (m.y + m.x * m.y + m.z)
+
+ cfg = VisitorConfig()
+ visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.z])
+ repn = visitor.walk_expression(expr)
+
+ self.assertEqual(cfg.subexpr, {})
+ self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y})
+ self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1})
+ self.assertEqual(repn.multiplier, 1)
+ assertExpressionsEqual(self, repn.constant, 0)
+ self.assertEqual(len(repn.linear), 1)
+ assertExpressionsEqual(self, repn.linear[id(m.x)], m.z)
+ self.assertEqual(len(repn.quadratic), 1)
+ self.assertEqual(repn.quadratic, {(id(m.x), id(m.y)): 1})
+ assertExpressionsEqual(self, repn.nonlinear, m.x * SumExpression([m.x * m.y]))
+ assertExpressionsEqual(
+ self,
+ repn.to_expression(visitor),
+ m.x * m.y + m.x * SumExpression([m.x * m.y]) + m.z * m.x,
+ )
diff --git a/pyomo/version/info.py b/pyomo/version/info.py
index 825483a70a0..95f7e89a729 100644
--- a/pyomo/version/info.py
+++ b/pyomo/version/info.py
@@ -25,10 +25,10 @@
# should generally be left at 0, unless a downstream package is tracking
# main and needs a hard reference to "suitably new" development.
major = 6
-minor = 7
-micro = 4
-releaselevel = 'invalid'
-# releaselevel = 'final'
+minor = 8
+micro = 0
+# releaselevel = 'invalid'
+releaselevel = 'final'
serial = 0
if releaselevel == 'final':
diff --git a/setup.cfg b/setup.cfg
index f670cef8f68..5b6214d40ab 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,9 +1,6 @@
[metadata]
license_files = LICENSE.md
-[bdist_wheel]
-universal=1
-
[tool:pytest]
filterwarnings = ignore::RuntimeWarning
junit_family = xunit2
diff --git a/setup.py b/setup.py
index 6d28e4d184b..63c6891bda2 100644
--- a/setup.py
+++ b/setup.py
@@ -53,19 +53,30 @@ def get_version():
return import_pyomo_module('pyomo', 'version', 'info.py')['__version__']
+def check_config_arg(name):
+ if name in sys.argv:
+ sys.argv.remove(name)
+ return True
+ if name in os.getenv('PYOMO_SETUP_ARGS', '').split():
+ return True
+ return False
+
+
CYTHON_REQUIRED = "required"
if not any(
- arg.startswith(cmd) for cmd in ('build', 'install', 'bdist') for arg in sys.argv
+ arg.startswith(cmd)
+ for cmd in ('build', 'install', 'bdist', 'wheel')
+ for arg in sys.argv
):
using_cython = False
-else:
+elif sys.version_info[:2] < (3, 11):
using_cython = "automatic"
-if '--with-cython' in sys.argv:
+else:
+ using_cython = False
+if check_config_arg('--with-cython'):
using_cython = CYTHON_REQUIRED
- sys.argv.remove('--with-cython')
-if '--without-cython' in sys.argv:
+if check_config_arg('--without-cython'):
using_cython = False
- sys.argv.remove('--without-cython')
ext_modules = []
if using_cython:
@@ -107,14 +118,7 @@ def get_version():
raise
using_cython = False
-if ('--with-distributable-extensions' in sys.argv) or (
- os.getenv('PYOMO_SETUP_ARGS') is not None
- and '--with-distributable-extensions' in os.getenv('PYOMO_SETUP_ARGS')
-):
- try:
- sys.argv.remove('--with-distributable-extensions')
- except:
- pass
+if check_config_arg('--with-distributable-extensions'):
#
# Import the APPSI extension builder
# NOTE: There is inconsistent behavior in Windows for APPSI.
@@ -262,6 +266,7 @@ def __ne__(self, other):
'optional': [
'dill', # No direct use, but improves lambda pickle
'ipython', # contrib.viewer
+ 'linear-tree', # contrib.piecewise
# Note: matplotlib 3.6.1 has bug #24127, which breaks
# seaborn's histplot (triggering parmest failures)
# Note: minimum version from community_detection use of