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